upd
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
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_TINYMCE_API_KEY=2vf68779upg45y46o6g5gaxldy9gzr399eyaaqa0ki3mj2h2
|
||||
@@ -1,7 +1,6 @@
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
readonly NEXT_PUBLIC_API: string;
|
||||
readonly NEXT_PUBLIC_OLD_API: string;
|
||||
readonly NEXT_PUBLIC_S3_BUCKET: string;
|
||||
readonly NEXT_PUBLIC_TINYMCE_API_KEY: string;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,6 @@
|
||||
import ky from 'ky';
|
||||
|
||||
export const oldApi = ky.extend({
|
||||
prefixUrl: process.env.NEXT_PUBLIC_OLD_API,
|
||||
});
|
||||
|
||||
export const api = ky.extend({
|
||||
prefixUrl: process.env.NEXT_PUBLIC_API,
|
||||
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();
|
||||
// }
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ArticlesList } from '@/components/pages/BlogPage/ArticlesList';
|
||||
import { ArticlesPageActions } from '@/components/pages/BlogPage/ArticlesPageActions';
|
||||
import { getQueryClient } from '@/lib/queryClient';
|
||||
import { IArticle } from '@/types/IArticle';
|
||||
import { IStory } from '@/types/IStory';
|
||||
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
|
||||
|
||||
export default async function BlogPage({
|
||||
@@ -33,6 +34,11 @@ export default async function BlogPage({
|
||||
.json<IArticle[]>(),
|
||||
});
|
||||
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: ['stories'],
|
||||
queryFn: async () => await api.get('stories').json<IStory[]>(),
|
||||
});
|
||||
|
||||
return (
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<ArticlesPageActions tags={tags} />
|
||||
|
||||
@@ -58,7 +58,6 @@ export default async function HomePage() {
|
||||
<Awards />
|
||||
<Projects />
|
||||
<Clients />
|
||||
{/* <ReorderGrid /> */}
|
||||
</HydrationBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ html {
|
||||
}
|
||||
|
||||
@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 {
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<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>
|
||||
{children}
|
||||
<ModalContainer />
|
||||
|
||||
@@ -9,7 +9,7 @@ export function DeleteItemModal({
|
||||
id,
|
||||
}: {
|
||||
title: string;
|
||||
entity: 'projects' | 'companies' | 'articles';
|
||||
entity: 'projects' | 'companies' | 'articles' | 'stories';
|
||||
id: string;
|
||||
}) {
|
||||
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">
|
||||
<p className="text-2xl">{title}</p>
|
||||
<button
|
||||
onClick={() => setModal(null, '')}
|
||||
onClick={() => setModal(null)}
|
||||
className="hover:bg-white
|
||||
hover:bg-opacity-10 p-2 transition-colors rounded-full cursor-pointer group"
|
||||
>
|
||||
@@ -37,7 +37,9 @@ export function DeleteItemModal({
|
||||
? 'проект'
|
||||
: entity === 'companies'
|
||||
? 'компанию'
|
||||
: 'статью'}
|
||||
: entity === 'articles'
|
||||
? 'статью'
|
||||
: 'историю'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,20 +16,17 @@ import TrashIcon from '../../public/icons/trash.svg';
|
||||
|
||||
export function ImageUploader<T extends FieldValues>({
|
||||
dest,
|
||||
fieldName,
|
||||
// item,
|
||||
name,
|
||||
label,
|
||||
className = '',
|
||||
required = false,
|
||||
}: {
|
||||
dest: string;
|
||||
fieldName: Path<T>;
|
||||
item: Record<'img', string> & Record<'id', string>;
|
||||
name: Path<T>;
|
||||
label: string;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
// const [currentImg, setCurrentImg] = useState(item.img);
|
||||
const [file, setFile] = useState<File>();
|
||||
const [previewFile, setPreviewFile] = useState('');
|
||||
|
||||
@@ -55,24 +52,19 @@ export function ImageUploader<T extends FieldValues>({
|
||||
const filePaths = await api
|
||||
.post('upload', { body: formData })
|
||||
.json<PathValue<T, Path<T>>[]>();
|
||||
setValue(fieldName, filePaths[0]!);
|
||||
setValue(name, filePaths[0]!);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(`Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}, [dest, fieldName, file, setValue]);
|
||||
}, [dest, name, file, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
uploadFile();
|
||||
}, [file, uploadFile]);
|
||||
|
||||
const currentImg = useWatch({ control, name: fieldName });
|
||||
|
||||
// useEffect(() => {
|
||||
// const { unsubscribe } = watch((input) => setCurrentImg(input[fieldName]));
|
||||
// return unsubscribe;
|
||||
// }, [watch, fieldName]);
|
||||
const currentImg = useWatch({ control, name: name });
|
||||
|
||||
return (
|
||||
<Dropzone
|
||||
@@ -93,7 +85,7 @@ export function ImageUploader<T extends FieldValues>({
|
||||
type="file"
|
||||
accept={'image/*'}
|
||||
required={required}
|
||||
{...{ ...register(fieldName, { required }), ref: inputRef }}
|
||||
{...{ ...register(name, { required }), ref: inputRef }}
|
||||
onChange={handleChangeFile}
|
||||
{...getInputProps()}
|
||||
/>
|
||||
@@ -101,7 +93,7 @@ export function ImageUploader<T extends FieldValues>({
|
||||
<>
|
||||
<div
|
||||
className={`relative${
|
||||
fieldName === 'image' ? ' max-w-[calc(292/824*100%)]' : ''
|
||||
name === 'image' ? ' max-w-[calc(292/824*100%)]' : ''
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
@@ -111,7 +103,7 @@ export function ImageUploader<T extends FieldValues>({
|
||||
}
|
||||
alt={''}
|
||||
className={`pointer-events-none aspect-square object-${
|
||||
fieldName === 'image' ? 'cover rounded-2xl' : 'contain'
|
||||
name === 'image' ? 'cover rounded-2xl' : 'contain'
|
||||
} !relative`}
|
||||
fill
|
||||
sizes="100%"
|
||||
@@ -133,7 +125,7 @@ export function ImageUploader<T extends FieldValues>({
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFile(undefined!);
|
||||
setValue(fieldName, undefined!);
|
||||
setValue(name, undefined!);
|
||||
setPreviewFile('');
|
||||
}}
|
||||
className="bg-[#37393B99] px-3 py-2 rounded-xl cursor-pointer"
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useModalStore } from '@/stores/useModalStore';
|
||||
import { IArticle } from '@/types/IArticle';
|
||||
import { ICompany } from '@/types/ICompany';
|
||||
import { IProject } from '@/types/IProject';
|
||||
import { IStory } from '@/types/IStory';
|
||||
import { isArticle } from '@/utils/isArticle';
|
||||
import { isCompany } from '@/utils/isCompany';
|
||||
import { isProject } from '@/utils/isProject';
|
||||
import { SyntheticEvent } from 'react';
|
||||
@@ -14,11 +16,12 @@ import { DeleteItemModal } from './DeleteItemModal';
|
||||
import { ArticleContentFormModal } from './modals/ArticleContentFormModal';
|
||||
import { CompanyFormModal } from './modals/CompanyFormModal';
|
||||
import { ProjectFormModal } from './modals/ProjectFormModal';
|
||||
import { StoryFormModal } from './modals/StoryFormModal';
|
||||
|
||||
export function ItemActions({
|
||||
item,
|
||||
}: {
|
||||
item: IProject | ICompany | IArticle;
|
||||
item: IProject | ICompany | IArticle | IStory;
|
||||
}) {
|
||||
const { data: auth } = useCheckAuthQuery();
|
||||
|
||||
@@ -28,21 +31,12 @@ export function ItemActions({
|
||||
e.stopPropagation();
|
||||
if (isProject(item)) {
|
||||
const { company, ...project } = item;
|
||||
setModal(
|
||||
<ProjectFormModal action="edit" defaultValues={project} />,
|
||||
'editProjectForm'
|
||||
);
|
||||
setModal(<ProjectFormModal action="edit" defaultValues={project} />);
|
||||
} else if (isCompany(item)) {
|
||||
const { projects, ...company } = item;
|
||||
setModal(
|
||||
<CompanyFormModal action="edit" defaultValues={company} />,
|
||||
'editCompanyForm'
|
||||
);
|
||||
} else
|
||||
setModal(
|
||||
<ArticleContentFormModal {...item} />,
|
||||
'articleContentFormModal'
|
||||
);
|
||||
setModal(<CompanyFormModal action="edit" defaultValues={company} />);
|
||||
} else if (isArticle(item)) setModal(<ArticleContentFormModal {...item} />);
|
||||
else setModal(<StoryFormModal action={'edit'} defaultValues={item} />);
|
||||
}
|
||||
|
||||
function handleDelete(e: SyntheticEvent) {
|
||||
@@ -56,19 +50,20 @@ export function ItemActions({
|
||||
? 'проекта'
|
||||
: isCompany(item)
|
||||
? 'компании'
|
||||
: 'статьи')
|
||||
: isArticle(item)
|
||||
? 'статьи'
|
||||
: 'истории')
|
||||
}
|
||||
entity={
|
||||
isProject(item)
|
||||
? 'projects'
|
||||
: isCompany(item)
|
||||
? 'companies'
|
||||
: 'articles'
|
||||
: isArticle(item)
|
||||
? 'articles'
|
||||
: 'stories'
|
||||
}
|
||||
/>,
|
||||
`delete${
|
||||
isProject(item) ? 'Project' : isCompany(item) ? 'Company' : 'Article'
|
||||
}`
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { api } from '@/api';
|
||||
import { useMediaQueries } from '@/hooks/useMediaQueries';
|
||||
import { useScroll } from '@/hooks/useScroll';
|
||||
import { useCheckAuthQuery } from '@/queries/checkAuth';
|
||||
import { useModalStore } from '@/stores/useModalStore';
|
||||
import { HeaderLink } from '@/ui/HeaderLink';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
@@ -21,8 +20,6 @@ import SkolkovoIcon from '../../../public/icons/skolkovo.svg';
|
||||
import { Products } from './Products';
|
||||
|
||||
export function Header() {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: auth } = useCheckAuthQuery();
|
||||
|
||||
@@ -4,11 +4,11 @@ import { useModalStore } from '@/stores/useModalStore';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function ModalContainer() {
|
||||
const { modal, name, setModal } = useModalStore();
|
||||
const { modal, setModal } = useModalStore();
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setModal(null, '');
|
||||
if (e.key === 'Escape') setModal(null);
|
||||
};
|
||||
document.addEventListener('keydown', listener);
|
||||
|
||||
@@ -17,16 +17,7 @@ export function ModalContainer() {
|
||||
|
||||
return (
|
||||
modal && (
|
||||
<div
|
||||
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="fixed left-0 z-[14] w-full h-full flex justify-center items-start transition-opacity">
|
||||
<div className="absolute backdrop-blur-lg w-full h-full z-[1]" />
|
||||
{modal}
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
<ImageUploader
|
||||
dest="articles"
|
||||
fieldName={`blocks.${index}.avatar`}
|
||||
name={`blocks.${index}.avatar`}
|
||||
required
|
||||
item={{ id: item.id, img: item.avatar }}
|
||||
label="Автор"
|
||||
/>
|
||||
<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">
|
||||
<ImageUploader
|
||||
dest="blog"
|
||||
fieldName={`blocks.${index}.images.${imgIndex}.img`}
|
||||
item={item}
|
||||
name={`blocks.${index}.images.${imgIndex}.img`}
|
||||
label="Выберите изображение"
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import { api } from '@/api';
|
||||
import { useFieldArrayFormContext } from '@/lib/FieldArrayFormProvider';
|
||||
import { IVideo } from '@/types/IArticle';
|
||||
import { Button } from '@/ui/Button';
|
||||
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 { IArticleInput } from '../modals/ArticleFormModal';
|
||||
import { VideoUploader } from '../VideoUploader';
|
||||
|
||||
export function ArticleVideoUploader({
|
||||
item,
|
||||
@@ -18,45 +11,44 @@ export function ArticleVideoUploader({
|
||||
item: IVideo & { id: string };
|
||||
index: number;
|
||||
}) {
|
||||
const { remove, register, control, setValue } =
|
||||
useFieldArrayFormContext<IArticleInput>();
|
||||
const { remove } = useFieldArrayFormContext();
|
||||
|
||||
const [file, setFile] = useState<File>();
|
||||
const [previewFile, setPreviewFile] = useState('');
|
||||
// const [file, setFile] = useState<File>();
|
||||
// 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>) {
|
||||
if (!e.target.files) return;
|
||||
// function handleChangeFile(e: ChangeEvent<HTMLInputElement>) {
|
||||
// if (!e.target.files) return;
|
||||
|
||||
const targetFile = e.target.files[0];
|
||||
// const targetFile = e.target.files[0];
|
||||
|
||||
setFile(targetFile);
|
||||
setPreviewFile(URL.createObjectURL(targetFile));
|
||||
}
|
||||
// setFile(targetFile);
|
||||
// setPreviewFile(URL.createObjectURL(targetFile));
|
||||
// }
|
||||
|
||||
const uploadFile = useCallback(async () => {
|
||||
if (!file) return;
|
||||
// const uploadFile = useCallback(async () => {
|
||||
// if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('dest', 'articles');
|
||||
formData.append('files', file);
|
||||
// const formData = new FormData();
|
||||
// formData.append('dest', 'articles');
|
||||
// formData.append('files', file);
|
||||
|
||||
try {
|
||||
const filePaths = await api
|
||||
.post('upload', { body: formData })
|
||||
.json<string[]>();
|
||||
setValue(`blocks.${index}.src`, filePaths[0]!);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
alert(`Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}, [file, index, setValue]);
|
||||
// try {
|
||||
// const filePaths = await api
|
||||
// .post('upload', { body: formData })
|
||||
// .json<string[]>();
|
||||
// setValue(`blocks.${index}.src`, filePaths[0]!);
|
||||
// } catch (error) {
|
||||
// if (error instanceof Error) {
|
||||
// alert(`Error: ${error.message}`);
|
||||
// }
|
||||
// }
|
||||
// }, [file, index, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
uploadFile();
|
||||
}, [file, uploadFile]);
|
||||
// useEffect(() => {
|
||||
// uploadFile();
|
||||
// }, [file, uploadFile]);
|
||||
|
||||
return (
|
||||
<Reorder.Item
|
||||
@@ -64,7 +56,7 @@ export function ArticleVideoUploader({
|
||||
as="div"
|
||||
className="w-full flex items-start gap-5"
|
||||
>
|
||||
<Dropzone
|
||||
{/* <Dropzone
|
||||
onDrop={([file]) => {
|
||||
setFile(file);
|
||||
setPreviewFile(URL.createObjectURL(file));
|
||||
@@ -141,7 +133,8 @@ export function ArticleVideoUploader({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</Dropzone> */}
|
||||
<VideoUploader name={`blocks.${index}.src`} dest="articles" />
|
||||
<button onClick={() => remove(index)} className="cursor-pointer">
|
||||
<TrashIcon className="text-white lg:w-[1.111vw] lg:h-[1.111vw] w-4 h-4" />
|
||||
</button>
|
||||
|
||||
@@ -57,7 +57,7 @@ export function ArticleContentFormModal({
|
||||
blocks: JSON.stringify(getValues('blocks')),
|
||||
drafted,
|
||||
});
|
||||
setModal(null, '');
|
||||
setModal(null);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -83,8 +83,7 @@ export function ArticleContentFormModal({
|
||||
posterImage,
|
||||
id,
|
||||
}}
|
||||
/>,
|
||||
'editArticleModal'
|
||||
/>
|
||||
)
|
||||
}
|
||||
icon={
|
||||
|
||||
@@ -36,7 +36,7 @@ export function ArticleFormActions({
|
||||
</div>
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
|
||||
@@ -39,10 +39,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
|
||||
...data,
|
||||
blocks: JSON.stringify(defaultValues ? defaultValues.blocks : []),
|
||||
});
|
||||
setModal(
|
||||
<ArticleContentFormModal {...article} />,
|
||||
'articleContentFormModal'
|
||||
);
|
||||
setModal(<ArticleContentFormModal {...article} />);
|
||||
}
|
||||
|
||||
const { handleSubmit, getValues, control } = form;
|
||||
@@ -57,7 +54,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
|
||||
),
|
||||
drafted,
|
||||
});
|
||||
setModal(null, '');
|
||||
setModal(null);
|
||||
}
|
||||
|
||||
const { mutateAsync } = useArticleMutation(
|
||||
@@ -77,8 +74,8 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
|
||||
submitHandler={handleSubmit(onSubmit)}
|
||||
disabled={!title || !tags || !cardImage || !posterImage}
|
||||
/>
|
||||
<FormProvider {...form}>
|
||||
<form className="space-y-10">
|
||||
<FormProvider {...form}>
|
||||
<TextInput
|
||||
name="title"
|
||||
label="Название статьи"
|
||||
@@ -87,8 +84,7 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
|
||||
<ImageUploader
|
||||
dest="blog"
|
||||
required
|
||||
fieldName="cardImage"
|
||||
item={{ id: '', img: defaultValues?.cardImage ?? '' }}
|
||||
name="cardImage"
|
||||
label="Загрузите или перетащите изображение для превью (рекомендованнный размер 1080/1080 px)"
|
||||
/>
|
||||
<div className="gap-y-4 flex flex-col">
|
||||
@@ -103,12 +99,11 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
|
||||
<ImageUploader
|
||||
dest="blog"
|
||||
required
|
||||
fieldName="posterImage"
|
||||
item={{ id: '', img: defaultValues?.posterImage ?? '' }}
|
||||
name="posterImage"
|
||||
label="Загрузите или перетащите изображение для превью (рекомендованнный размер 1080/1080 px)"
|
||||
/>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -43,7 +43,7 @@ export function CompanyFormModal<TAction extends 'create' | 'edit'>({
|
||||
: await api.put(`companies/${defaultValues?.id}`, { json }).json(),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['companies'] });
|
||||
setModal(null, '');
|
||||
setModal(null);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ export function CompanyFormModal<TAction extends 'create' | 'edit'>({
|
||||
<>
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
@@ -69,15 +69,13 @@ export function CompanyFormModal<TAction extends 'create' | 'edit'>({
|
||||
<ImageUploader
|
||||
className="aspect-square w-full"
|
||||
dest="projects"
|
||||
fieldName="logo"
|
||||
item={{ id: '', img: defaultValues?.logo ?? '' }}
|
||||
name="logo"
|
||||
label="Загрузите или перетащите логотип ( в формате svg )"
|
||||
/>
|
||||
<ImageUploader
|
||||
className="aspect-square w-full"
|
||||
dest="projects"
|
||||
fieldName="mapIcon"
|
||||
item={{ id: '', img: defaultValues?.mapIcon ?? '' }}
|
||||
name="mapIcon"
|
||||
label="Загрузите или перетащите иконку ( в формате svg )"
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -96,8 +96,7 @@ export function ProjectFormModal<TAction extends 'create' | 'edit'>({
|
||||
</div>
|
||||
<ImageUploader
|
||||
dest="projects"
|
||||
fieldName="image"
|
||||
item={{ img: defaultValues?.image ?? '', id: '' }}
|
||||
name="image"
|
||||
label="Загрузите или перетащите изображение для превью (рекомендованнный размер 1080/1080 px)"
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -126,7 +125,6 @@ export function ProjectFormModal<TAction extends 'create' | 'edit'>({
|
||||
</select>
|
||||
<OpenFormModalWrapper
|
||||
modal={<CompanyFormModal action={'create'} />}
|
||||
modalName="companyForm"
|
||||
>
|
||||
<Button
|
||||
rounded="2xl"
|
||||
@@ -184,7 +182,7 @@ export function ProjectFormModal<TAction extends 'create' | 'edit'>({
|
||||
</div>
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { stories } from '@/consts/stories';
|
||||
import { useMediaQueries } from '@/hooks/useMediaQueries';
|
||||
import { useGetStories } from '@/queries/getStories';
|
||||
import { useModalStore } from '@/stores/useModalStore';
|
||||
import { createRef, RefObject, useEffect, useRef, useState } from 'react';
|
||||
import { useSwipeable } from 'react-swipeable';
|
||||
import CloseIcon from '../../../public/icons/close.svg';
|
||||
import { ItemActions } from '../ItemActions';
|
||||
|
||||
export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
|
||||
const { setModal } = useModalStore();
|
||||
@@ -16,9 +17,12 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data: stories } = useGetStories();
|
||||
|
||||
useEffect(() => {
|
||||
if (!stories) return;
|
||||
setVideoRefs(stories.map(createRef<HTMLVideoElement>));
|
||||
}, []);
|
||||
}, [stories]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -40,7 +44,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
|
||||
|
||||
useEffect(() => {
|
||||
if (currentProgress === 1 && videoRefs.length) {
|
||||
if (currentIndex === videoRefs.length - 1) setModal(null, '');
|
||||
if (currentIndex === videoRefs.length - 1) setModal(null);
|
||||
else setCurrentIndex((prev) => prev + 1);
|
||||
}
|
||||
}, [currentProgress, setModal, videoRefs]);
|
||||
@@ -68,7 +72,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
|
||||
preventScrollOnSwipe: true,
|
||||
touchEventOptions: { passive: false },
|
||||
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)),
|
||||
onTouchStartOrOnMouseDown: (e) => {
|
||||
const slider = ref.current;
|
||||
@@ -80,26 +84,30 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
|
||||
)
|
||||
return;
|
||||
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));
|
||||
},
|
||||
});
|
||||
|
||||
if (!stories) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
<div
|
||||
{...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">
|
||||
{!!videoRefs.length &&
|
||||
stories.map(({ id, video, title, poster }, index) => (
|
||||
stories?.map(({ id, video, preview, text, createdAt }, index) => (
|
||||
<div
|
||||
style={{
|
||||
transform: isLg
|
||||
@@ -110,7 +118,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
|
||||
left: isLg || isMd ? 24 * (1 - currentIndex) : 0,
|
||||
}}
|
||||
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
|
||||
? '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'
|
||||
@@ -119,8 +127,8 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
|
||||
>
|
||||
<video
|
||||
ref={videoRefs[index]}
|
||||
src={video}
|
||||
poster={poster}
|
||||
src={process.env.NEXT_PUBLIC_S3_BUCKET + video}
|
||||
poster={process.env.NEXT_PUBLIC_S3_BUCKET + preview}
|
||||
className="absolute inset-0 object-cover object-center md:rounded-xl"
|
||||
playsInline
|
||||
/>
|
||||
@@ -133,7 +141,7 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
|
||||
/>
|
||||
{currentIndex === index && (
|
||||
<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="h-1 bg-white transition-[width] rounded-[34px]"
|
||||
@@ -142,11 +150,13 @@ export function StoriesModal({ startIndex = 0 }: { startIndex?: number }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ItemActions item={{ id, text, video, preview, createdAt }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{stories && (
|
||||
<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">
|
||||
{stories.map(({ id }, index) => (
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
color="secondary"
|
||||
rounded="2xl"
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ArticleContentFormModal {...article} />,
|
||||
'articleContentFormModal'
|
||||
)
|
||||
}
|
||||
onClick={() => setModal(<ArticleContentFormModal {...article} />)}
|
||||
icon={
|
||||
<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 (
|
||||
<div className="lg:sticky lg:top-12 absolute flex flex-col justify-between gap-y-4">
|
||||
<div className="space-y-2 max-lg:hidden">
|
||||
<OpenFormModalWrapper
|
||||
modalName="addArticle"
|
||||
modal={<ArticleFormModal action="create" />}
|
||||
>
|
||||
<OpenFormModalWrapper modal={<ArticleFormModal action="create" />}>
|
||||
<Button
|
||||
className="px-3 py-2 outline-none btns flex items-center text-nowrap"
|
||||
rounded="xl"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { oldApi } from '@/api';
|
||||
import { api } from '@/api';
|
||||
import regionsData from '@/consts/regionsData.json';
|
||||
import { GradientButton } from '@/ui/GradientButton';
|
||||
import { Title } from '@/ui/Title';
|
||||
@@ -33,18 +33,17 @@ export function Calculator() {
|
||||
const [calculated, setCalculated] = useState(true);
|
||||
|
||||
async function getRegionName() {
|
||||
const result: any = await oldApi.get('getRegionName').json();
|
||||
|
||||
if (result.error) {
|
||||
setSelectedRegion(regionsData.find((region) => region.id === 11));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result: any = await api
|
||||
.get('getRegionName')
|
||||
.json<{ regionName: string }>();
|
||||
const foundRegion =
|
||||
regionsData.find((region) => region.name === result.regionName) ||
|
||||
regionsData.find((region) => region.name === result.regionName) ??
|
||||
regionsData.find((region) => region.id === 11);
|
||||
|
||||
setSelectedRegion(foundRegion);
|
||||
} catch (error) {
|
||||
setSelectedRegion(regionsData.find((region) => region.id === 11));
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -66,7 +66,6 @@ export function Clients() {
|
||||
</Title>
|
||||
<OpenFormModalWrapper
|
||||
modal={<CompanyFormModal action="create" />}
|
||||
modalName="addCompany"
|
||||
className="aspect-square flex flex-col items-center justify-center gap-3"
|
||||
>
|
||||
<GradientButton>
|
||||
|
||||
@@ -62,7 +62,7 @@ export function Motivation() {
|
||||
/>
|
||||
</div>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
{auth && (
|
||||
<OpenFormModalWrapper
|
||||
modalName="addProjectForm"
|
||||
modal={<ProjectFormModal action={'create'} />}
|
||||
className="btns sticky top-0"
|
||||
>
|
||||
|
||||
+45
-46
@@ -8,63 +8,63 @@
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Амурская область",
|
||||
"name": "Амурская Область",
|
||||
"areaInComplex": 5837,
|
||||
"areaApartment": 48,
|
||||
"costPerSquare": 131
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Архангельская область",
|
||||
"name": "Архангельская Область",
|
||||
"areaInComplex": 6566,
|
||||
"areaApartment": 44,
|
||||
"costPerSquare": 101
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Астраханская область",
|
||||
"name": "Астраханская Область",
|
||||
"areaInComplex": 11851,
|
||||
"areaApartment": 50,
|
||||
"costPerSquare": 96
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Белгородская область",
|
||||
"name": "Белгородская Область",
|
||||
"areaInComplex": 4570,
|
||||
"areaApartment": 51,
|
||||
"costPerSquare": 86
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Брянская область",
|
||||
"name": "Брянская Область",
|
||||
"areaInComplex": 7333,
|
||||
"areaApartment": 59,
|
||||
"costPerSquare": 60
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "Владимирская область",
|
||||
"name": "Владимирская Область",
|
||||
"areaInComplex": 7509,
|
||||
"areaApartment": 56,
|
||||
"costPerSquare": 54
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Волгоградская область",
|
||||
"name": "Волгоградская Область",
|
||||
"areaInComplex": 7378,
|
||||
"areaApartment": 52,
|
||||
"costPerSquare": 76
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Вологодская область",
|
||||
"name": "Вологодская Область",
|
||||
"areaInComplex": 5235,
|
||||
"areaApartment": 51,
|
||||
"costPerSquare": 54
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "Воронежская область",
|
||||
"name": "Воронежская Область",
|
||||
"areaInComplex": 13002,
|
||||
"areaApartment": 48,
|
||||
"costPerSquare": 77
|
||||
@@ -99,14 +99,14 @@
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "Ивановская область",
|
||||
"name": "Ивановская Область",
|
||||
"areaInComplex": 6594,
|
||||
"areaApartment": 58,
|
||||
"costPerSquare": 60
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "Иркутская область",
|
||||
"name": "Иркутская Область",
|
||||
"areaInComplex": 5067,
|
||||
"areaApartment": 51,
|
||||
"costPerSquare": 103
|
||||
@@ -120,14 +120,14 @@
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "Калининградская область",
|
||||
"name": "Калининградская Область",
|
||||
"areaInComplex": 7209,
|
||||
"areaApartment": 56,
|
||||
"costPerSquare": 87
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "Калужская область",
|
||||
"name": "Калужская Область",
|
||||
"areaInComplex": 7833,
|
||||
"areaApartment": 55,
|
||||
"costPerSquare": 88
|
||||
@@ -141,21 +141,21 @@
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "Кемеровская область - Кузбасс",
|
||||
"name": "Кемеровская Область - Кузбасс",
|
||||
"areaInComplex": 7256,
|
||||
"areaApartment": 52,
|
||||
"costPerSquare": 71
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "Кировская область",
|
||||
"name": "Кировская Область",
|
||||
"areaInComplex": 6249,
|
||||
"areaApartment": 51,
|
||||
"costPerSquare": 80
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "Костромская область",
|
||||
"name": "Костромская Область",
|
||||
"areaInComplex": 2821,
|
||||
"areaApartment": 55,
|
||||
"costPerSquare": 67
|
||||
@@ -176,91 +176,91 @@
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "Курганская область",
|
||||
"name": "Курганская Область",
|
||||
"areaInComplex": 5434,
|
||||
"areaApartment": 53,
|
||||
"costPerSquare": 63
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "Курская область",
|
||||
"name": "Курская Область",
|
||||
"areaInComplex": 7045,
|
||||
"areaApartment": 54,
|
||||
"costPerSquare": 90
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "Ленинградская область",
|
||||
"name": "Ленинградская Область",
|
||||
"areaInComplex": 12385,
|
||||
"areaApartment": 40,
|
||||
"costPerSquare": 134
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "Липецкая область",
|
||||
"name": "Липецкая Область",
|
||||
"areaInComplex": 6231,
|
||||
"areaApartment": 58,
|
||||
"costPerSquare": 69
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "Магаданская область",
|
||||
"name": "Магаданская Область",
|
||||
"areaInComplex": 3187,
|
||||
"areaApartment": 55,
|
||||
"costPerSquare": 85
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"name": "Московская область",
|
||||
"name": "Московская Область",
|
||||
"areaInComplex": 13213,
|
||||
"areaApartment": 46,
|
||||
"costPerSquare": 149
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "Нижегородская область",
|
||||
"name": "Нижегородская Область",
|
||||
"areaInComplex": 8760,
|
||||
"areaApartment": 53,
|
||||
"costPerSquare": 106
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"name": "Новгородская область",
|
||||
"name": "Новгородская Область",
|
||||
"areaInComplex": 5953,
|
||||
"areaApartment": 53,
|
||||
"costPerSquare": 71
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"name": "Новосибирская область",
|
||||
"name": "Новосибирская Область",
|
||||
"areaInComplex": 6748,
|
||||
"areaApartment": 51,
|
||||
"costPerSquare": 107
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"name": "Омская область",
|
||||
"name": "Омская Область",
|
||||
"areaInComplex": 8625,
|
||||
"areaApartment": 55,
|
||||
"costPerSquare": 85
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"name": "Оренбургская область",
|
||||
"name": "Оренбургская Область",
|
||||
"areaInComplex": 7686,
|
||||
"areaApartment": 49,
|
||||
"costPerSquare": 67
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"name": "Орловская область",
|
||||
"name": "Орловская Область",
|
||||
"areaInComplex": 11543,
|
||||
"areaApartment": 58,
|
||||
"costPerSquare": 34
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"name": "Пензенская область",
|
||||
"name": "Пензенская Область",
|
||||
"areaInComplex": 12832,
|
||||
"areaApartment": 54,
|
||||
"costPerSquare": 74
|
||||
@@ -281,7 +281,7 @@
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"name": "Псковская область",
|
||||
"name": "Псковская Область",
|
||||
"areaInComplex": 5750,
|
||||
"areaApartment": 52,
|
||||
"costPerSquare": 59
|
||||
@@ -400,49 +400,49 @@
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"name": "Ростовская область",
|
||||
"name": "Ростовская Область",
|
||||
"areaInComplex": 10987,
|
||||
"areaApartment": 48,
|
||||
"costPerSquare": 94
|
||||
},
|
||||
{
|
||||
"id": 59,
|
||||
"name": "Рязанская область",
|
||||
"name": "Рязанская Область",
|
||||
"areaInComplex": 14581,
|
||||
"areaApartment": 49,
|
||||
"costPerSquare": 76
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"name": "Самарская область",
|
||||
"name": "Самарская Область",
|
||||
"areaInComplex": 10571,
|
||||
"areaApartment": 55,
|
||||
"costPerSquare": 82
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
"name": "Саратовская область",
|
||||
"name": "Саратовская Область",
|
||||
"areaInComplex": 7789,
|
||||
"areaApartment": 53,
|
||||
"costPerSquare": 59
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"name": "Сахалинская область",
|
||||
"name": "Сахалинская Область",
|
||||
"areaInComplex": 5668,
|
||||
"areaApartment": 51,
|
||||
"costPerSquare": 153
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"name": "Свердловская область",
|
||||
"name": "Свердловская Область",
|
||||
"areaInComplex": 12467,
|
||||
"areaApartment": 48,
|
||||
"costPerSquare": 111
|
||||
},
|
||||
{
|
||||
"id": 64,
|
||||
"name": "Смоленская область",
|
||||
"name": "Смоленская Область",
|
||||
"areaInComplex": 5512,
|
||||
"areaApartment": 55,
|
||||
"costPerSquare": 54
|
||||
@@ -456,35 +456,35 @@
|
||||
},
|
||||
{
|
||||
"id": 66,
|
||||
"name": "Тамбовская область",
|
||||
"name": "Тамбовская Область",
|
||||
"areaInComplex": 6631,
|
||||
"areaApartment": 54,
|
||||
"costPerSquare": 61
|
||||
},
|
||||
{
|
||||
"id": 67,
|
||||
"name": "Тверская область",
|
||||
"name": "Тверская Область",
|
||||
"areaInComplex": 5696,
|
||||
"areaApartment": 52,
|
||||
"costPerSquare": 79
|
||||
},
|
||||
{
|
||||
"id": 68,
|
||||
"name": "Томская область",
|
||||
"name": "Томская Область",
|
||||
"areaInComplex": 4397,
|
||||
"areaApartment": 51,
|
||||
"costPerSquare": 105
|
||||
},
|
||||
{
|
||||
"id": 69,
|
||||
"name": "Тульская область",
|
||||
"name": "Тульская Область",
|
||||
"areaInComplex": 7696,
|
||||
"areaApartment": 48,
|
||||
"costPerSquare": 90
|
||||
},
|
||||
{
|
||||
"id": 70,
|
||||
"name": "Тюменская область",
|
||||
"name": "Тюменская Область",
|
||||
"areaInComplex": 11328,
|
||||
"areaApartment": 52,
|
||||
"costPerSquare": 100
|
||||
@@ -498,7 +498,7 @@
|
||||
},
|
||||
{
|
||||
"id": 72,
|
||||
"name": "Ульяновская область",
|
||||
"name": "Ульяновская Область",
|
||||
"areaInComplex": 7177,
|
||||
"areaApartment": 48,
|
||||
"costPerSquare": 75
|
||||
@@ -519,7 +519,7 @@
|
||||
},
|
||||
{
|
||||
"id": 75,
|
||||
"name": "Челябинская область",
|
||||
"name": "Челябинская Область",
|
||||
"areaInComplex": 8631,
|
||||
"areaApartment": 51,
|
||||
"costPerSquare": 78
|
||||
@@ -547,10 +547,9 @@
|
||||
},
|
||||
{
|
||||
"id": 79,
|
||||
"name": "Ярославская область",
|
||||
"name": "Ярославская Область",
|
||||
"areaInComplex": 6502,
|
||||
"areaApartment": 54,
|
||||
"costPerSquare": 77
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,12 +4,10 @@ import { PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
export function OpenFormModalWrapper({
|
||||
modal,
|
||||
modalName,
|
||||
children,
|
||||
className,
|
||||
}: PropsWithChildren<{
|
||||
modal: ReactNode;
|
||||
modalName: string;
|
||||
className?: string;
|
||||
}>) {
|
||||
const { data: auth } = useCheckAuthQuery();
|
||||
@@ -21,7 +19,7 @@ export function OpenFormModalWrapper({
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (!el) return;
|
||||
const handler = () => setModal(modal, modalName);
|
||||
const handler = () => setModal(modal);
|
||||
el!.children.item(0)!.addEventListener('click', handler);
|
||||
return () =>
|
||||
el!.children.item(0)!.removeEventListener('click', handler);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useModalStore } from '@/stores/useModalStore';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export function useDeleteMutation(
|
||||
entity: 'projects' | 'companies' | 'articles',
|
||||
entity: 'projects' | 'companies' | 'articles' | 'stories',
|
||||
id: string
|
||||
) {
|
||||
const { setModal } = useModalStore();
|
||||
@@ -18,7 +18,7 @@ export function useDeleteMutation(
|
||||
if (entity === 'articles')
|
||||
queryClient.setQueryData(['articles', 'drafted', []], []);
|
||||
|
||||
setModal(null, '');
|
||||
setModal(null);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export function useProjectMutation({
|
||||
: await api.put(`projects/${id}`, { json }).json<IProject>(),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['projects'] });
|
||||
setModal(null, '');
|
||||
setModal(null);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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,12 +3,10 @@ import { create } from 'zustand';
|
||||
|
||||
interface IModalState {
|
||||
modal: ReactNode | null;
|
||||
setModal: (modal: ReactNode, modalName: string) => void;
|
||||
name: string;
|
||||
setModal: (modal: ReactNode) => void;
|
||||
}
|
||||
|
||||
export const useModalStore = create<IModalState>(set => ({
|
||||
export const useModalStore = create<IModalState>((set) => ({
|
||||
modal: null,
|
||||
name: '',
|
||||
setModal: (modal: ReactNode, name: string) => set({ modal, name }),
|
||||
setModal: (modal: ReactNode) => set({ modal }),
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface IStory {
|
||||
id: string;
|
||||
text: string;
|
||||
video: string;
|
||||
preview: string;
|
||||
createdAt: string;
|
||||
}
|
||||
+1
-1
@@ -28,7 +28,7 @@ export function MenuLink({
|
||||
<Link
|
||||
href={href}
|
||||
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>
|
||||
<div className="self-end">{children}</div>
|
||||
|
||||
+20
-12
@@ -1,35 +1,43 @@
|
||||
'use client';
|
||||
|
||||
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 Image from 'next/image';
|
||||
import AddIcon from '../../public/icons/add.svg';
|
||||
import { GradientButton } from './GradientButton';
|
||||
|
||||
export function StoriesButton() {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const { data: stories } = useGetStories();
|
||||
|
||||
return (
|
||||
<div className="flex mx-auto">
|
||||
{stories.slice(-3).map(({ id, title, poster }, index) => (
|
||||
<div className="flex items-center mx-auto">
|
||||
{stories?.slice(-3).map(({ id, text, preview }, index) => (
|
||||
<button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<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"
|
||||
onClick={() => setModal(<StoriesModal startIndex={index} />)}
|
||||
className="cursor-pointer outline-none rounded-full aspect-square first:translate-x-2 [:nth-last-child(2)]:-translate-x-2 w-full"
|
||||
key={id}
|
||||
>
|
||||
<div className="lg:p-[0.069vw] p-px bg-gradient rounded-full relative aspect-square">
|
||||
<Image
|
||||
src={poster}
|
||||
src={process.env.NEXT_PUBLIC_S3_BUCKET + preview}
|
||||
fill
|
||||
alt={title}
|
||||
alt={text}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { IArticle } from '@/types/IArticle';
|
||||
import { ICompany } from '@/types/ICompany';
|
||||
import { IProject } from '@/types/IProject';
|
||||
import { IStory } from '@/types/IStory';
|
||||
|
||||
export function isCompany(
|
||||
item: IProject | ICompany | IArticle
|
||||
item: IProject | ICompany | IArticle | IStory
|
||||
): item is ICompany {
|
||||
return 'color' in item;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { IArticle } from '@/types/IArticle';
|
||||
import { ICompany } from '@/types/ICompany';
|
||||
import { IProject } from '@/types/IProject';
|
||||
import { IStory } from '@/types/IStory';
|
||||
|
||||
export function isProject(
|
||||
item: IProject | ICompany | IArticle
|
||||
item: IProject | ICompany | IArticle | IStory
|
||||
): item is IProject {
|
||||
return 'image' in item;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user