This commit is contained in:
2025-02-20 12:21:40 +05:00
parent c82e5116c4
commit d3adac5f0d
43 changed files with 1000 additions and 816 deletions
-1
View File
@@ -1,4 +1,3 @@
NEXT_PUBLIC_API=http://192.168.1.250:3001/ NEXT_PUBLIC_API=http://192.168.1.250:3001/
NEXT_PUBLIC_OLD_API=https://graff.estate/api
NEXT_PUBLIC_S3_BUCKET=https://storage.yandexcloud.net/dult-faib-knac-fint/ NEXT_PUBLIC_S3_BUCKET=https://storage.yandexcloud.net/dult-faib-knac-fint/
NEXT_PUBLIC_TINYMCE_API_KEY=2vf68779upg45y46o6g5gaxldy9gzr399eyaaqa0ki3mj2h2 NEXT_PUBLIC_TINYMCE_API_KEY=2vf68779upg45y46o6g5gaxldy9gzr399eyaaqa0ki3mj2h2
Vendored
-1
View File
@@ -1,7 +1,6 @@
declare namespace NodeJS { declare namespace NodeJS {
interface ProcessEnv { interface ProcessEnv {
readonly NEXT_PUBLIC_API: string; readonly NEXT_PUBLIC_API: string;
readonly NEXT_PUBLIC_OLD_API: string;
readonly NEXT_PUBLIC_S3_BUCKET: string; readonly NEXT_PUBLIC_S3_BUCKET: string;
readonly NEXT_PUBLIC_TINYMCE_API_KEY: string; readonly NEXT_PUBLIC_TINYMCE_API_KEY: string;
} }
-17
View File
@@ -1,23 +1,6 @@
import ky from 'ky'; import ky from 'ky';
export const oldApi = ky.extend({
prefixUrl: process.env.NEXT_PUBLIC_OLD_API,
});
export const api = ky.extend({ export const api = ky.extend({
prefixUrl: process.env.NEXT_PUBLIC_API, prefixUrl: process.env.NEXT_PUBLIC_API,
credentials: 'include', credentials: 'include',
// hooks: {
// beforeRequest: [
// async (req) => {
// if (new URL(req.url).pathname === '/auth/check') {
// await ky
// .get(process.env.NEXT_PUBLIC_API + 'auth/refresh', {
// credentials: 'include',
// })
// .json();
// }
// },
// ],
// },
}); });
+6
View File
@@ -3,6 +3,7 @@ import { ArticlesList } from '@/components/pages/BlogPage/ArticlesList';
import { ArticlesPageActions } from '@/components/pages/BlogPage/ArticlesPageActions'; import { ArticlesPageActions } from '@/components/pages/BlogPage/ArticlesPageActions';
import { getQueryClient } from '@/lib/queryClient'; import { getQueryClient } from '@/lib/queryClient';
import { IArticle } from '@/types/IArticle'; import { IArticle } from '@/types/IArticle';
import { IStory } from '@/types/IStory';
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
export default async function BlogPage({ export default async function BlogPage({
@@ -33,6 +34,11 @@ export default async function BlogPage({
.json<IArticle[]>(), .json<IArticle[]>(),
}); });
await queryClient.prefetchQuery({
queryKey: ['stories'],
queryFn: async () => await api.get('stories').json<IStory[]>(),
});
return ( return (
<HydrationBoundary state={dehydrate(queryClient)}> <HydrationBoundary state={dehydrate(queryClient)}>
<ArticlesPageActions tags={tags} /> <ArticlesPageActions tags={tags} />
-1
View File
@@ -58,7 +58,6 @@ export default async function HomePage() {
<Awards /> <Awards />
<Projects /> <Projects />
<Clients /> <Clients />
{/* <ReorderGrid /> */}
</HydrationBoundary> </HydrationBoundary>
); );
} }
+1 -1
View File
@@ -111,7 +111,7 @@ html {
} }
@utility line1 { @utility line1 {
@apply 2xl:text-[128px] lg:max-2xl:text-[clamp(96px,4.444vw,128px)] md:max-lg:text-[clamp(56px,56px+(100vw-768px)/672*40,96px)] xs:max-md:text-[clamp(40px,40px+(100vw-360px)/408*16,56px)] text-[40px] leading-[85%]; @apply 2xl:text-[128px] lg:max-2xl:text-[clamp(96px,96px+(100vw-1440px)/96*32,128px)] md:max-lg:text-[clamp(56px,56px+(100vw-768px)/672*40,96px)] xs:max-md:text-[clamp(40px,40px+(100vw-360px)/408*16,56px)] text-[40px] leading-[85%];
} }
@utility line2 { @utility line2 {
+1 -1
View File
@@ -39,7 +39,7 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html> <html>
<body className="min-h-screen flex flex-col justify-between snap-ysnap-mandatory"> <body className="min-h-screen flex flex-col justify-between">
<Providers> <Providers>
{children} {children}
<ModalContainer /> <ModalContainer />
+5 -3
View File
@@ -9,7 +9,7 @@ export function DeleteItemModal({
id, id,
}: { }: {
title: string; title: string;
entity: 'projects' | 'companies' | 'articles'; entity: 'projects' | 'companies' | 'articles' | 'stories';
id: string; id: string;
}) { }) {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
@@ -21,7 +21,7 @@ export function DeleteItemModal({
<div className="flex justify-between items-center border-b border-[#ccc] pb-4 gap-4"> <div className="flex justify-between items-center border-b border-[#ccc] pb-4 gap-4">
<p className="text-2xl">{title}</p> <p className="text-2xl">{title}</p>
<button <button
onClick={() => setModal(null, '')} onClick={() => setModal(null)}
className="hover:bg-white className="hover:bg-white
hover:bg-opacity-10 p-2 transition-colors rounded-full cursor-pointer group" hover:bg-opacity-10 p-2 transition-colors rounded-full cursor-pointer group"
> >
@@ -37,7 +37,9 @@ export function DeleteItemModal({
? 'проект' ? 'проект'
: entity === 'companies' : entity === 'companies'
? 'компанию' ? 'компанию'
: 'статью'} : entity === 'articles'
? 'статью'
: 'историю'}
</Button> </Button>
</div> </div>
); );
+9 -17
View File
@@ -16,20 +16,17 @@ import TrashIcon from '../../public/icons/trash.svg';
export function ImageUploader<T extends FieldValues>({ export function ImageUploader<T extends FieldValues>({
dest, dest,
fieldName, name,
// item,
label, label,
className = '', className = '',
required = false, required = false,
}: { }: {
dest: string; dest: string;
fieldName: Path<T>; name: Path<T>;
item: Record<'img', string> & Record<'id', string>;
label: string; label: string;
required?: boolean; required?: boolean;
className?: string; className?: string;
}) { }) {
// const [currentImg, setCurrentImg] = useState(item.img);
const [file, setFile] = useState<File>(); const [file, setFile] = useState<File>();
const [previewFile, setPreviewFile] = useState(''); const [previewFile, setPreviewFile] = useState('');
@@ -55,24 +52,19 @@ export function ImageUploader<T extends FieldValues>({
const filePaths = await api const filePaths = await api
.post('upload', { body: formData }) .post('upload', { body: formData })
.json<PathValue<T, Path<T>>[]>(); .json<PathValue<T, Path<T>>[]>();
setValue(fieldName, filePaths[0]!); setValue(name, filePaths[0]!);
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
alert(`Error: ${error.message}`); alert(`Error: ${error.message}`);
} }
} }
}, [dest, fieldName, file, setValue]); }, [dest, name, file, setValue]);
useEffect(() => { useEffect(() => {
uploadFile(); uploadFile();
}, [file, uploadFile]); }, [file, uploadFile]);
const currentImg = useWatch({ control, name: fieldName }); const currentImg = useWatch({ control, name: name });
// useEffect(() => {
// const { unsubscribe } = watch((input) => setCurrentImg(input[fieldName]));
// return unsubscribe;
// }, [watch, fieldName]);
return ( return (
<Dropzone <Dropzone
@@ -93,7 +85,7 @@ export function ImageUploader<T extends FieldValues>({
type="file" type="file"
accept={'image/*'} accept={'image/*'}
required={required} required={required}
{...{ ...register(fieldName, { required }), ref: inputRef }} {...{ ...register(name, { required }), ref: inputRef }}
onChange={handleChangeFile} onChange={handleChangeFile}
{...getInputProps()} {...getInputProps()}
/> />
@@ -101,7 +93,7 @@ export function ImageUploader<T extends FieldValues>({
<> <>
<div <div
className={`relative${ className={`relative${
fieldName === 'image' ? ' max-w-[calc(292/824*100%)]' : '' name === 'image' ? ' max-w-[calc(292/824*100%)]' : ''
}`} }`}
> >
<Image <Image
@@ -111,7 +103,7 @@ export function ImageUploader<T extends FieldValues>({
} }
alt={''} alt={''}
className={`pointer-events-none aspect-square object-${ className={`pointer-events-none aspect-square object-${
fieldName === 'image' ? 'cover rounded-2xl' : 'contain' name === 'image' ? 'cover rounded-2xl' : 'contain'
} !relative`} } !relative`}
fill fill
sizes="100%" sizes="100%"
@@ -133,7 +125,7 @@ export function ImageUploader<T extends FieldValues>({
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setFile(undefined!); setFile(undefined!);
setValue(fieldName, undefined!); setValue(name, undefined!);
setPreviewFile(''); setPreviewFile('');
}} }}
className="bg-[#37393B99] px-3 py-2 rounded-xl cursor-pointer" className="bg-[#37393B99] px-3 py-2 rounded-xl cursor-pointer"
+15 -20
View File
@@ -5,6 +5,8 @@ import { useModalStore } from '@/stores/useModalStore';
import { IArticle } from '@/types/IArticle'; import { IArticle } from '@/types/IArticle';
import { ICompany } from '@/types/ICompany'; import { ICompany } from '@/types/ICompany';
import { IProject } from '@/types/IProject'; import { IProject } from '@/types/IProject';
import { IStory } from '@/types/IStory';
import { isArticle } from '@/utils/isArticle';
import { isCompany } from '@/utils/isCompany'; import { isCompany } from '@/utils/isCompany';
import { isProject } from '@/utils/isProject'; import { isProject } from '@/utils/isProject';
import { SyntheticEvent } from 'react'; import { SyntheticEvent } from 'react';
@@ -14,11 +16,12 @@ import { DeleteItemModal } from './DeleteItemModal';
import { ArticleContentFormModal } from './modals/ArticleContentFormModal'; import { ArticleContentFormModal } from './modals/ArticleContentFormModal';
import { CompanyFormModal } from './modals/CompanyFormModal'; import { CompanyFormModal } from './modals/CompanyFormModal';
import { ProjectFormModal } from './modals/ProjectFormModal'; import { ProjectFormModal } from './modals/ProjectFormModal';
import { StoryFormModal } from './modals/StoryFormModal';
export function ItemActions({ export function ItemActions({
item, item,
}: { }: {
item: IProject | ICompany | IArticle; item: IProject | ICompany | IArticle | IStory;
}) { }) {
const { data: auth } = useCheckAuthQuery(); const { data: auth } = useCheckAuthQuery();
@@ -28,21 +31,12 @@ export function ItemActions({
e.stopPropagation(); e.stopPropagation();
if (isProject(item)) { if (isProject(item)) {
const { company, ...project } = item; const { company, ...project } = item;
setModal( setModal(<ProjectFormModal action="edit" defaultValues={project} />);
<ProjectFormModal action="edit" defaultValues={project} />,
'editProjectForm'
);
} else if (isCompany(item)) { } else if (isCompany(item)) {
const { projects, ...company } = item; const { projects, ...company } = item;
setModal( setModal(<CompanyFormModal action="edit" defaultValues={company} />);
<CompanyFormModal action="edit" defaultValues={company} />, } else if (isArticle(item)) setModal(<ArticleContentFormModal {...item} />);
'editCompanyForm' else setModal(<StoryFormModal action={'edit'} defaultValues={item} />);
);
} else
setModal(
<ArticleContentFormModal {...item} />,
'articleContentFormModal'
);
} }
function handleDelete(e: SyntheticEvent) { function handleDelete(e: SyntheticEvent) {
@@ -56,19 +50,20 @@ export function ItemActions({
? 'проекта' ? 'проекта'
: isCompany(item) : isCompany(item)
? 'компании' ? 'компании'
: 'статьи') : isArticle(item)
? 'статьи'
: 'истории')
} }
entity={ entity={
isProject(item) isProject(item)
? 'projects' ? 'projects'
: isCompany(item) : isCompany(item)
? 'companies' ? 'companies'
: 'articles' : isArticle(item)
? 'articles'
: 'stories'
} }
/>, />
`delete${
isProject(item) ? 'Project' : isCompany(item) ? 'Company' : 'Article'
}`
); );
} }
-3
View File
@@ -4,7 +4,6 @@ import { api } from '@/api';
import { useMediaQueries } from '@/hooks/useMediaQueries'; import { useMediaQueries } from '@/hooks/useMediaQueries';
import { useScroll } from '@/hooks/useScroll'; import { useScroll } from '@/hooks/useScroll';
import { useCheckAuthQuery } from '@/queries/checkAuth'; import { useCheckAuthQuery } from '@/queries/checkAuth';
import { useModalStore } from '@/stores/useModalStore';
import { HeaderLink } from '@/ui/HeaderLink'; import { HeaderLink } from '@/ui/HeaderLink';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
@@ -21,8 +20,6 @@ import SkolkovoIcon from '../../../public/icons/skolkovo.svg';
import { Products } from './Products'; import { Products } from './Products';
export function Header() { export function Header() {
const { setModal } = useModalStore();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: auth } = useCheckAuthQuery(); const { data: auth } = useCheckAuthQuery();
+3 -12
View File
@@ -4,11 +4,11 @@ import { useModalStore } from '@/stores/useModalStore';
import { useEffect } from 'react'; import { useEffect } from 'react';
export function ModalContainer() { export function ModalContainer() {
const { modal, name, setModal } = useModalStore(); const { modal, setModal } = useModalStore();
useEffect(() => { useEffect(() => {
const listener = (e: KeyboardEvent) => { const listener = (e: KeyboardEvent) => {
if (e.key === 'Escape') setModal(null, ''); if (e.key === 'Escape') setModal(null);
}; };
document.addEventListener('keydown', listener); document.addEventListener('keydown', listener);
@@ -17,16 +17,7 @@ export function ModalContainer() {
return ( return (
modal && ( modal && (
<div <div className="fixed left-0 z-[14] w-full h-full flex justify-center items-start transition-opacity">
className={
'fixed left-0 z-[14] w-full h-full flex justify-center items-start transition-opacity' +
(name === 'video' || name === 'form'
? ' bg-black bg-opacity-90 [backdrop-filter:blur(10px);]'
: '') +
(name === 'menu' ? ' lg:top-16 top-12' : ' top-0') +
(name.endsWith('Form') ? ' overflow-auto' : '')
}
>
<div className="absolute backdrop-blur-lg w-full h-full z-[1]" /> <div className="absolute backdrop-blur-lg w-full h-full z-[1]" />
{modal} {modal}
</div> </div>
+133
View File
@@ -0,0 +1,133 @@
import { api } from '@/api';
import { Button } from '@/ui/Button';
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
import Dropzone from 'react-dropzone';
import { useFormContext, useWatch } from 'react-hook-form';
import AddIcon from '../../public/icons/add.svg';
import RestartIcon from '../../public/icons/restart.svg';
import TrashIcon from '../../public/icons/trash.svg';
export function VideoUploader({ name, dest }: { name: string; dest: string }) {
const { register, control, setValue } = useFormContext();
const [file, setFile] = useState<File>();
const [previewFile, setPreviewFile] = useState('');
const currentVideo = useWatch({ control, name });
function handleChangeFile(e: ChangeEvent<HTMLInputElement>) {
if (!e.target.files) return;
const targetFile = e.target.files[0];
setFile(targetFile);
setPreviewFile(URL.createObjectURL(targetFile));
}
const uploadFile = useCallback(async () => {
if (!file) return;
const formData = new FormData();
formData.append('dest', dest);
formData.append('files', file);
try {
const filePaths = await api
.post('upload', { body: formData })
.json<string[]>();
setValue(name, filePaths[0]!);
} catch (error) {
if (error instanceof Error) {
alert(`Error: ${error.message}`);
}
}
}, [file, name, setValue]);
useEffect(() => {
uploadFile();
}, [file, uploadFile]);
return (
<Dropzone
onDrop={([file]) => {
setFile(file);
setPreviewFile(URL.createObjectURL(file));
}}
accept={{
'video/*': ['*'],
}}
noClick
>
{({ getRootProps, getInputProps, inputRef }) => (
<div
{...getRootProps()}
className="relative border border-[#37393B] px-3 py-2 rounded-lg flex flex-col justify-center items-center gap-2 flex-1"
>
<input
type="file"
accept="video/mp4, video/mov"
required
{...{ ...register(name), ref: inputRef }}
onChange={handleChangeFile}
{...getInputProps()}
/>
{previewFile || currentVideo ? (
<>
<div className="relative">
<video
src={
previewFile ||
process.env.NEXT_PUBLIC_S3_BUCKET + currentVideo
}
playsInline
muted
loop
autoPlay
className="pointer-events-none !relative rounded-xl"
/>
</div>
<div className="left-6 top-6 absolute flex gap-2">
<Button
onClick={() => inputRef.current?.click()}
color="secondary"
className="px-3 py-2 bg-[#37393B99] btns"
rounded="xl"
icon={
<RestartIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" />
}
>
Заменить
</Button>
<button
onClick={(e) => {
e.preventDefault();
setFile(undefined!);
setValue(name, undefined!);
setPreviewFile('');
}}
className="bg-[#37393B99] px-3 py-2 rounded-xl cursor-pointer"
>
<TrashIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" />
</button>
</div>
</>
) : (
<div className="flex flex-col items-center gap-4">
<p className="text-[#7A7A7A] font-medium text1 text-center max-w-[calc(346/824*100%)]">
Выберите или перетащите видео
</p>
<Button
onClick={() => inputRef.current?.click()}
color="secondary"
className="bg-[#37393B99] rounded-xl px-3 py-[9px] btnm"
icon={<AddIcon className="w-4 h-4 text-white" />}
>
Выбрать
</Button>
</div>
)}
</div>
)}
</Dropzone>
);
}
@@ -27,9 +27,8 @@ export function ArticleQuoteInput({
<div className="flex gap-4"> <div className="flex gap-4">
<ImageUploader <ImageUploader
dest="articles" dest="articles"
fieldName={`blocks.${index}.avatar`} name={`blocks.${index}.avatar`}
required required
item={{ id: item.id, img: item.avatar }}
label="Автор" label="Автор"
/> />
<div className="space-y-1 w-full"> <div className="space-y-1 w-full">
@@ -26,8 +26,7 @@ export function ArticleSliderImageInput({
<Reorder.Item value={item} className="flex items-center" drag as="div"> <Reorder.Item value={item} className="flex items-center" drag as="div">
<ImageUploader <ImageUploader
dest="blog" dest="blog"
fieldName={`blocks.${index}.images.${imgIndex}.img`} name={`blocks.${index}.images.${imgIndex}.img`}
item={item}
label="Выберите изображение" label="Выберите изображение"
/> />
<button <button
@@ -1,15 +1,8 @@
import { api } from '@/api';
import { useFieldArrayFormContext } from '@/lib/FieldArrayFormProvider'; import { useFieldArrayFormContext } from '@/lib/FieldArrayFormProvider';
import { IVideo } from '@/types/IArticle'; import { IVideo } from '@/types/IArticle';
import { Button } from '@/ui/Button';
import { Reorder } from 'framer-motion'; import { Reorder } from 'framer-motion';
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
import Dropzone from 'react-dropzone';
import { useWatch } from 'react-hook-form';
import AddIcon from '../../../public/icons/add.svg';
import RestartIcon from '../../../public/icons/restart.svg';
import TrashIcon from '../../../public/icons/trash.svg'; import TrashIcon from '../../../public/icons/trash.svg';
import { IArticleInput } from '../modals/ArticleFormModal'; import { VideoUploader } from '../VideoUploader';
export function ArticleVideoUploader({ export function ArticleVideoUploader({
item, item,
@@ -18,45 +11,44 @@ export function ArticleVideoUploader({
item: IVideo & { id: string }; item: IVideo & { id: string };
index: number; index: number;
}) { }) {
const { remove, register, control, setValue } = const { remove } = useFieldArrayFormContext();
useFieldArrayFormContext<IArticleInput>();
const [file, setFile] = useState<File>(); // const [file, setFile] = useState<File>();
const [previewFile, setPreviewFile] = useState(''); // const [previewFile, setPreviewFile] = useState('');
const currentVideo = useWatch({ control, name: `blocks.${index}.src` }); // const currentVideo = useWatch({ control, name: `blocks.${index}.src` });
function handleChangeFile(e: ChangeEvent<HTMLInputElement>) { // function handleChangeFile(e: ChangeEvent<HTMLInputElement>) {
if (!e.target.files) return; // if (!e.target.files) return;
const targetFile = e.target.files[0]; // const targetFile = e.target.files[0];
setFile(targetFile); // setFile(targetFile);
setPreviewFile(URL.createObjectURL(targetFile)); // setPreviewFile(URL.createObjectURL(targetFile));
} // }
const uploadFile = useCallback(async () => { // const uploadFile = useCallback(async () => {
if (!file) return; // if (!file) return;
const formData = new FormData(); // const formData = new FormData();
formData.append('dest', 'articles'); // formData.append('dest', 'articles');
formData.append('files', file); // formData.append('files', file);
try { // try {
const filePaths = await api // const filePaths = await api
.post('upload', { body: formData }) // .post('upload', { body: formData })
.json<string[]>(); // .json<string[]>();
setValue(`blocks.${index}.src`, filePaths[0]!); // setValue(`blocks.${index}.src`, filePaths[0]!);
} catch (error) { // } catch (error) {
if (error instanceof Error) { // if (error instanceof Error) {
alert(`Error: ${error.message}`); // alert(`Error: ${error.message}`);
} // }
} // }
}, [file, index, setValue]); // }, [file, index, setValue]);
useEffect(() => { // useEffect(() => {
uploadFile(); // uploadFile();
}, [file, uploadFile]); // }, [file, uploadFile]);
return ( return (
<Reorder.Item <Reorder.Item
@@ -64,7 +56,7 @@ export function ArticleVideoUploader({
as="div" as="div"
className="w-full flex items-start gap-5" className="w-full flex items-start gap-5"
> >
<Dropzone {/* <Dropzone
onDrop={([file]) => { onDrop={([file]) => {
setFile(file); setFile(file);
setPreviewFile(URL.createObjectURL(file)); setPreviewFile(URL.createObjectURL(file));
@@ -141,7 +133,8 @@ export function ArticleVideoUploader({
)} )}
</div> </div>
)} )}
</Dropzone> </Dropzone> */}
<VideoUploader name={`blocks.${index}.src`} dest="articles" />
<button onClick={() => remove(index)} className="cursor-pointer"> <button onClick={() => remove(index)} className="cursor-pointer">
<TrashIcon className="text-white lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4" /> <TrashIcon className="text-white lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4" />
</button> </button>
@@ -57,7 +57,7 @@ export function ArticleContentFormModal({
blocks: JSON.stringify(getValues('blocks')), blocks: JSON.stringify(getValues('blocks')),
drafted, drafted,
}); });
setModal(null, ''); setModal(null);
} }
return ( return (
@@ -83,8 +83,7 @@ export function ArticleContentFormModal({
posterImage, posterImage,
id, id,
}} }}
/>, />
'editArticleModal'
) )
} }
icon={ icon={
+1 -1
View File
@@ -36,7 +36,7 @@ export function ArticleFormActions({
</div> </div>
<button <button
className="bg-[#37393B99] rounded-2xl p-4 absolute top-5 right-5 z-[2] cursor-pointer" className="bg-[#37393B99] rounded-2xl p-4 absolute top-5 right-5 z-[2] cursor-pointer"
onClick={() => setModal(null, '')} onClick={() => setModal(null)}
> >
<CloseIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" /> <CloseIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" />
</button> </button>
+6 -11
View File
@@ -39,10 +39,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
...data, ...data,
blocks: JSON.stringify(defaultValues ? defaultValues.blocks : []), blocks: JSON.stringify(defaultValues ? defaultValues.blocks : []),
}); });
setModal( setModal(<ArticleContentFormModal {...article} />);
<ArticleContentFormModal {...article} />,
'articleContentFormModal'
);
} }
const { handleSubmit, getValues, control } = form; const { handleSubmit, getValues, control } = form;
@@ -57,7 +54,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
), ),
drafted, drafted,
}); });
setModal(null, ''); setModal(null);
} }
const { mutateAsync } = useArticleMutation( const { mutateAsync } = useArticleMutation(
@@ -77,8 +74,8 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
submitHandler={handleSubmit(onSubmit)} submitHandler={handleSubmit(onSubmit)}
disabled={!title || !tags || !cardImage || !posterImage} disabled={!title || !tags || !cardImage || !posterImage}
/> />
<FormProvider {...form}>
<form className="space-y-10"> <form className="space-y-10">
<FormProvider {...form}>
<TextInput <TextInput
name="title" name="title"
label="Название статьи" label="Название статьи"
@@ -87,8 +84,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
<ImageUploader <ImageUploader
dest="blog" dest="blog"
required required
fieldName="cardImage" name="cardImage"
item={{ id: '', img: defaultValues?.cardImage ?? '' }}
label="Загрузите или перетащите изображение для превью (рекомендованнный размер 1080/1080 px)" label="Загрузите или перетащите изображение для превью (рекомендованнный размер 1080/1080 px)"
/> />
<div className="gap-y-4 flex flex-col"> <div className="gap-y-4 flex flex-col">
@@ -103,12 +99,11 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
<ImageUploader <ImageUploader
dest="blog" dest="blog"
required required
fieldName="posterImage" name="posterImage"
item={{ id: '', img: defaultValues?.posterImage ?? '' }}
label="Загрузите или перетащите изображение для превью (рекомендованнный размер 1080/1080 px)" label="Загрузите или перетащите изображение для превью (рекомендованнный размер 1080/1080 px)"
/> />
</form>
</FormProvider> </FormProvider>
</form>
</div> </div>
</> </>
); );
+4 -6
View File
@@ -43,7 +43,7 @@ export function CompanyFormModal<TAction extends 'create' | 'edit'>({
: await api.put(`companies/${defaultValues?.id}`, { json }).json(), : await api.put(`companies/${defaultValues?.id}`, { json }).json(),
onSuccess: async () => { onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['companies'] }); await queryClient.invalidateQueries({ queryKey: ['companies'] });
setModal(null, ''); setModal(null);
}, },
}); });
@@ -51,7 +51,7 @@ export function CompanyFormModal<TAction extends 'create' | 'edit'>({
<> <>
<button <button
className="bg-[#37393B99] rounded-2xl p-4 absolute top-5 right-5 z-[2] cursor-pointer" className="bg-[#37393B99] rounded-2xl p-4 absolute top-5 right-5 z-[2] cursor-pointer"
onClick={() => setModal(null, '')} onClick={() => setModal(null)}
> >
<CloseIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" /> <CloseIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" />
</button> </button>
@@ -69,15 +69,13 @@ export function CompanyFormModal<TAction extends 'create' | 'edit'>({
<ImageUploader <ImageUploader
className="aspect-square w-full" className="aspect-square w-full"
dest="projects" dest="projects"
fieldName="logo" name="logo"
item={{ id: '', img: defaultValues?.logo ?? '' }}
label="Загрузите или перетащите логотип ( в формате svg )" label="Загрузите или перетащите логотип ( в формате svg )"
/> />
<ImageUploader <ImageUploader
className="aspect-square w-full" className="aspect-square w-full"
dest="projects" dest="projects"
fieldName="mapIcon" name="mapIcon"
item={{ id: '', img: defaultValues?.mapIcon ?? '' }}
label="Загрузите или перетащите иконку ( в формате svg )" label="Загрузите или перетащите иконку ( в формате svg )"
/> />
</div> </div>
@@ -1,30 +0,0 @@
import { useModalStore } from '@/stores/useModalStore';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
export function DeleteArticleModal({ id }: { id: string }) {
const { setModal } = useModalStore();
async function handleDeleteArticle() {}
return (
<div className="bg-white shadow-lg text-black p-8 rounded-xl flex flex-col gap-4">
<div className="flex justify-between items-center border-b border-[#ccc] pb-4 gap-4">
<p className="text-xl">Удаление статьи</p>
<button
onClick={() => setModal(null, '')}
className="p-2 hover:bg-white hover:bg-opacity-10 transition-colors rounded-full"
>
<Icon name='close' color='white' />
</button>
</div>
<Button
onClick={handleDeleteArticle}
className="text-white self-end outline-none"
>
Удалить статью
</Button>
</div>
);
}
+2 -4
View File
@@ -96,8 +96,7 @@ export function ProjectFormModal<TAction extends 'create' | 'edit'>({
</div> </div>
<ImageUploader <ImageUploader
dest="projects" dest="projects"
fieldName="image" name="image"
item={{ img: defaultValues?.image ?? '', id: '' }}
label="Загрузите или перетащите изображение для превью (рекомендованнный размер 1080/1080 px)" label="Загрузите или перетащите изображение для превью (рекомендованнный размер 1080/1080 px)"
/> />
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -126,7 +125,6 @@ export function ProjectFormModal<TAction extends 'create' | 'edit'>({
</select> </select>
<OpenFormModalWrapper <OpenFormModalWrapper
modal={<CompanyFormModal action={'create'} />} modal={<CompanyFormModal action={'create'} />}
modalName="companyForm"
> >
<Button <Button
rounded="2xl" rounded="2xl"
@@ -184,7 +182,7 @@ export function ProjectFormModal<TAction extends 'create' | 'edit'>({
</div> </div>
<button <button
className="bg-[#37393B99] rounded-2xl p-4 absolute top-5 right-5 z-[2] cursor-pointer" className="bg-[#37393B99] rounded-2xl p-4 absolute top-5 right-5 z-[2] cursor-pointer"
onClick={() => setModal(null, '')} onClick={() => setModal(null)}
> >
<CloseIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" /> <CloseIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" />
</button> </button>
+24 -13
View File
@@ -1,9 +1,10 @@
import { stories } from '@/consts/stories';
import { useMediaQueries } from '@/hooks/useMediaQueries'; import { useMediaQueries } from '@/hooks/useMediaQueries';
import { useGetStories } from '@/queries/getStories';
import { useModalStore } from '@/stores/useModalStore'; import { useModalStore } from '@/stores/useModalStore';
import { createRef, RefObject, useEffect, useRef, useState } from 'react'; import { createRef, RefObject, useEffect, useRef, useState } from 'react';
import { useSwipeable } from 'react-swipeable'; import { useSwipeable } from 'react-swipeable';
import CloseIcon from '../../../public/icons/close.svg'; import CloseIcon from '../../../public/icons/close.svg';
import { ItemActions } from '../ItemActions';
export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) { export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
@@ -16,9 +17,12 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const { data: stories } = useGetStories();
useEffect(() => { useEffect(() => {
if (!stories) return;
setVideoRefs(stories.map(createRef<HTMLVideoElement>)); setVideoRefs(stories.map(createRef<HTMLVideoElement>));
}, []); }, [stories]);
useEffect(() => { useEffect(() => {
if ( if (
@@ -40,7 +44,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
useEffect(() => { useEffect(() => {
if (currentProgress === 1 && videoRefs.length) { if (currentProgress === 1 && videoRefs.length) {
if (currentIndex === videoRefs.length - 1) setModal(null, ''); if (currentIndex === videoRefs.length - 1) setModal(null);
else setCurrentIndex((prev) => prev + 1); else setCurrentIndex((prev) => prev + 1);
} }
}, [currentProgress, setModal, videoRefs]); }, [currentProgress, setModal, videoRefs]);
@@ -68,7 +72,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
preventScrollOnSwipe: true, preventScrollOnSwipe: true,
touchEventOptions: { passive: false }, touchEventOptions: { passive: false },
onSwipedLeft: () => onSwipedLeft: () =>
setCurrentIndex((prev) => Math.min(stories.length - 1, prev + 1)), setCurrentIndex((prev) => Math.min((stories?.length ?? 0) - 1, prev + 1)),
onSwipedRight: () => setCurrentIndex((prev) => Math.max(0, prev - 1)), onSwipedRight: () => setCurrentIndex((prev) => Math.max(0, prev - 1)),
onTouchStartOrOnMouseDown: (e) => { onTouchStartOrOnMouseDown: (e) => {
const slider = ref.current; const slider = ref.current;
@@ -80,26 +84,30 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
) )
return; return;
if ((e.event as TouchEvent).touches[0].clientX > slider!.clientWidth / 2) if ((e.event as TouchEvent).touches[0].clientX > slider!.clientWidth / 2)
setCurrentIndex((prev) => Math.min(stories.length - 1, prev + 1)); setCurrentIndex((prev) =>
Math.min((stories?.length ?? 0) - 1, prev + 1)
);
else setCurrentIndex((prev) => Math.max(0, prev - 1)); else setCurrentIndex((prev) => Math.max(0, prev - 1));
}, },
}); });
if (!stories) return null;
return ( return (
<> <>
<button <button
className="rounded-2xl p-4 bg-[#37393B99] absolute top-5 right-5 z-10 cursor-pointer" className="rounded-2xl p-4 bg-[#37393B99] absolute top-5 right-5 z-10 cursor-pointer"
onClick={() => setModal(null, '')} onClick={() => setModal(null)}
> >
<CloseIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" /> <CloseIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" />
</button> </button>
<div <div
{...handlers} {...handlers}
className="overflow-hidden z-1 md:m-auto lg:max-w-[84.028vw] lg:max-h-[76.433vh] md:max-lg:max-w-[157.422vw] md:max-lg:max-h-[67.669vh] max-md:max-w-screen max-h-dvh max-md:top-0" className="overflow-hidden z-1 md:m-auto lg:w-[84.028vw] lg:h-[76.433vh] md:max-lg:w-[157.422vw] md:max-lg:h-[67.669vh] max-md:w-screen h-dvh max-md:top-0"
> >
<div ref={ref} className="flex md:gap-6 items-center h-full relative"> <div ref={ref} className="flex md:gap-6 items-center h-full relative">
{!!videoRefs.length && {!!videoRefs.length &&
stories.map(({ id, video, title, poster }, index) => ( stories?.map(({ id, video, preview, text, createdAt }, index) => (
<div <div
style={{ style={{
transform: isLg transform: isLg
@@ -110,7 +118,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
left: isLg || isMd ? 24 * (1 - currentIndex) : 0, left: isLg || isMd ? 24 * (1 - currentIndex) : 0,
}} }}
key={id} key={id}
className={`select-none relative flex items-end overflow-hidden md:rounded-xl cursor-pointer transition-transform p-4 ${ className={`select-none relative flex items-end group overflow-hidden md:rounded-xl cursor-pointer transition-transform p-4 ${
index === currentIndex index === currentIndex
? 'lg:min-w-[28.125vw] lg:h-[76.433vh] md:max-lg:min-w-[52.734vw] md:max-lg:h-[67.669vh] max-md:min-w-screen max-md:h-dvh' ? 'lg:min-w-[28.125vw] lg:h-[76.433vh] md:max-lg:min-w-[52.734vw] md:max-lg:h-[67.669vh] max-md:min-w-screen max-md:h-dvh'
: 'lg:min-w-[26.25vw] lg:h-[71.338vh] md:max-lg:min-w-[49.219vw] md:max-lg:h-[63.158vh] max-md:min-w-screen max-md:h-dvh' : 'lg:min-w-[26.25vw] lg:h-[71.338vh] md:max-lg:min-w-[49.219vw] md:max-lg:h-[63.158vh] max-md:min-w-screen max-md:h-dvh'
@@ -119,8 +127,8 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
> >
<video <video
ref={videoRefs[index]} ref={videoRefs[index]}
src={video} src={process.env.NEXT_PUBLIC_S3_BUCKET + video}
poster={poster} poster={process.env.NEXT_PUBLIC_S3_BUCKET + preview}
className="absolute inset-0 object-cover object-center md:rounded-xl" className="absolute inset-0 object-cover object-center md:rounded-xl"
playsInline playsInline
/> />
@@ -133,7 +141,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
/> />
{currentIndex === index && ( {currentIndex === index && (
<div className="space-y-5 z-1 max-md:hidden"> <div className="space-y-5 z-1 max-md:hidden">
<p className="heading1 font-medium">{title}</p> <p className="heading1 font-medium">{text}</p>
<div className="bg-white/30 w-full h-1 rounded-[34px]"> <div className="bg-white/30 w-full h-1 rounded-[34px]">
<div <div
className="h-1 bg-white transition-[width] rounded-[34px]" className="h-1 bg-white transition-[width] rounded-[34px]"
@@ -142,11 +150,13 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
</div> </div>
</div> </div>
)} )}
<ItemActions item={{ id, text, video, preview, createdAt }} />
</div> </div>
))} ))}
</div> </div>
{stories && (
<div className="md:hidden absolute space-y-6 left-2.5 right-2.5 bottom-4 z-1 w-full"> <div className="md:hidden absolute space-y-6 left-2.5 right-2.5 bottom-4 z-1 w-full">
<p className="heading1 font-medium">{stories[currentIndex].title}</p> <p className="heading1 font-medium">{stories[currentIndex].text}</p>
<div className="flex gap-1"> <div className="flex gap-1">
{stories.map(({ id }, index) => ( {stories.map(({ id }, index) => (
<div key={id} className="bg-white/40 flex-1 rounded-[34px] h-1"> <div key={id} className="bg-white/40 flex-1 rounded-[34px] h-1">
@@ -162,6 +172,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
))} ))}
</div> </div>
</div> </div>
)}
</div> </div>
</> </>
); );
+79
View File
@@ -0,0 +1,79 @@
import { useStoryMutation } from '@/hooks/useStoryMutation';
import { useModalStore } from '@/stores/useModalStore';
import { TextInput } from '@/ui/TextInput';
import {
FormProvider,
SubmitHandler,
useForm,
useWatch,
} from 'react-hook-form';
import CloseIcon from '../../../public/icons/close.svg';
import { ImageUploader } from '../ImageUploader';
import { VideoUploader } from '../VideoUploader';
import { FormModalHeader } from './FormModalHeader';
export interface IStoryFormInput {
text: string;
video: string;
preview: string;
createdAt: string;
}
interface IStoryFormModalProps<TAction extends 'create' | 'edit'> {
action: TAction;
defaultValues?: TAction extends 'edit'
? IStoryFormInput & { id: string }
: undefined;
}
export function StoryFormModal<TAction extends 'create' | 'edit'>({
action,
defaultValues,
}: IStoryFormModalProps<TAction>) {
const { mutateAsync } = useStoryMutation(
action === 'create'
? { action, id: undefined }
: { action, id: defaultValues!.id }
);
const form = useForm<IStoryFormInput>({
defaultValues: defaultValues ?? { createdAt: new Date().toISOString() },
});
const { handleSubmit, control } = form;
const { preview, video } = useWatch({ control });
const { setModal } = useModalStore();
return (
<>
<div className="relative py-10 space-y-10 bg-[#232425] rounded-[28px] top-5 w-[66.25vw] pl-[75px] pr-[55px] overflow-y-auto z-[2] max-h-[calc(100vh-40px)]">
<FormModalHeader
disabled={!preview || !video}
submitHandler={handleSubmit(
mutateAsync as SubmitHandler<IStoryFormInput>
)}
/>
<FormProvider {...form}>
<form className="space-y-10">
<VideoUploader name="video" dest="stories" />
<ImageUploader
dest="stories"
name="preview"
label="Загрузите или перетащите изображение для превью"
required
/>
<TextInput name="text" label="Описание сториса" placeholder="" />
</form>
</FormProvider>
</div>
<button
className="bg-[#37393B99] rounded-2xl p-4 absolute top-5 right-5 z-[2] cursor-pointer"
onClick={() => setModal(null)}
>
<CloseIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" />
</button>
</>
);
}
@@ -31,12 +31,7 @@ export function ArticleSyncPage({ articleId }: { articleId: string }) {
className="bg-[#37393B99] z-3 backdrop-blur-sm p-4 btnm" className="bg-[#37393B99] z-3 backdrop-blur-sm p-4 btnm"
color="secondary" color="secondary"
rounded="2xl" rounded="2xl"
onClick={() => onClick={() => setModal(<ArticleContentFormModal {...article} />)}
setModal(
<ArticleContentFormModal {...article} />,
'articleContentFormModal'
)
}
icon={ icon={
<EditIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" /> <EditIcon className="lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4 text-white" />
} }
@@ -16,10 +16,7 @@ export function ArticlesPageActions({ tags }: { tags: string[] }) {
return ( return (
<div className="lg:sticky lg:top-12 absolute flex flex-col justify-between gap-y-4"> <div className="lg:sticky lg:top-12 absolute flex flex-col justify-between gap-y-4">
<div className="space-y-2 max-lg:hidden"> <div className="space-y-2 max-lg:hidden">
<OpenFormModalWrapper <OpenFormModalWrapper modal={<ArticleFormModal action="create" />}>
modalName="addArticle"
modal={<ArticleFormModal action="create" />}
>
<Button <Button
className="px-3 py-2 outline-none btns flex items-center text-nowrap" className="px-3 py-2 outline-none btns flex items-center text-nowrap"
rounded="xl" rounded="xl"
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { oldApi } from '@/api'; import { api } from '@/api';
import regionsData from '@/consts/regionsData.json'; import regionsData from '@/consts/regionsData.json';
import { GradientButton } from '@/ui/GradientButton'; import { GradientButton } from '@/ui/GradientButton';
import { Title } from '@/ui/Title'; import { Title } from '@/ui/Title';
@@ -33,18 +33,17 @@ export function Calculator() {
const [calculated, setCalculated] = useState(true); const [calculated, setCalculated] = useState(true);
async function getRegionName() { async function getRegionName() {
const result: any = await oldApi.get('getRegionName').json(); try {
const result: any = await api
if (result.error) { .get('getRegionName')
setSelectedRegion(regionsData.find((region) => region.id === 11)); .json<{ regionName: string }>();
return;
}
const foundRegion = const foundRegion =
regionsData.find((region) => region.name === result.regionName) || regionsData.find((region) => region.name === result.regionName) ??
regionsData.find((region) => region.id === 11); regionsData.find((region) => region.id === 11);
setSelectedRegion(foundRegion); setSelectedRegion(foundRegion);
} catch (error) {
setSelectedRegion(regionsData.find((region) => region.id === 11));
}
} }
useEffect(() => { useEffect(() => {
@@ -66,7 +66,6 @@ export function Clients() {
</Title> </Title>
<OpenFormModalWrapper <OpenFormModalWrapper
modal={<CompanyFormModal action="create" />} modal={<CompanyFormModal action="create" />}
modalName="addCompany"
className="aspect-square flex flex-col items-center justify-center gap-3" className="aspect-square flex flex-col items-center justify-center gap-3"
> >
<GradientButton> <GradientButton>
+1 -1
View File
@@ -62,7 +62,7 @@ export function Motivation() {
/> />
</div> </div>
<button <button
onClick={() => setModal(<StoriesModal />, 'stories')} onClick={() => setModal(<StoriesModal />)}
className="flex flex-col justify-between lg:w-[28.611vw] md:max-lg:w-[47.135vw] lg:h-[15.694vw] md:max-lg:h-[29.427vw] w-[46.111vw] h-[61.111vw] lg:p-[1.667vw] p-4 lg:col-start-2 lg:row-start-1 row-start-2 bg-[radial-gradient(ellipse_at_bottom_right,#7A7A7A50,transparent)] rounded-2xl relative cursor-pointer outline-none" className="flex flex-col justify-between lg:w-[28.611vw] md:max-lg:w-[47.135vw] lg:h-[15.694vw] md:max-lg:h-[29.427vw] w-[46.111vw] h-[61.111vw] lg:p-[1.667vw] p-4 lg:col-start-2 lg:row-start-1 row-start-2 bg-[radial-gradient(ellipse_at_bottom_right,#7A7A7A50,transparent)] rounded-2xl relative cursor-pointer outline-none"
> >
<p className="font-medium heading2 lg:max-w-[60%] md:max-lg:max-w-[19.271vw] self-start text-left"> <p className="font-medium heading2 lg:max-w-[60%] md:max-lg:max-w-[19.271vw] self-start text-left">
@@ -23,7 +23,6 @@ export function ProjectsPageHeader() {
</Title> </Title>
{auth && ( {auth && (
<OpenFormModalWrapper <OpenFormModalWrapper
modalName="addProjectForm"
modal={<ProjectFormModal action={'create'} />} modal={<ProjectFormModal action={'create'} />}
className="btns sticky top-0" className="btns sticky top-0"
> >
+45 -46
View File
@@ -8,63 +8,63 @@
}, },
{ {
"id": 2, "id": 2,
"name": "Амурская область", "name": "Амурская Область",
"areaInComplex": 5837, "areaInComplex": 5837,
"areaApartment": 48, "areaApartment": 48,
"costPerSquare": 131 "costPerSquare": 131
}, },
{ {
"id": 3, "id": 3,
"name": "Архангельская область", "name": "Архангельская Область",
"areaInComplex": 6566, "areaInComplex": 6566,
"areaApartment": 44, "areaApartment": 44,
"costPerSquare": 101 "costPerSquare": 101
}, },
{ {
"id": 4, "id": 4,
"name": "Астраханская область", "name": "Астраханская Область",
"areaInComplex": 11851, "areaInComplex": 11851,
"areaApartment": 50, "areaApartment": 50,
"costPerSquare": 96 "costPerSquare": 96
}, },
{ {
"id": 5, "id": 5,
"name": "Белгородская область", "name": "Белгородская Область",
"areaInComplex": 4570, "areaInComplex": 4570,
"areaApartment": 51, "areaApartment": 51,
"costPerSquare": 86 "costPerSquare": 86
}, },
{ {
"id": 6, "id": 6,
"name": "Брянская область", "name": "Брянская Область",
"areaInComplex": 7333, "areaInComplex": 7333,
"areaApartment": 59, "areaApartment": 59,
"costPerSquare": 60 "costPerSquare": 60
}, },
{ {
"id": 7, "id": 7,
"name": "Владимирская область", "name": "Владимирская Область",
"areaInComplex": 7509, "areaInComplex": 7509,
"areaApartment": 56, "areaApartment": 56,
"costPerSquare": 54 "costPerSquare": 54
}, },
{ {
"id": 8, "id": 8,
"name": "Волгоградская область", "name": "Волгоградская Область",
"areaInComplex": 7378, "areaInComplex": 7378,
"areaApartment": 52, "areaApartment": 52,
"costPerSquare": 76 "costPerSquare": 76
}, },
{ {
"id": 9, "id": 9,
"name": "Вологодская область", "name": "Вологодская Область",
"areaInComplex": 5235, "areaInComplex": 5235,
"areaApartment": 51, "areaApartment": 51,
"costPerSquare": 54 "costPerSquare": 54
}, },
{ {
"id": 10, "id": 10,
"name": "Воронежская область", "name": "Воронежская Область",
"areaInComplex": 13002, "areaInComplex": 13002,
"areaApartment": 48, "areaApartment": 48,
"costPerSquare": 77 "costPerSquare": 77
@@ -99,14 +99,14 @@
}, },
{ {
"id": 15, "id": 15,
"name": "Ивановская область", "name": "Ивановская Область",
"areaInComplex": 6594, "areaInComplex": 6594,
"areaApartment": 58, "areaApartment": 58,
"costPerSquare": 60 "costPerSquare": 60
}, },
{ {
"id": 16, "id": 16,
"name": "Иркутская область", "name": "Иркутская Область",
"areaInComplex": 5067, "areaInComplex": 5067,
"areaApartment": 51, "areaApartment": 51,
"costPerSquare": 103 "costPerSquare": 103
@@ -120,14 +120,14 @@
}, },
{ {
"id": 18, "id": 18,
"name": "Калининградская область", "name": "Калининградская Область",
"areaInComplex": 7209, "areaInComplex": 7209,
"areaApartment": 56, "areaApartment": 56,
"costPerSquare": 87 "costPerSquare": 87
}, },
{ {
"id": 19, "id": 19,
"name": "Калужская область", "name": "Калужская Область",
"areaInComplex": 7833, "areaInComplex": 7833,
"areaApartment": 55, "areaApartment": 55,
"costPerSquare": 88 "costPerSquare": 88
@@ -141,21 +141,21 @@
}, },
{ {
"id": 21, "id": 21,
"name": "Кемеровская область - Кузбасс", "name": "Кемеровская Область - Кузбасс",
"areaInComplex": 7256, "areaInComplex": 7256,
"areaApartment": 52, "areaApartment": 52,
"costPerSquare": 71 "costPerSquare": 71
}, },
{ {
"id": 22, "id": 22,
"name": "Кировская область", "name": "Кировская Область",
"areaInComplex": 6249, "areaInComplex": 6249,
"areaApartment": 51, "areaApartment": 51,
"costPerSquare": 80 "costPerSquare": 80
}, },
{ {
"id": 23, "id": 23,
"name": "Костромская область", "name": "Костромская Область",
"areaInComplex": 2821, "areaInComplex": 2821,
"areaApartment": 55, "areaApartment": 55,
"costPerSquare": 67 "costPerSquare": 67
@@ -176,91 +176,91 @@
}, },
{ {
"id": 26, "id": 26,
"name": "Курганская область", "name": "Курганская Область",
"areaInComplex": 5434, "areaInComplex": 5434,
"areaApartment": 53, "areaApartment": 53,
"costPerSquare": 63 "costPerSquare": 63
}, },
{ {
"id": 27, "id": 27,
"name": "Курская область", "name": "Курская Область",
"areaInComplex": 7045, "areaInComplex": 7045,
"areaApartment": 54, "areaApartment": 54,
"costPerSquare": 90 "costPerSquare": 90
}, },
{ {
"id": 28, "id": 28,
"name": "Ленинградская область", "name": "Ленинградская Область",
"areaInComplex": 12385, "areaInComplex": 12385,
"areaApartment": 40, "areaApartment": 40,
"costPerSquare": 134 "costPerSquare": 134
}, },
{ {
"id": 29, "id": 29,
"name": "Липецкая область", "name": "Липецкая Область",
"areaInComplex": 6231, "areaInComplex": 6231,
"areaApartment": 58, "areaApartment": 58,
"costPerSquare": 69 "costPerSquare": 69
}, },
{ {
"id": 30, "id": 30,
"name": "Магаданская область", "name": "Магаданская Область",
"areaInComplex": 3187, "areaInComplex": 3187,
"areaApartment": 55, "areaApartment": 55,
"costPerSquare": 85 "costPerSquare": 85
}, },
{ {
"id": 31, "id": 31,
"name": "Московская область", "name": "Московская Область",
"areaInComplex": 13213, "areaInComplex": 13213,
"areaApartment": 46, "areaApartment": 46,
"costPerSquare": 149 "costPerSquare": 149
}, },
{ {
"id": 32, "id": 32,
"name": "Нижегородская область", "name": "Нижегородская Область",
"areaInComplex": 8760, "areaInComplex": 8760,
"areaApartment": 53, "areaApartment": 53,
"costPerSquare": 106 "costPerSquare": 106
}, },
{ {
"id": 33, "id": 33,
"name": "Новгородская область", "name": "Новгородская Область",
"areaInComplex": 5953, "areaInComplex": 5953,
"areaApartment": 53, "areaApartment": 53,
"costPerSquare": 71 "costPerSquare": 71
}, },
{ {
"id": 34, "id": 34,
"name": "Новосибирская область", "name": "Новосибирская Область",
"areaInComplex": 6748, "areaInComplex": 6748,
"areaApartment": 51, "areaApartment": 51,
"costPerSquare": 107 "costPerSquare": 107
}, },
{ {
"id": 35, "id": 35,
"name": "Омская область", "name": "Омская Область",
"areaInComplex": 8625, "areaInComplex": 8625,
"areaApartment": 55, "areaApartment": 55,
"costPerSquare": 85 "costPerSquare": 85
}, },
{ {
"id": 36, "id": 36,
"name": "Оренбургская область", "name": "Оренбургская Область",
"areaInComplex": 7686, "areaInComplex": 7686,
"areaApartment": 49, "areaApartment": 49,
"costPerSquare": 67 "costPerSquare": 67
}, },
{ {
"id": 37, "id": 37,
"name": "Орловская область", "name": "Орловская Область",
"areaInComplex": 11543, "areaInComplex": 11543,
"areaApartment": 58, "areaApartment": 58,
"costPerSquare": 34 "costPerSquare": 34
}, },
{ {
"id": 38, "id": 38,
"name": "Пензенская область", "name": "Пензенская Область",
"areaInComplex": 12832, "areaInComplex": 12832,
"areaApartment": 54, "areaApartment": 54,
"costPerSquare": 74 "costPerSquare": 74
@@ -281,7 +281,7 @@
}, },
{ {
"id": 41, "id": 41,
"name": "Псковская область", "name": "Псковская Область",
"areaInComplex": 5750, "areaInComplex": 5750,
"areaApartment": 52, "areaApartment": 52,
"costPerSquare": 59 "costPerSquare": 59
@@ -400,49 +400,49 @@
}, },
{ {
"id": 58, "id": 58,
"name": "Ростовская область", "name": "Ростовская Область",
"areaInComplex": 10987, "areaInComplex": 10987,
"areaApartment": 48, "areaApartment": 48,
"costPerSquare": 94 "costPerSquare": 94
}, },
{ {
"id": 59, "id": 59,
"name": "Рязанская область", "name": "Рязанская Область",
"areaInComplex": 14581, "areaInComplex": 14581,
"areaApartment": 49, "areaApartment": 49,
"costPerSquare": 76 "costPerSquare": 76
}, },
{ {
"id": 60, "id": 60,
"name": "Самарская область", "name": "Самарская Область",
"areaInComplex": 10571, "areaInComplex": 10571,
"areaApartment": 55, "areaApartment": 55,
"costPerSquare": 82 "costPerSquare": 82
}, },
{ {
"id": 61, "id": 61,
"name": "Саратовская область", "name": "Саратовская Область",
"areaInComplex": 7789, "areaInComplex": 7789,
"areaApartment": 53, "areaApartment": 53,
"costPerSquare": 59 "costPerSquare": 59
}, },
{ {
"id": 62, "id": 62,
"name": "Сахалинская область", "name": "Сахалинская Область",
"areaInComplex": 5668, "areaInComplex": 5668,
"areaApartment": 51, "areaApartment": 51,
"costPerSquare": 153 "costPerSquare": 153
}, },
{ {
"id": 63, "id": 63,
"name": "Свердловская область", "name": "Свердловская Область",
"areaInComplex": 12467, "areaInComplex": 12467,
"areaApartment": 48, "areaApartment": 48,
"costPerSquare": 111 "costPerSquare": 111
}, },
{ {
"id": 64, "id": 64,
"name": "Смоленская область", "name": "Смоленская Область",
"areaInComplex": 5512, "areaInComplex": 5512,
"areaApartment": 55, "areaApartment": 55,
"costPerSquare": 54 "costPerSquare": 54
@@ -456,35 +456,35 @@
}, },
{ {
"id": 66, "id": 66,
"name": "Тамбовская область", "name": "Тамбовская Область",
"areaInComplex": 6631, "areaInComplex": 6631,
"areaApartment": 54, "areaApartment": 54,
"costPerSquare": 61 "costPerSquare": 61
}, },
{ {
"id": 67, "id": 67,
"name": "Тверская область", "name": "Тверская Область",
"areaInComplex": 5696, "areaInComplex": 5696,
"areaApartment": 52, "areaApartment": 52,
"costPerSquare": 79 "costPerSquare": 79
}, },
{ {
"id": 68, "id": 68,
"name": "Томская область", "name": "Томская Область",
"areaInComplex": 4397, "areaInComplex": 4397,
"areaApartment": 51, "areaApartment": 51,
"costPerSquare": 105 "costPerSquare": 105
}, },
{ {
"id": 69, "id": 69,
"name": "Тульская область", "name": "Тульская Область",
"areaInComplex": 7696, "areaInComplex": 7696,
"areaApartment": 48, "areaApartment": 48,
"costPerSquare": 90 "costPerSquare": 90
}, },
{ {
"id": 70, "id": 70,
"name": "Тюменская область", "name": "Тюменская Область",
"areaInComplex": 11328, "areaInComplex": 11328,
"areaApartment": 52, "areaApartment": 52,
"costPerSquare": 100 "costPerSquare": 100
@@ -498,7 +498,7 @@
}, },
{ {
"id": 72, "id": 72,
"name": "Ульяновская область", "name": "Ульяновская Область",
"areaInComplex": 7177, "areaInComplex": 7177,
"areaApartment": 48, "areaApartment": 48,
"costPerSquare": 75 "costPerSquare": 75
@@ -519,7 +519,7 @@
}, },
{ {
"id": 75, "id": 75,
"name": "Челябинская область", "name": "Челябинская Область",
"areaInComplex": 8631, "areaInComplex": 8631,
"areaApartment": 51, "areaApartment": 51,
"costPerSquare": 78 "costPerSquare": 78
@@ -547,10 +547,9 @@
}, },
{ {
"id": 79, "id": 79,
"name": "Ярославская область", "name": "Ярославская Область",
"areaInComplex": 6502, "areaInComplex": 6502,
"areaApartment": 54, "areaApartment": 54,
"costPerSquare": 77 "costPerSquare": 77
} }
] ]
+1 -3
View File
@@ -4,12 +4,10 @@ import { PropsWithChildren, ReactNode } from 'react';
export function OpenFormModalWrapper({ export function OpenFormModalWrapper({
modal, modal,
modalName,
children, children,
className, className,
}: PropsWithChildren<{ }: PropsWithChildren<{
modal: ReactNode; modal: ReactNode;
modalName: string;
className?: string; className?: string;
}>) { }>) {
const { data: auth } = useCheckAuthQuery(); const { data: auth } = useCheckAuthQuery();
@@ -21,7 +19,7 @@ export function OpenFormModalWrapper({
<div <div
ref={(el) => { ref={(el) => {
if (!el) return; if (!el) return;
const handler = () => setModal(modal, modalName); const handler = () => setModal(modal);
el!.children.item(0)!.addEventListener('click', handler); el!.children.item(0)!.addEventListener('click', handler);
return () => return () =>
el!.children.item(0)!.removeEventListener('click', handler); el!.children.item(0)!.removeEventListener('click', handler);
+2 -2
View File
@@ -3,7 +3,7 @@ import { useModalStore } from '@/stores/useModalStore';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useDeleteMutation( export function useDeleteMutation(
entity: 'projects' | 'companies' | 'articles', entity: 'projects' | 'companies' | 'articles' | 'stories',
id: string id: string
) { ) {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
@@ -18,7 +18,7 @@ export function useDeleteMutation(
if (entity === 'articles') if (entity === 'articles')
queryClient.setQueryData(['articles', 'drafted', []], []); queryClient.setQueryData(['articles', 'drafted', []], []);
setModal(null, ''); setModal(null);
}, },
}); });
+1 -1
View File
@@ -19,7 +19,7 @@ export function useProjectMutation({
: await api.put(`projects/${id}`, { json }).json<IProject>(), : await api.put(`projects/${id}`, { json }).json<IProject>(),
onSuccess: async () => { onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['projects'] }); await queryClient.invalidateQueries({ queryKey: ['projects'] });
setModal(null, ''); setModal(null);
}, },
}); });
} }
+26
View File
@@ -0,0 +1,26 @@
import { api } from '@/api';
import { IStoryFormInput } from '@/components/modals/StoryFormModal';
import { useModalStore } from '@/stores/useModalStore';
import { IStory } from '@/types/IStory';
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useStoryMutation({
action,
id,
}: { action: 'create'; id: undefined } | { action: 'edit'; id: string }) {
const queryClient = useQueryClient();
const { setModal } = useModalStore();
return useMutation({
mutationFn: async (json: IStoryFormInput) => {
action === 'create'
? await api.post('stories', { json }).json<IStory>()
: await api.put(`stories/${id}`, { json }).json<IStory>();
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ['stories'] });
setModal(null);
},
});
}
+10
View File
@@ -0,0 +1,10 @@
import { api } from '@/api';
import { IStory } from '@/types/IStory';
import { useQuery } from '@tanstack/react-query';
export function useGetStories() {
return useQuery({
queryKey: ['stories'],
queryFn: async () => await api.get('stories').json<IStory[]>(),
});
}
+3 -5
View File
@@ -3,12 +3,10 @@ import { create } from 'zustand';
interface IModalState { interface IModalState {
modal: ReactNode | null; modal: ReactNode | null;
setModal: (modal: ReactNode, modalName: string) => void; setModal: (modal: ReactNode) => void;
name: string;
} }
export const useModalStore = create<IModalState>(set => ({ export const useModalStore = create<IModalState>((set) => ({
modal: null, modal: null,
name: '', setModal: (modal: ReactNode) => set({ modal }),
setModal: (modal: ReactNode, name: string) => set({ modal, name }),
})); }));
+7
View File
@@ -0,0 +1,7 @@
export interface IStory {
id: string;
text: string;
video: string;
preview: string;
createdAt: string;
}
+1 -1
View File
@@ -28,7 +28,7 @@ export function MenuLink({
<Link <Link
href={href} href={href}
className="flex sm:flex-col max-sm:items-center justify-between h-full" className="flex sm:flex-col max-sm:items-center justify-between h-full"
onClick={() => setModal(null, '')} onClick={() => setModal(null)}
> >
<h4 className="h4 fontme">{title}</h4> <h4 className="h4 fontme">{title}</h4>
<div className="self-end">{children}</div> <div className="self-end">{children}</div>
+20 -12
View File
@@ -1,35 +1,43 @@
'use client'; 'use client';
import { StoriesModal } from '@/components/modals/StoriesModal'; import { StoriesModal } from '@/components/modals/StoriesModal';
import { stories } from '@/consts/stories'; import { StoryFormModal } from '@/components/modals/StoryFormModal';
import { OpenFormModalWrapper } from '@/hocs/OpenFormModalWrapper';
import { useGetStories } from '@/queries/getStories';
import { useModalStore } from '@/stores/useModalStore'; import { useModalStore } from '@/stores/useModalStore';
import Image from 'next/image'; import Image from 'next/image';
import AddIcon from '../../public/icons/add.svg';
import { GradientButton } from './GradientButton';
export function StoriesButton() { export function StoriesButton() {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
const { data: stories } = useGetStories();
return ( return (
<div className="flex mx-auto"> <div className="flex items-center mx-auto">
{stories.slice(-3).map(({ id, title, poster }, index) => ( {stories?.slice(-3).map(({ id, text, preview }, index) => (
<button <button
onClick={() => onClick={() => setModal(<StoriesModal startIndex={index} />)}
setModal( className="cursor-pointer outline-none rounded-full aspect-square first:translate-x-2 [:nth-last-child(2)]:-translate-x-2 w-full"
<StoriesModal startIndex={stories.length - 3 + index} />,
'stories'
)
}
className="lg:p-[0.069vw] p-px cursor-pointer outline-none bg-gradient rounded-full aspect-square relative first:translate-x-2 last:-translate-x-2"
key={id} key={id}
> >
<div className="lg:p-[0.069vw] p-px bg-gradient rounded-full relative aspect-square">
<Image <Image
src={poster} src={process.env.NEXT_PUBLIC_S3_BUCKET + preview}
fill fill
alt={title} alt={text}
className="rounded-full object-cover object-center aspect-square lg:border-[0.139vw] border-[2px] border-[#0F1011] !relative" className="rounded-full object-cover object-center aspect-square lg:border-[0.139vw] border-[2px] border-[#0F1011] !relative"
sizes="(min-width: 1440px) 3.611vw, 52px" sizes="(min-width: 1440px) 3.611vw, 52px"
/> />
</div>
</button> </button>
))} ))}
<OpenFormModalWrapper modal={<StoryFormModal action={'create'} />}>
<GradientButton>
<AddIcon className="text-white lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4" />
</GradientButton>
</OpenFormModalWrapper>
</div> </div>
); );
} }
+10
View File
@@ -0,0 +1,10 @@
import { IArticle } from '@/types/IArticle';
import { ICompany } from '@/types/ICompany';
import { IProject } from '@/types/IProject';
import { IStory } from '@/types/IStory';
export function isArticle(
item: ICompany | IArticle | IProject | IStory
): item is IArticle {
return 'blocks' in item;
}
+2 -1
View File
@@ -1,9 +1,10 @@
import { IArticle } from '@/types/IArticle'; import { IArticle } from '@/types/IArticle';
import { ICompany } from '@/types/ICompany'; import { ICompany } from '@/types/ICompany';
import { IProject } from '@/types/IProject'; import { IProject } from '@/types/IProject';
import { IStory } from '@/types/IStory';
export function isCompany( export function isCompany(
item: IProject | ICompany | IArticle item: IProject | ICompany | IArticle | IStory
): item is ICompany { ): item is ICompany {
return 'color' in item; return 'color' in item;
} }
+2 -1
View File
@@ -1,9 +1,10 @@
import { IArticle } from '@/types/IArticle'; import { IArticle } from '@/types/IArticle';
import { ICompany } from '@/types/ICompany'; import { ICompany } from '@/types/ICompany';
import { IProject } from '@/types/IProject'; import { IProject } from '@/types/IProject';
import { IStory } from '@/types/IStory';
export function isProject( export function isProject(
item: IProject | ICompany | IArticle item: IProject | ICompany | IArticle | IStory
): item is IProject { ): item is IProject {
return 'image' in item; return 'image' in item;
} }