upd
This commit is contained in:
@@ -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
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ export default async function HomePage() {
|
|||||||
<Awards />
|
<Awards />
|
||||||
<Projects />
|
<Projects />
|
||||||
<Clients />
|
<Clients />
|
||||||
{/* <ReorderGrid /> */}
|
|
||||||
</HydrationBoundary>
|
</HydrationBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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'
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
+46
-47
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
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 }),
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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
|
<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
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user