This commit is contained in:
2025-01-31 19:20:11 +05:00
parent f0dd7ecf94
commit 664e8f5ce1
35 changed files with 577 additions and 565 deletions
+4
View File
@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 14.3889C5.31872 14.3889 4.36111 15.3465 4.36111 16.5278C4.36111 17.7091 5.31872 18.6667 6.5 18.6667C7.68128 18.6667 8.63889 17.7091 8.63889 16.5278C8.63889 15.3465 7.68128 14.3889 6.5 14.3889ZM2.63407 15.6111C3.04789 13.8592 4.6217 12.5556 6.5 12.5556C8.3783 12.5556 9.95211 13.8592 10.3659 15.6111H23V17.4444H10.3659C9.95211 19.1963 8.3783 20.5 6.5 20.5C4.6217 20.5 3.04789 19.1963 2.63407 17.4444H1V15.6111H2.63407Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5 5.83333C18.6813 5.83333 19.6389 6.79095 19.6389 7.97222C19.6389 9.1535 18.6813 10.1111 17.5 10.1111C16.3187 10.1111 15.3611 9.1535 15.3611 7.97222C15.3611 6.79095 16.3187 5.83333 17.5 5.83333ZM21.3659 7.05556C20.9521 5.30368 19.3783 4 17.5 4C15.6217 4 14.0479 5.30368 13.6341 7.05556H1V8.88889H13.6341C14.0479 10.6408 15.6217 11.9444 17.5 11.9444C19.3783 11.9444 20.9521 10.6408 21.3659 8.88889H23V7.05556H21.3659Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+27 -34
View File
@@ -1,18 +1,11 @@
'use client';
import { ArticleActions } from '@/components/ArticleActions';
import { ArticleHeader } from '@/components/ArticleHeader';
import { ArticleContentInput } from '@/components/articleInputs/ArticleContentInput';
import { ArticleSliderInput } from '@/components/articleInputs/ArticleSliderInput';
import { IArticleFormInput } from '@/components/modals/ArticleFormModal';
import { ArticleContent } from '@/components/pages/BlogPage/ArticleContent';
import { IArticle } from '@/types/IArticle';
import { Button } from '@/ui/Button';
import { reorderFields } from '@/utils/reorderFields';
import { Reorder } from 'framer-motion';
import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
import { useState } from 'react';
export default function DashboardArticlePage() {
const { articleId } = useParams<{ articleId: string }>();
@@ -20,13 +13,13 @@ export default function DashboardArticlePage() {
const [showPreview, setShowPreview] = useState(false);
const [article, setArticle] = useState<IArticle>();
const { control, handleSubmit, setValue, getValues, watch } =
useForm<IArticleFormInput>();
// const { control, handleSubmit, setValue, getValues, watch } =
// useForm<IArticleFormInput>();
const { fields, append, swap, remove, insert } = useFieldArray({
name: 'blocks',
control,
});
// const { fields, append, swap, remove, insert } = useFieldArray({
// name: 'blocks',
// control,
// });
// useGetArticleByIdQuery({
// variables: { id: +articleId },
@@ -55,22 +48,22 @@ export default function DashboardArticlePage() {
// const client = useApolloClient();
const [title, description, createdAt, cardImage, tags, blocks] = useWatch({
name: ['title', 'description', 'createdAt', 'cardImage', 'tags', 'blocks'],
control,
});
// const [title, description, createdAt, cardImage, tags, blocks] = useWatch({
// name: ['title', 'description', 'createdAt', 'cardImage', 'tags', 'blocks'],
// control,
// });
useEffect(() => {
setArticle({
title,
description,
createdAt: new Date(createdAt),
cardImage,
tags,
blocks,
id: articleId,
});
}, [title, description, createdAt, cardImage, tags, blocks, articleId]);
// useEffect(() => {
// setArticle({
// title,
// description,
// createdAt: new Date(createdAt),
// cardImage,
// tags,
// blocks,
// id: articleId,
// });
// }, [title, description, createdAt, cardImage, tags, blocks, articleId]);
async function onSubmit() {
if (!article) return;
@@ -81,7 +74,7 @@ export default function DashboardArticlePage() {
// blocks: JSON.stringify(getValues('blocks')),
// });
// client.refetchQueries({ include: ['GetArticlesCount'] });
window.location.href = '/blog';
// window.location.href = '/blog';
}
if (!article) return <div>not found</div>;
@@ -98,9 +91,9 @@ export default function DashboardArticlePage() {
</div>
) : (
<div className="space-y-4">
<ArticleActions {...{ ...article, setValue }} />
{/* <ArticleActions {...{ ...article, setValue }} /> */}
{article.title && <ArticleHeader {...article} />}
<form className="space-y-5" onSubmit={handleSubmit(onSubmit)}>
{/* <form className="space-y-5" onSubmit={handleSubmit(onSubmit)}>
<div className="flex gap-x-4 absolute top-4">
<Button type="submit">Сохранить</Button>
<Button onClick={() => setShowPreview(true)}>
@@ -132,7 +125,7 @@ export default function DashboardArticlePage() {
onReorder={reorderFields(fields, swap)}
className="grid grid-cols-4 gap-y-20"
>
{fields.map((item, index) =>
{/* {fields.map((item, index) =>
item.type === 'Slider' ? (
<ArticleSliderInput
{...{ control, index, item, setValue, remove }}
@@ -154,7 +147,7 @@ export default function DashboardArticlePage() {
)
)}
</Reorder.Group>
</form>
</form> */}
</div>
)}
</>
+2 -17
View File
@@ -3,8 +3,10 @@ import { Awards } from '@/components/pages/MainPage/Awards';
import { Calculator } from '@/components/pages/MainPage/Calculator/Calculator';
import { Clients } from '@/components/pages/MainPage/Clients';
import { ProjectsMap } from '@/components/pages/MainPage/Map/ProjectsMap';
import { Motivation } from '@/components/pages/MainPage/Motivation';
import { Presentation } from '@/components/pages/MainPage/Presentation/Presentation';
import { Projects } from '@/components/pages/MainPage/Projects';
import { Reviews } from '@/components/pages/MainPage/Reviews/Reviews';
import { Statistics } from '@/components/pages/MainPage/Statistics';
import { Streaming } from '@/components/pages/MainPage/Streaming/Streaming';
import { ICompany } from '@/types/ICompany';
@@ -14,23 +16,6 @@ import {
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query';
import dynamic from 'next/dynamic';
const Motivation = dynamic(
() =>
import('@/components/pages/MainPage/Motivation').then(
(mod) => mod.Motivation
),
{ ssr: false }
);
const Reviews = dynamic(
() =>
import('@/components/pages/MainPage/Reviews/Reviews').then(
(mod) => mod.Reviews
),
{ ssr: false }
);
export default async function HomePage() {
const queryClient = new QueryClient();
+7 -1
View File
@@ -1,6 +1,7 @@
import { ModalContainer } from '@/components/Layout/ModalContainer';
import { Providers } from '@/lib/Providers';
// import { Providers } from '@/lib/Providers';
import type { Metadata } from 'next';
import dynamic from 'next/dynamic';
import Script from 'next/script';
import './globals.css';
@@ -26,6 +27,11 @@ export const metadata: Metadata = {
},
};
const Providers = dynamic(
() => import('@/lib/Providers').then((mod) => mod.Providers),
{ ssr: false }
);
export default function RootLayout({
children,
}: Readonly<{
+83 -83
View File
@@ -1,90 +1,90 @@
'use client';
// 'use client';
import { useModalStore } from '@/stores/useModalStore';
import { IArticle } from '@/types/IArticle';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { usePathname, useRouter } from 'next/navigation';
import { UseFormSetValue } from 'react-hook-form';
import { ArticleFormModal, IArticleInput } from './modals/ArticleFormModal';
import { DeleteArticleModal } from './modals/DeleteArticleModal';
// import { useModalStore } from '@/stores/useModalStore';
// import { IArticle } from '@/types/IArticle';
// import { Button } from '@/ui/Button';
// import { Icon } from '@/ui/Icon';
// import { usePathname, useRouter } from 'next/navigation';
// import { UseFormSetValue } from 'react-hook-form';
// import { ArticleFormModal, IArticleInput } from './modals/ArticleFormModal';
// import { DeleteArticleModal } from './modals/DeleteArticleModal';
export function ArticleActions({
blocks,
cardImage,
createdAt,
tags,
title,
id,
setValue,
inCard = false,
}: IArticle & {
setValue?: UseFormSetValue<IArticleInput>;
inCard?: boolean;
}) {
const { push } = useRouter();
// export function ArticleActions({
// blocks,
// cardImage,
// createdAt,
// tags,
// title,
// id,
// setValue,
// inCard = false,
// }: IArticle & {
// setValue?: UseFormSetValue<IArticleInput>;
// inCard?: boolean;
// }) {
// const { push } = useRouter();
const { setModal } = useModalStore();
// const { setModal } = useModalStore();
function handleEditArticle() {
if (setValue)
setModal(
<ArticleFormModal
set={setValue}
defaultValues={{
cardImage,
createdAt: createdAt.toISOString().split('T')[0],
tags,
title,
blocks,
}}
/>,
'editArticle'
);
else push(`/blog/${id}/edit`);
}
// function handleEditArticle() {
// if (setValue)
// setModal(
// <ArticleFormModal
// set={setValue}
// defaultValues={{
// cardImage,
// createdAt: createdAt.toISOString().split('T')[0],
// tags,
// title,
// blocks,
// }}
// />,
// 'editArticle'
// );
// else push(`/blog/${id}/edit`);
// }
function handleDeleteArticle() {
setModal(<DeleteArticleModal id={id} />, 'deleteArticle');
}
// function handleDeleteArticle() {
// setModal(<DeleteArticleModal id={id} />, 'deleteArticle');
// }
const pathname = usePathname();
// const pathname = usePathname();
return (
<div
className={`absolute top-4 right-0 p-4 gap-2 z-[5] ${
inCard ? 'group-hover:flex hidden' : 'flex'
}`}
>
{inCard ? (
<>
<button
onClick={handleEditArticle}
className="bg-opacity-60 hover:bg-opacity-70 relative p-2 transition-opacity bg-black rounded-full"
>
<Icon name="edit" color="white" />
</button>
<button
onClick={handleDeleteArticle}
className="bg-opacity-60 hover:bg-opacity-70 relative p-2 transition-opacity bg-black rounded-full"
>
<Icon name="trash" color="white" />
</button>
</>
) : (
<>
<Button onClick={handleEditArticle}>
Редактировать {pathname.endsWith('edit') ? 'карточку' : 'статью'}
</Button>
<Button
color="secondary"
className="bg-[red]"
onClick={handleDeleteArticle}
>
Удалить
</Button>
</>
)}
</div>
);
}
// return (
// <div
// className={`absolute top-4 right-0 p-4 gap-2 z-[5] ${
// inCard ? 'group-hover:flex hidden' : 'flex'
// }`}
// >
// {inCard ? (
// <>
// <button
// onClick={handleEditArticle}
// className="bg-opacity-60 hover:bg-opacity-70 relative p-2 transition-opacity bg-black rounded-full"
// >
// <Icon name="edit" color="white" />
// </button>
// <button
// onClick={handleDeleteArticle}
// className="bg-opacity-60 hover:bg-opacity-70 relative p-2 transition-opacity bg-black rounded-full"
// >
// <Icon name="trash" color="white" />
// </button>
// </>
// ) : (
// <>
// <Button onClick={handleEditArticle}>
// Редактировать {pathname.endsWith('edit') ? 'карточку' : 'статью'}
// </Button>
// <Button
// color="secondary"
// className="bg-[red]"
// onClick={handleDeleteArticle}
// >
// Удалить
// </Button>
// </>
// )}
// </div>
// );
// }
+60 -60
View File
@@ -1,66 +1,66 @@
import { useModalStore } from '@/stores/useModalStore';
import { IContent } from '@/types/IArticle';
import { Icon } from '@/ui/Icon';
import { RefObject, SyntheticEvent } from 'react';
import {
Control,
UseFieldArrayInsert,
UseFieldArrayRemove,
} from 'react-hook-form';
import { ArticleContentEditorModal } from './modals/ArticleContentEditorModal';
import { IArticleFormInput } from './modals/ArticleFormModal';
// import { useModalStore } from '@/stores/useModalStore';
// import { IContent } from '@/types/IArticle';
// import { Icon } from '@/ui/Icon';
// import { RefObject, SyntheticEvent } from 'react';
// import {
// Control,
// UseFieldArrayInsert,
// UseFieldArrayRemove,
// } from 'react-hook-form';
// import { ArticleContentEditorModal } from './modals/ArticleContentEditorModal';
// import { IArticleFormInput } from './modals/ArticleFormModal';
interface IBlockActionsProps {
item: IContent & Record<'id', string>;
index: number;
control: Control<IArticleFormInput, any>;
remove: UseFieldArrayRemove;
insert: UseFieldArrayInsert<IArticleFormInput, 'blocks'>;
btnref: RefObject<HTMLButtonElement>;
}
// interface IBlockActionsProps {
// item: IContent & Record<'id', string>;
// index: number;
// control: Control<IArticleFormInput, any>;
// remove: UseFieldArrayRemove;
// insert: UseFieldArrayInsert<IArticleFormInput, 'blocks'>;
// btnref: RefObject<HTMLButtonElement>;
// }
export function BlockActions({
control,
index,
item,
remove,
insert,
btnref,
}: IBlockActionsProps) {
const { setModal } = useModalStore();
// export function BlockActions({
// control,
// index,
// item,
// remove,
// insert,
// btnref,
// }: IBlockActionsProps) {
// const { setModal } = useModalStore();
function handleEditBlock(e: SyntheticEvent) {
e.preventDefault();
setModal(
<ArticleContentEditorModal {...{ control, index, item }} />,
'content'
);
}
// function handleEditBlock(e: SyntheticEvent) {
// e.preventDefault();
// setModal(
// <ArticleContentEditorModal {...{ control, index, item }} />,
// 'content'
// );
// }
function handleDublicateBlock(e: SyntheticEvent) {
e.preventDefault();
insert(index, item);
}
// function handleDublicateBlock(e: SyntheticEvent) {
// e.preventDefault();
// insert(index, item);
// }
function handleRemoveBlock(e: SyntheticEvent) {
e.preventDefault();
remove(index);
}
// function handleRemoveBlock(e: SyntheticEvent) {
// e.preventDefault();
// remove(index);
// }
return (
<div className="flex gap-x-4 absolute lg:-right-[150px] max-lg:top-full">
<button ref={btnref} onClick={handleEditBlock}>
<Icon name="edit" color="white" />
</button>
<button
className="border p-2 rounded-full"
onClick={handleDublicateBlock}
>
<Icon name="add" color="white" />
</button>
<button onClick={handleRemoveBlock}>
<Icon name="trash" color="white" />
</button>
</div>
);
}
// return (
// <div className="flex gap-x-4 absolute lg:-right-[150px] max-lg:top-full">
// <button ref={btnref} onClick={handleEditBlock}>
// <Icon name="edit" color="white" />
// </button>
// <button
// className="border p-2 rounded-full"
// onClick={handleDublicateBlock}
// >
// <Icon name="add" color="white" />
// </button>
// <button onClick={handleRemoveBlock}>
// <Icon name="trash" color="white" />
// </button>
// </div>
// );
// }
+7 -5
View File
@@ -16,6 +16,8 @@ import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useRef, useState } from 'react';
import { useOnClickOutside } from 'usehooks-ts';
import BurgerIcon from '../../../public/icons/burger.svg';
import CloseIcon from '../../../public/icons/close.svg';
import { LogoIcon } from '../icons/LogoIcon';
import { LogoWithTextIcon } from '../icons/LogoWithTextIcon';
import { ModalWithFeedbackForm } from '../modals/ModalWithFeedbackForm';
@@ -131,11 +133,11 @@ export function Header() {
className="!border-none p-[18px] hover:bg-[#232425] rounded-2xl active:opacity-50 outline-none"
onClick={() => setBurgerOpened((prev) => !prev)}
>
<Icon
name={burgerOpened ? 'close' : 'burger'}
color="white"
size={16}
/>
{burgerOpened ? (
<CloseIcon className="w-4 h-4" />
) : (
<BurgerIcon className="w-4 h-4" />
)}
</button>
</div>
{auth && (
+7 -6
View File
@@ -1,4 +1,3 @@
import { Icon } from '@/ui/Icon';
import countries from 'countries-phone-masks';
import {
CountryCode,
@@ -8,6 +7,8 @@ import {
import Image from 'next/image';
import { SyntheticEvent, useRef, useState } from 'react';
import { useOnClickOutside } from 'usehooks-ts';
import ChevronDownIcon from '../../../public/icons/chevron_down.svg';
import ChevronUpIcon from '../../../public/icons/chevron_up.svg';
export function SelectPhoneCode({
currentPhoneCodeAndCountry: [currentPhoneCode, currentCountry],
@@ -47,11 +48,11 @@ export function SelectPhoneCode({
sizes=""
/>
<p className="btnl font-medium">{currentPhoneCode}</p>
<Icon
name={open ? 'chevron_up' : 'chevron_down'}
color="white"
svgProp={{ className: 'max-sm:w-4 sm:max-lg:w-5 flex-1' }}
/>
{open ? (
<ChevronUpIcon className="max-sm:w-4 sm:max-lg:w-5 flex-1 text-white" />
) : (
<ChevronDownIcon className="max-sm:w-4 sm:max-lg:w-5 flex-1 text-white" />
)}
</button>
{open && (
<div className="space-y-1 absolute z-10 bg-[#14161F] top-[100%] -left-1 border border-t-0 rounded-b-lg border-[#37393B] max-h-[300px] overflow-y-auto overflow-x-hidden">
@@ -1,61 +1,61 @@
import { IContent } from '@/types/IArticle';
import { Reorder } from 'framer-motion';
import parse from 'html-react-parser';
import { useEffect, useRef, useState } from 'react';
import {
Control,
UseFieldArrayInsert,
UseFieldArrayRemove,
UseFormSetValue,
UseFormWatch,
} from 'react-hook-form';
import { BlockActions } from '../BlockActions';
import { IArticleFormInput } from '../modals/ArticleFormModal';
// import { IContent } from '@/types/IArticle';
// import { Reorder } from 'framer-motion';
// import parse from 'html-react-parser';
// import { useEffect, useRef, useState } from 'react';
// import {
// Control,
// UseFieldArrayInsert,
// UseFieldArrayRemove,
// UseFormSetValue,
// UseFormWatch,
// } from 'react-hook-form';
// import { BlockActions } from '../BlockActions';
// import { IArticleFormInput } from '../modals/ArticleFormModal';
export interface IArticleContentInputProps {
item: IContent & Record<'id', string>;
index: number;
control: Control<IArticleFormInput, any>;
setValue: UseFormSetValue<IArticleFormInput>;
watch: UseFormWatch<IArticleFormInput>;
remove: UseFieldArrayRemove;
insert: UseFieldArrayInsert<IArticleFormInput, 'blocks'>;
}
// export interface IArticleContentInputProps {
// item: IContent & Record<'id', string>;
// index: number;
// control: Control<IArticleFormInput, any>;
// setValue: UseFormSetValue<IArticleFormInput>;
// watch: UseFormWatch<IArticleFormInput>;
// remove: UseFieldArrayRemove;
// insert: UseFieldArrayInsert<IArticleFormInput, 'blocks'>;
// }
export function ArticleContentInput({
control,
index,
item,
setValue,
watch,
remove,
insert,
}: IArticleContentInputProps) {
const [content, setContent] = useState(item.content);
// export function ArticleContentInput({
// control,
// index,
// item,
// setValue,
// watch,
// remove,
// insert,
// }: IArticleContentInputProps) {
// const [content, setContent] = useState(item.content);
useEffect(() => {
const { unsubscribe } = watch((value) => {
if (value.blocks?.[index]?.type === 'Content')
setContent(value.blocks[index].content!);
});
return unsubscribe;
}, [index, setValue, watch]);
// useEffect(() => {
// const { unsubscribe } = watch((value) => {
// if (value.blocks?.[index]?.type === 'Content')
// setContent(value.blocks[index].content!);
// });
// return unsubscribe;
// }, [index, setValue, watch]);
const ref = useRef<HTMLButtonElement>(null);
// const ref = useRef<HTMLButtonElement>(null);
return (
<Reorder.Item
as="div"
value={item}
className="border-[#3D425C] border p-4 rounded-3xl relative flex lg:col-start-2 lg:col-span-2 sm:col-span-3 col-span-full"
onDoubleClick={() => ref.current && ref.current?.click()}
>
<div className="no-tailwindcss-base">{parse(content)}</div>
<BlockActions
btnref={ref}
{...{ control, index, remove, insert }}
item={{ ...item, content }}
/>
</Reorder.Item>
);
}
// return (
// <Reorder.Item
// as="div"
// value={item}
// className="border-[#3D425C] border p-4 rounded-3xl relative flex lg:col-start-2 lg:col-span-2 sm:col-span-3 col-span-full"
// onDoubleClick={() => ref.current && ref.current?.click()}
// >
// <div className="no-tailwindcss-base">{parse(content)}</div>
// <BlockActions
// btnref={ref}
// {...{ control, index, remove, insert }}
// item={{ ...item, content }}
// />
// </Reorder.Item>
// );
// }
@@ -1,42 +1,42 @@
import { Icon } from '@/ui/Icon';
import { Reorder } from 'framer-motion';
import { SyntheticEvent } from 'react';
import { UseFieldArrayRemove, UseFormSetValue } from 'react-hook-form';
import { ImageUploader } from '../ImageUploader';
import { IArticleFormInput } from '../modals/ArticleFormModal';
// import { Icon } from '@/ui/Icon';
// import { Reorder } from 'framer-motion';
// import { SyntheticEvent } from 'react';
// import { UseFieldArrayRemove, UseFormSetValue } from 'react-hook-form';
// import { ImageUploader } from '../ImageUploader';
// import { IArticleFormInput } from '../modals/ArticleFormModal';
interface IArticleSliderImageInputProps {
setValue: UseFormSetValue<IArticleFormInput>;
remove: UseFieldArrayRemove;
item: Record<'img', string> & Record<'id', string>;
index: number;
imgIndex: number;
}
// interface IArticleSliderImageInputProps {
// setValue: UseFormSetValue<IArticleFormInput>;
// remove: UseFieldArrayRemove;
// item: Record<'img', string> & Record<'id', string>;
// index: number;
// imgIndex: number;
// }
export function ArticleSliderImageInput({
item,
imgIndex,
index,
remove,
setValue,
}: IArticleSliderImageInputProps) {
function handleClickClose(e: SyntheticEvent) {
e.preventDefault();
remove(imgIndex);
}
// export function ArticleSliderImageInput({
// item,
// imgIndex,
// index,
// remove,
// setValue,
// }: IArticleSliderImageInputProps) {
// function handleClickClose(e: SyntheticEvent) {
// e.preventDefault();
// remove(imgIndex);
// }
return (
<Reorder.Item value={item} className="flex items-center" drag as="div">
<ImageUploader
dest="blog"
setValue={setValue}
fieldName={`blocks.${index}.images.${imgIndex}.img`}
item={item}
label="Выберите изображение"
/>
<button className="self-start z-[1]" onClick={handleClickClose}>
<Icon name="close" color="white" />
</button>
</Reorder.Item>
);
}
// return (
// <Reorder.Item value={item} className="flex items-center" drag as="div">
// <ImageUploader
// dest="blog"
// setValue={setValue}
// fieldName={`blocks.${index}.images.${imgIndex}.img`}
// item={item}
// label="Выберите изображение"
// />
// <button className="self-start z-[1]" onClick={handleClickClose}>
// <Icon name="close" color="white" />
// </button>
// </Reorder.Item>
// );
// }
@@ -1,75 +1,75 @@
import { ISlider } from '@/types/IArticle';
import { Icon } from '@/ui/Icon';
import { reorderFields } from '@/utils/reorderFields';
import { Reorder } from 'framer-motion';
import { SyntheticEvent } from 'react';
import {
Control,
useFieldArray,
UseFieldArrayRemove,
UseFormSetValue,
} from 'react-hook-form';
import { IArticleFormInput } from '../modals/ArticleFormModal';
import { ArticleSliderImageInput } from './ArticleSliderImageInput';
// import { ISlider } from '@/types/IArticle';
// import { Icon } from '@/ui/Icon';
// import { reorderFields } from '@/utils/reorderFields';
// import { Reorder } from 'framer-motion';
// import { SyntheticEvent } from 'react';
// import {
// Control,
// useFieldArray,
// UseFieldArrayRemove,
// UseFormSetValue,
// } from 'react-hook-form';
// import { IArticleFormInput } from '../modals/ArticleFormModal';
// import { ArticleSliderImageInput } from './ArticleSliderImageInput';
interface IArticleSliderInputProps {
setValue: UseFormSetValue<IArticleFormInput>;
item: ISlider & Record<'id', string>;
index: number;
control: Control<IArticleFormInput, any>;
remove: UseFieldArrayRemove;
}
// interface IArticleSliderInputProps {
// setValue: UseFormSetValue<IArticleFormInput>;
// item: ISlider & Record<'id', string>;
// index: number;
// control: Control<IArticleFormInput, any>;
// remove: UseFieldArrayRemove;
// }
export function ArticleSliderInput({
index,
item,
setValue,
control,
remove: removeSlider,
}: IArticleSliderInputProps) {
const { swap, append, remove, fields } = useFieldArray({
control,
name: `blocks.${index}.images`,
});
// export function ArticleSliderInput({
// index,
// item,
// setValue,
// control,
// remove: removeSlider,
// }: IArticleSliderInputProps) {
// const { swap, append, remove, fields } = useFieldArray({
// control,
// name: `blocks.${index}.images`,
// });
function handleAddSlide(e: SyntheticEvent) {
e.preventDefault();
append({ img: '' });
}
// function handleAddSlide(e: SyntheticEvent) {
// e.preventDefault();
// append({ img: '' });
// }
function handleRemoveSlider(e: SyntheticEvent) {
e.preventDefault();
removeSlider(index);
}
// function handleRemoveSlider(e: SyntheticEvent) {
// e.preventDefault();
// removeSlider(index);
// }
return (
<Reorder.Item
value={item}
as="div"
className="border border-[#3D425C] rounded-3xl p-4 col-span-full space-y-4"
>
<div className="flex gap-x-4 justify-center">
<button onClick={handleAddSlide}>
<Icon name="add" color="white" />
</button>
<button onClick={handleRemoveSlider}>
<Icon name="trash" color="white" />
</button>
</div>
<Reorder.Group
as="div"
axis="x"
values={fields}
onReorder={reorderFields(fields, swap)}
className="flex gap-4 flex-wrap"
>
{fields.map((item, imgIndex) => (
<ArticleSliderImageInput
key={item.id}
{...{ imgIndex, item, setValue, remove, index }}
/>
))}
</Reorder.Group>
</Reorder.Item>
);
}
// return (
// <Reorder.Item
// value={item}
// as="div"
// className="border border-[#3D425C] rounded-3xl p-4 col-span-full space-y-4"
// >
// <div className="flex gap-x-4 justify-center">
// <button onClick={handleAddSlide}>
// <Icon name="add" color="white" />
// </button>
// <button onClick={handleRemoveSlider}>
// <Icon name="trash" color="white" />
// </button>
// </div>
// <Reorder.Group
// as="div"
// axis="x"
// values={fields}
// onReorder={reorderFields(fields, swap)}
// className="flex gap-4 flex-wrap"
// >
// {fields.map((item, imgIndex) => (
// <ArticleSliderImageInput
// key={item.id}
// {...{ imgIndex, item, setValue, remove, index }}
// />
// ))}
// </Reorder.Group>
// </Reorder.Item>
// );
// }
@@ -1,132 +1,132 @@
import { api } from '@/api';
import { useModalStore } from '@/stores/useModalStore';
import { Icon } from '@/ui/Icon';
import { Editor } from '@tinymce/tinymce-react';
import { useRef } from 'react';
import { Controller } from 'react-hook-form';
import { Editor as Tinymce } from 'tinymce';
import { IArticleContentInputProps } from '../articleInputs/ArticleContentInput';
// import { api } from '@/api';
// import { useModalStore } from '@/stores/useModalStore';
// import { Icon } from '@/ui/Icon';
// import { Editor } from '@tinymce/tinymce-react';
// import { useRef } from 'react';
// import { Controller } from 'react-hook-form';
// import { Editor as Tinymce } from 'tinymce';
// import { IArticleContentInputProps } from '../articleInputs/ArticleContentInput';
export function ArticleContentEditorModal({
control,
index,
item,
}: Pick<IArticleContentInputProps, 'control' | 'index' | 'item'>) {
const { setModal } = useModalStore();
// export function ArticleContentEditorModal({
// control,
// index,
// item,
// }: Pick<IArticleContentInputProps, 'control' | 'index' | 'item'>) {
// const { setModal } = useModalStore();
const editorRef = useRef<Tinymce | null>(null);
const videoUploadRef = useRef<HTMLInputElement>(null);
// const editorRef = useRef<Tinymce | null>(null);
// const videoUploadRef = useRef<HTMLInputElement>(null);
return (
<div className="relative w-full h-full">
<Controller
control={control}
name={`blocks.${index}.content`}
render={({ field: { onChange, value } }) => (
<Editor
id={item.id}
onEditorChange={onChange}
value={value}
onInit={(_, editor) => {
editorRef.current = editor;
}}
init={{
content_style:
'body {color: #fff; background: #14161f; font-size:16px;display:grid;grid-template-columns:repeat(4,1fr);} body > * {grid-column-start: 2;grid-column-end:4',
height: '100%',
font_size_formats: '10px 12px 14px 16px 18px 20px 24px 28px 30px',
video_template_callback: (data: { source: string }) =>
'<video src="' +
data.source +
'" muted="true" autoplay="true" loop="true" playsinline style="aspect-ratio: 16/9; width: 768px"></video>',
images_upload_credentials: true,
images_upload_handler: async (blobInfo) => {
const formData = new FormData();
formData.append('files', blobInfo.blob(), blobInfo.filename());
formData.append('dest', 'blog');
const res = await api
.post('upload', { body: formData })
.json<{ files: string[] }>();
return process.env.NEXT_PUBLIC_S3_BUCKET + res.files[0];
},
file_picker_types: 'image media',
file_picker_callback: async (cb) => {
videoUploadRef.current!.onchange = async function () {
const reader = new FileReader();
const file = videoUploadRef.current?.files?.[0];
reader.onload = async function () {
const id = 'blobId' + new Date().getTime();
const blobCache = editorRef.current?.editorUpload.blobCache;
const base64 = reader.result?.toString().split(',')[1];
const blobInfo = blobCache?.create(id, file!, base64!);
blobCache?.add(blobInfo!);
const formData = new FormData();
formData.append(
'files',
blobInfo?.blob()!,
blobInfo?.filename()
);
formData.append('dest', 'blog');
const res = await api
.post('upload', { body: formData })
.json<{ files: string[] }>();
// return (
// <div className="relative w-full h-full">
// <Controller
// control={control}
// name={`blocks.${index}.content`}
// render={({ field: { onChange, value } }) => (
// <Editor
// id={item.id}
// onEditorChange={onChange}
// value={value}
// onInit={(_, editor) => {
// editorRef.current = editor;
// }}
// init={{
// content_style:
// 'body {color: #fff; background: #14161f; font-size:16px;display:grid;grid-template-columns:repeat(4,1fr);} body > * {grid-column-start: 2;grid-column-end:4',
// height: '100%',
// font_size_formats: '10px 12px 14px 16px 18px 20px 24px 28px 30px',
// video_template_callback: (data: { source: string }) =>
// '<video src="' +
// data.source +
// '" muted="true" autoplay="true" loop="true" playsinline style="aspect-ratio: 16/9; width: 768px"></video>',
// images_upload_credentials: true,
// images_upload_handler: async (blobInfo) => {
// const formData = new FormData();
// formData.append('files', blobInfo.blob(), blobInfo.filename());
// formData.append('dest', 'blog');
// const res = await api
// .post('upload', { body: formData })
// .json<{ files: string[] }>();
// return process.env.NEXT_PUBLIC_S3_BUCKET + res.files[0];
// },
// file_picker_types: 'image media',
// file_picker_callback: async (cb) => {
// videoUploadRef.current!.onchange = async function () {
// const reader = new FileReader();
// const file = videoUploadRef.current?.files?.[0];
// reader.onload = async function () {
// const id = 'blobId' + new Date().getTime();
// const blobCache = editorRef.current?.editorUpload.blobCache;
// const base64 = reader.result?.toString().split(',')[1];
// const blobInfo = blobCache?.create(id, file!, base64!);
// blobCache?.add(blobInfo!);
// const formData = new FormData();
// formData.append(
// 'files',
// blobInfo?.blob()!,
// blobInfo?.filename()
// );
// formData.append('dest', 'blog');
// const res = await api
// .post('upload', { body: formData })
// .json<{ files: string[] }>();
cb(process.env.NEXT_PUBLIC_S3_BUCKET + res.files[0], {
title: file?.name,
});
};
reader.readAsDataURL(file!);
};
videoUploadRef.current!.click();
},
automatic_uploads: true,
plugins: [
'anchor',
'autolink',
'charmap',
'codesample',
'emoticons',
'image',
'link',
'lists',
'media',
'nonbreaking',
'preview',
'save',
'searchreplace',
'table',
'visualblocks',
'wordcount',
],
toolbar:
'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | link image media table mergetags | addcomment showcomments | spellcheckdialog a11ycheck typography | align lineheight | checklist numlist bullist indent outdent | emoticons charmap | removeformat',
tinycomments_mode: 'embedded',
tinycomments_author: 'Author name',
mergetags_list: [
{ value: 'First.Name', title: 'First Name' },
{ value: 'Email', title: 'Email' },
],
}}
licenseKey="gpl"
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
/>
)}
/>
<input
type="file"
accept="media/*"
className="invisible"
ref={videoUploadRef}
/>
<button
onClick={(e) => {
e.preventDefault();
setModal(null, '');
}}
className="absolute top-4 right-4 z-[2]"
>
<Icon name="close" color="black" />
</button>
</div>
);
}
// cb(process.env.NEXT_PUBLIC_S3_BUCKET + res.files[0], {
// title: file?.name,
// });
// };
// reader.readAsDataURL(file!);
// };
// videoUploadRef.current!.click();
// },
// automatic_uploads: true,
// plugins: [
// 'anchor',
// 'autolink',
// 'charmap',
// 'codesample',
// 'emoticons',
// 'image',
// 'link',
// 'lists',
// 'media',
// 'nonbreaking',
// 'preview',
// 'save',
// 'searchreplace',
// 'table',
// 'visualblocks',
// 'wordcount',
// ],
// toolbar:
// 'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | link image media table mergetags | addcomment showcomments | spellcheckdialog a11ycheck typography | align lineheight | checklist numlist bullist indent outdent | emoticons charmap | removeformat',
// tinycomments_mode: 'embedded',
// tinycomments_author: 'Author name',
// mergetags_list: [
// { value: 'First.Name', title: 'First Name' },
// { value: 'Email', title: 'Email' },
// ],
// }}
// licenseKey="gpl"
// apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
// />
// )}
// />
// <input
// type="file"
// accept="media/*"
// className="invisible"
// ref={videoUploadRef}
// />
// <button
// onClick={(e) => {
// e.preventDefault();
// setModal(null, '');
// }}
// className="absolute top-4 right-4 z-[2]"
// >
// <Icon name="close" color="black" />
// </button>
// </div>
// );
// }
@@ -1,4 +1,3 @@
import { ArticleHeader } from '@/components/ArticleHeader';
import { IArticle } from '@/types/IArticle';
import parse from 'html-react-parser';
import { ArticleSlider } from './ArticleSlider';
@@ -12,7 +11,7 @@ export function ArticleContent({
}: IArticle) {
return (
<>
<ArticleHeader {...{ createdAt, cardImage, tags, title }} />
{/* <ArticleHeader {...{ createdAt, cardImage, tags, title }} /> */}
<div className="space-y-20 lg:pt-20 pt-14 lg:grid grid-cols-4 gap-x-4">
{blocks.map((block, index) =>
block.type === 'Content' ? (
@@ -11,7 +11,6 @@ export function RelevantArticle({
id,
tags,
createdAt,
description,
relevantTags,
}: IArticle & { relevantTags: string[] }) {
const [year, month, date] = new Date(createdAt)
@@ -44,11 +43,6 @@ export function RelevantArticle({
<h3 className="h3 font-medium [-webkit-line-clamp:3] [-webkit-box-orient:vertical] [display:-webkit-box] text-ellipsis overflow-hidden">
{title}
</h3>
{description && (
<p className="m-text font-medium [-webkit-line-clamp:3] [-webkit-box-orient:vertical] [display:-webkit-box] overflow-hidden text-ellipsis">
{description}
</p>
)}
</div>
<div className="flex justify-between items-center">
<p className="l-caption uppercase font-medium">
@@ -1,5 +1,7 @@
import { Icon } from '@/ui/Icon';
import { MouseEvent, useRef, useState } from 'react';
'use client';
import { MouseEvent, Suspense, useRef, useState } from 'react';
import DotsIcon from '../../../../../public/icons/dots.svg';
export function ConsultationRange({
consultations,
@@ -17,7 +19,6 @@ export function ConsultationRange({
const [isMouseDown, setIsMouseDown] = useState(false);
function handleMouseDown(e: MouseEvent) {
console.log(e.clientX - root.current!.getBoundingClientRect().x);
setIsMouseDown(true);
setStart(
Math.max(
@@ -71,12 +72,9 @@ export function ConsultationRange({
onMouseLeave={handleMouseUp}
onMouseUp={handleMouseUp}
>
<Icon
name="dots"
size={24}
color="#7A7A7A"
svgProp={{ className: 'select-none' }}
/>
<Suspense fallback={<></>}>
<DotsIcon className="w-6 h-6 text-[#7a7a7a] select-none" />
</Suspense>
</div>
</div>
<p className="self-center right-8 font-medium text-[#7A7A7A] z-[1] btnl">
@@ -1,7 +1,10 @@
'use client';
import regionsData from '@/consts/regionsData.json';
import { Icon } from '@/ui/Icon';
import { useRef, useState } from 'react';
import { LazySvg } from '@/ui/LazySvg';
import { Suspense, useRef, useState } from 'react';
import { useOnClickOutside } from 'usehooks-ts';
import ChevronDownIcon from '../../../../../public/icons/chevron_down.svg';
export interface Region {
id: number;
@@ -26,19 +29,23 @@ export function RegionSelector({
useOnClickOutside([dropdownRef, root], () => setOpened(false));
return (
<div className="md:space-y-3 max-md:order-2 md:max-lg:flex-1 relative select-none">
<p className="font-medium text-[#7A7A7A] btnl max-md:hidden">Регион</p>
<div className="max-md:order-2 md:max-lg:flex-1 relative select-none">
<p className="font-medium text-[#7A7A7A] btnl max-md:hidden mb-3">
Регион
</p>
<div
ref={root}
className="px-8 lg:py-6 py-4 bg-[#37393B99] rounded-2xl flex items-center justify-between lg:w-[360px]"
onClick={() => setOpened(!opened)}
onClick={() => setOpened((prev) => !prev)}
>
<p className="lg:btnl btnl font-medium">{chosen.name}</p>
<Icon
name={opened ? 'chevron_up' : 'chevron_down'}
color="white"
size={24}
/>
<div
className={
'transition-transform ' + (opened ? 'rotate-180' : 'rotate-0')
}
>
<ChevronDownIcon className="w-6 h-6 text-white" />
</div>
</div>
{opened && (
<div
@@ -46,7 +53,7 @@ export function RegionSelector({
className="bg-[#37393B99] absolute z-10 w-full p-2 backdrop-blur-2xl mt-1 rounded-2xl max-h-96 overflow-y-auto"
>
{(regionsData as Region[]).map((region, index) => (
<p
<div
key={index}
className="hover:backdrop-blur-2xl rounded-xl lg:btnl btnm flex items-center justify-between w-full px-8 py-6 font-medium"
onClick={() => {
@@ -55,10 +62,20 @@ export function RegionSelector({
}}
>
{region.name}
{chosen.name === region.name && (
<Icon name="check" color="white" />
)}
</p>
<div className="relative w-6 h-6">
{chosen.name === region.name && (
<Suspense fallback={<div className="w-6 h-6 absolute" />}>
<LazySvg
name="check"
width={24}
height={24}
color="white"
className="absolute top-0 left-0"
/>
</Suspense>
)}
</div>
</div>
))}
</div>
)}
@@ -109,7 +109,7 @@ export function ProjectsSlider({
setCurrent((prev) => Math.min(projects.length, prev + 1))
}
>
<Icon name="arrow_right" color="white" size={16} />
{/* <Icon name="arrow_right" color="white" size={16} /> */}
</button>
</div>
</div>
@@ -18,14 +18,14 @@ export function Engine({ slide }: { slide?: number }) {
</p>
<div className="flex-nowrap flex gap-2">
{engine.map((item) => (
<SlideBadge {...item} key={item.title} className="pr-6" />
<SlideBadge {...item} key={item.title} className="lg:pr-6" />
))}
</div>
</motion.div>
)}
</AnimatePresence>
) : (
<div className="md:space-y-6 space-y-7 md:h-[348px] max-md:bg-radial-[at_100%_100%] before:absolute before:max-md:w-full before:h-full before:top-0 before:left-0 before:-z-1 before:bg-[#0F1011] before:bg-radial-[at_100%_100%] before:from-[#7a7a7a66] before:rounded-2xl max-md:px-4 max-md:py-6 max-md:rounded-2xl from-[#7A7A7A66] max-md:backdrop-blur-[500px] overflow-hidden sticky md:top-[calc(100vh-388px)] top-100 md:bg-[#0F1011]">
<div className="md:space-y-6 space-y-7 md:h-[348px] max-md:bg-radial-[at_100%_100%] before:absolute before:max-md:w-full before:h-full before:top-0 before:left-0 before:-z-1 before:bg-[#0F1011] before:bg-radial-[at_100%_100%] before:from-[#7a7a7a66] before:rounded-2xl max-md:px-4 max-md:py-6 max-md:rounded-2xl from-[#7A7A7A66] max-md:backdrop-blur-[500px] overflow-hidden sticky md:top-[calc(100vh-388px)] top-120 md:bg-[#0F1011] h-[340px]">
<p className="heading2 md:text-center font-medium">
Модуль инженерных систем
</p>
@@ -36,11 +36,11 @@ export function Engine({ slide }: { slide?: number }) {
key={title}
>
<p className="text1 max-md:hidden">{title}</p>
<div className="relative md:w-[calc(216/240*100%)] max-md:max-w-16 md:self-end">
<div className="relative md:max-lg:w-[calc(216/240*100%)] max-md:max-w-16 md:self-end">
<Image
src={src}
fill
alt={title}
fill
className="rounded-xl md:self-end !relative w-full h-full max-md:aspect-square"
sizes="100%"
/>
@@ -49,7 +49,7 @@ export function Infrastructure({ slide }: { slide?: number }) {
)}
</AnimatePresence>
) : (
<div className="rounded-2xl backdrop-blur-[500px] before:bg-[#0F1011] before:absolute before:w-full before:h-full before:top-0 before:left-0 before:-z-1 before:rounded-2xl before:bg-radial-[at_100%_100%] before:from-[#7A7A7A66] px-4 md:px-6 py-6 md:grid gap-7 space-y-7 grid-rows-[5fr_2fr] grid-cols-2 md:h-[348px] sticky md:top-[calc(100vh-388px)] top-100">
<div className="rounded-2xl backdrop-blur-[500px] before:bg-[#0F1011] before:absolute before:w-full before:h-full before:top-0 before:left-0 before:-z-1 before:rounded-2xl before:bg-radial-[at_100%_100%] before:from-[#7A7A7A66] px-4 md:px-6 py-6 md:grid gap-7 space-y-7 grid-rows-[5fr_2fr] grid-cols-2 md:h-[348px] sticky md:top-[calc(100vh-388px)] top-120 h-[340px] overflow-hidden">
<div className="max-md:space-y-4 gap-7 md:grid grid-cols-2 col-span-2">
<p className="heading2 font-medium">
Демонстрация инфраструктуры вокруг будущего комплекса
@@ -58,7 +58,7 @@ export function Infrastructure({ slide }: { slide?: number }) {
Режим «Инфраструктура» знакомит пользователя с перспективной
застройкой целого района. В зависимости от срока сдачи либо
функциональной нагрузки того или иного блока можно ввести выделение
цветом.
цветом
</p>
</div>
<div className="flex col-span-2 gap-1 md:gap-[5px]">
@@ -36,7 +36,7 @@ export function Insolation({ slide }: { slide?: number }) {
)}
</AnimatePresence>
) : (
<div className="rounded-2xl backdrop-blur-[500px] bg-radial-[at_100%_100%] before:from-[#7a7a7a66] before:rounded-2xl before:absolute before:w-full before:h-full before:top-0 before:left-0 before:bg-[#0F1011] before:bg-radial-[at_100%_100%] before:-z-1 overflow-hidden md:bg-radial-[at_50%_0%] from-[#7A7A7A66] md:px-6 py-6 px-4 max-md:space-y-7 md:grid gap-y-[30px] grid-cols-2 grid-rows-[1fr_3fr] md:h-[348px] sticky md:top-[calc(100vh-388px)] top-100">
<div className="rounded-2xl backdrop-blur-[500px] bg-radial-[at_100%_100%] before:from-[#7a7a7a66] before:rounded-2xl before:absolute before:w-full before:h-full before:top-0 before:left-0 before:bg-[#0F1011] before:bg-radial-[at_100%_100%] before:-z-1 overflow-hidden md:bg-radial-[at_50%_0%] from-[#7A7A7A66] md:px-6 py-6 px-4 max-md:space-y-7 md:grid gap-y-[30px] grid-cols-2 grid-rows-[1fr_3fr] md:h-[348px] sticky md:top-[calc(100vh-388px)] top-120 h-[340px]">
<div className="md:grid max-md:space-y-4 grid-cols-2 col-span-2 gap-1">
<p className="heading2 font-medium">Интерактивная инсоляция</p>
<p className="text1">
@@ -69,7 +69,7 @@ export function IntegrationCRM({ slide }: { slide?: number }) {
)}
</AnimatePresence>
) : (
<div className="rounded-2xl backdrop-blur-[500px] md:bg-[#0F1011] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:-z-1 before:bg-[#0f1011] before:bg-radial-[at_100%_100%] before:from-[#7a7a7a66] md:bg-radial-[at_50%_0%] bg-radial-[at_100%_100%] overflow-hidden max-md:space-y-7 from-[#7A7A7A66] md:px-6 px-4 py-6 md:grid grid-cols-2 md:h-[348px] sticky md:top-[calc(100vh-388px)] top-100">
<div className="rounded-2xl backdrop-blur-[500px] md:bg-[#0F1011] before:absolute before:top-0 before:left-0 before:w-full before:h-full before:-z-1 before:bg-[#0f1011] before:bg-radial-[at_100%_100%] before:from-[#7a7a7a66] md:bg-radial-[at_50%_0%] bg-radial-[at_100%_100%] overflow-hidden max-md:space-y-7 from-[#7A7A7A66] md:px-6 px-4 py-6 md:grid grid-cols-2 md:h-[348px] sticky md:top-[calc(100vh-388px)] h-[340px] top-120">
<p className="heading2 font-medium">
Интеграция с CRM-системой застройщика
</p>
@@ -42,8 +42,8 @@ export function PresentationMini() {
}, [slide, videoRefs]);
return (
<div className="mt-25">
<div className="md:top-25 -top-25 md:space-y-12 sticky space-y-10">
<div className="mt-25 lg:hidden">
<div className="top-20 md:space-y-12 sticky space-y-10">
<Title>
Интерактивная презентация{' '}
<span className="text-gradient">
@@ -77,6 +77,7 @@ export function PresentationMini() {
<Insolation />
<Engine />
<IntegrationCRM />
<div className="h-[50vh] bg-[#0F1011]" />
</div>
</div>
);
@@ -4,7 +4,7 @@ import { Icon } from '@/ui/Icon';
export function SearchAndSelect({ slide }: { slide?: number }) {
return (
<div
className={`max-md:p-6 flex max-md:flex-col lg:flex-col lg:h-full md:gap-3 gap-y-7 lg:z-10 lg:max-w-[33vw] md:h-[348px] lg:transition-all max-md:rounded-2xl max-lg:sticky md:max-lg:top-[calc(100vh-388px)] max-md:top-100 lg:duration-1000 lg:col-span-2 select-none max-md:bg-radial-[at_100%_100%] from-[#7A7A7A66] max-md:backdrop-blur-[500px]${
className={`max-md:p-6 flex max-md:flex-col lg:flex-col lg:h-full md:gap-3 gap-y-7 lg:z-10 lg:max-w-[33vw] md:h-[348px] h-[340px] lg:transition-all max-md:rounded-2xl max-lg:sticky md:max-lg:top-[calc(100vh-388px)] max-md:top-120 lg:duration-1000 lg:col-span-2 select-none max-md:bg-radial-[at_100%_100%] from-[#7A7A7A66] max-md:backdrop-blur-[500px]${
slide ? ' lg:absolute lg:translate-x-[33%] lg:opacity-0' : ''
}`}
>
@@ -26,7 +26,7 @@ export function ThreeDTour({ slide }: { slide?: number }) {
)}
</AnimatePresence>
) : (
<div className="md:space-y-6 space-y-7 before:bg-[#0F1011] before:max-md:bg-radial-[at_100%_100%] before:w-full before:absolute before:rounded-2xl before:top-0 before:left-0 before:h-full before:-z-1 before:from-[#7A7A7A66] backdrop-blur-[500px] md:backdrop-blur-xs md:h-[348px] sticky max-md:px-4 max-md:py-6 max-md:rounded-2xl md:top-[calc(100vh-388px)] top-100">
<div className="md:space-y-6 space-y-7 before:bg-[#0F1011] before:max-md:bg-radial-[at_100%_100%] before:w-full before:absolute before:rounded-2xl before:top-0 before:left-0 before:h-full before:-z-1 before:from-[#7A7A7A66] h-[340px] backdrop-blur-[500px] md:backdrop-blur-xs md:h-[348px] sticky max-md:px-4 max-md:py-6 max-md:rounded-2xl md:top-[calc(100vh-388px)] top-120">
<p className="heading2 md:text-center font-medium">3D-тур</p>
<div className="max-md:w-fit md:items-stretch md:gap-2 gap-x-3 gap-y-6 grid grid-cols-3">
{threeDTour.map((item) => (
@@ -22,7 +22,7 @@ export function Reviews() {
<div className="max-lg:space-y-2">
<div
ref={ref}
className="relative m-auto max-sm:aspect-[340/480] lg:space-y-20 lg:aspect-[1400/616] lg:min-w-[1040px] max-w-full sm:max-lg:aspect-[736/480] lg:mt-[140px] mt-[100px] max-lg:flex max-lg:flex-col transition-all group"
className="relative m-auto max-sm:aspect-[340/480] lg:aspect-[1400/616] lg:min-w-[1040px] max-w-full sm:max-lg:aspect-[736/480] lg:mt-[140px] mt-[100px] max-lg:flex max-lg:flex-col transition-all group"
style={{
width: isLg
? scroll < 500
@@ -37,7 +37,6 @@ export function Streaming() {
const [point, setPoint] = useState([0, 0]);
const [muted, setMuted] = useState(true);
const [playing, setPlaying] = useState(true);
const [mouseDown, setMouseDown] = useState(false);
function handleProgressbarClick(e: MouseEvent) {
videoRef.current!.currentTime =
@@ -125,10 +124,10 @@ export function Streaming() {
className="lg:aspect-[1400/640] sm:aspect-[736/480] aspect-[340/600] rounded-2xl w-full h-full object-cover"
/>
<div className="absolute left-0 top-0 h-5/6 w-full z-[1]">
<div ref={ref} className="relative w-full h-full">
<div ref={ref} className="relative w-full h-full group">
<button
ref={btnRef}
className="bg-[#37393B99] p-[50px] backdrop-blur-[30.72px] rounded-full in-hover:opacity-100 transition-opacity in-hover:cursor-none opacity-0 sticky"
className="bg-[#37393B99] p-[50px] backdrop-blur-[30.72px] rounded-full group-hover:opacity-100 transition-opacity group-hover:cursor-none opacity-0 sticky outline-none"
style={{ left: point[0] - 64, top: point[1] - 64 }}
onClick={handleVideoClick}
>
@@ -25,7 +25,7 @@ export function AwardsCard({ className }: { className?: string }) {
<Image
src={'/img/components/header/show_case_card.png'}
fill
sizes="calc(225/361*100%)"
sizes="100%"
alt="upside"
className="object-cover !relative"
/>
@@ -1,7 +1,6 @@
'use client';
import { IProject } from '@/types/IProject';
import { Icon } from '@/ui/Icon';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { AwardsCard } from './AwardsCard';
@@ -30,7 +29,7 @@ export function ProjectsSection({ projects }: { projects: IProject[] }) {
className="rounded-xl bg-[#232425] aspect-square h-fit self-center p-5 opacity-60 hover:opacity-100 transition-opacity flex items-center justify-center btnl font-medium gap-2"
>
Смотреть все
<Icon name="arrow_more" color="white" size={20} />
{/* <Icon name="arrow_more" color="white" size={20} /> */}
</Link>
)}
</div>
@@ -3,8 +3,9 @@
import { postTags } from '@/consts/postTags';
import { projectsTags } from '@/consts/projectsTags';
import { Icon } from '@/ui/Icon';
import { LazySvg } from '@/ui/LazySvg';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useRef, useState } from 'react';
import { Suspense, useRef, useState } from 'react';
import { useOnClickOutside } from 'usehooks-ts';
import { TagFilter } from './TagsFilter';
@@ -90,7 +91,9 @@ export function TagsFilters({
className="gap-x-2 rounded-2xl flex items-center pl-6 p-4 bg-[#37393B99] backdrop-blur-sm"
>
<p className="btnm font-medium">Фильтры</p>
<Icon name={'filters'} color="white" />
<Suspense fallback={<div className="w-4 h-4" />}>
<LazySvg name="filters" color="white" width={16} height={16} />
</Suspense>
</button>
)}
</div>
+1 -4
View File
@@ -9,14 +9,11 @@ export function useDynamicSvgImport(iconName: string) {
useEffect(() => {
setLoading(true);
// dynamically import the mentioned svg icon name in props
const importSvgIcon = async (): Promise<void> => {
// please make sure all your svg icons are placed in the same directory
// if we want that part to be configurable then instead of iconName we will send iconPath as prop
try {
importedIconRef.current = (
await import(`../../public/icons/${iconName}.svg`)
).default; // svgr provides ReactComponent for given svg path
).default;
} catch (err) {
setError(err);
console.error(err);
+4 -4
View File
@@ -15,25 +15,25 @@ export function useMediaQueries(lg = 1024, md = 768, sm = 640) {
const handler = (e: MediaQueryListEvent) => setIsLg(e.matches);
lgMedia.addEventListener('change', handler);
return () => lgMedia.removeEventListener('change', handler);
}, []);
}, [lgMedia]);
useEffect(() => {
const handler = (e: MediaQueryListEvent) => setIsMd(e.matches);
mdMedia.addEventListener('change', handler);
return () => mdMedia.removeEventListener('change', handler);
}, []);
}, [mdMedia]);
useEffect(() => {
const handler = (e: MediaQueryListEvent) => setIsSm(e.matches);
smMedia.addEventListener('change', handler);
return () => smMedia.removeEventListener('change', handler);
}, []);
}, [smMedia]);
useEffect(() => {
const handler = (e: MediaQueryListEvent) => setIsXs(e.matches);
xsMedia.addEventListener('change', handler);
return () => xsMedia.removeEventListener('change', handler);
}, []);
}, [xsMedia]);
return { isLg, isMd, isSm, isXs };
}
+1 -2
View File
@@ -1,10 +1,9 @@
'use client';
import { motion, useInView, useMotionValue, useSpring } from 'framer-motion';
import { Manrope } from 'next/font/google';
import { useEffect, useRef } from 'react';
const manrope = Manrope({ subsets: ['latin'] });
// const manrope = Manrope({ subsets: ['latin'] });
export function Figure({
percent,
+15
View File
@@ -0,0 +1,15 @@
import dynamic from 'next/dynamic';
import { ComponentProps } from 'react';
interface LazySvgProps extends ComponentProps<'svg'> {
name: string;
}
export const LazySvg = async ({ name, ...props }: LazySvgProps) => {
const Svg = dynamic(() => import('../../public/icons/' + name + '.svg'));
// Or without using `dynamic`:
// We use `default` here because `@svgr/webpack` converts all other *.svg imports to React components, this might be different for other loaders.
return <Svg {...props} />;
};
+1 -1
View File
@@ -21,7 +21,7 @@ export function SeasonCard({
className="!relative lg:min-w-full md:min-w-[calc(138/224*100%)]"
alt={title}
fill
sizes="(min-width: 768px) calc(138/224*100%), (min-width: 1024px) 100%"
sizes="100%"
/>
</div>
<p className="text1 md:font-medium text-center">{title}</p>
+1 -1
View File
@@ -21,7 +21,7 @@ export function SlideBadge({
alt={title}
fill
className="rounded-xl !relative"
sizes="(min-width: 1024px) 48px, (min-width: 768px) 125px, 64px"
sizes="100%"
/>
</div>
<p className="text1">{title}</p>