todo video uploading using editor
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
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
|
||||
@@ -20,6 +20,7 @@
|
||||
"framer-motion": "^11.3.9",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
"html-react-parser": "^5.1.18",
|
||||
"jose": "^5.9.3",
|
||||
"ky": "^1.4.0",
|
||||
"libphonenumber-js": "^1.11.7",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
+1
-1
@@ -5,6 +5,6 @@ export const oldApi = ky.extend({
|
||||
});
|
||||
|
||||
export const api = ky.extend({
|
||||
prefixUrl: process.env.NEXT_PUBLIC_OLD_API,
|
||||
prefixUrl: process.env.NEXT_PUBLIC_API,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
'use client';
|
||||
|
||||
import { ArticleContentInput } from '@/components/articleInputs/ArticleContentInput';
|
||||
import { ArticleSliderInput } from '@/components/articleInputs/ArticleSliderInput';
|
||||
import { CloseIcon } from '@/components/icons/CloseIcon';
|
||||
import { IArticleFormInput } from '@/components/modals/ArticleFormModal';
|
||||
import { ArticleContent } from '@/components/pages/BlogPage/ArticleContent';
|
||||
import { useEditArticleMutation } from '@/queries/articles/editArticle';
|
||||
import {
|
||||
GetArticleByIdDocument,
|
||||
useGetArticleByIdQuery,
|
||||
} from '@/queries/articles/getArticleById';
|
||||
import { Block, IArticle } from '@/types/IPost';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { reorderFields } from '@/utils/reorderFields';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { Reorder } from 'framer-motion';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
|
||||
export default function DashboardArticlePage({
|
||||
params: { articleId },
|
||||
}: {
|
||||
params: { articleId: string };
|
||||
}) {
|
||||
const { push } = useRouter();
|
||||
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [article, setArticle] = useState<IArticle>();
|
||||
|
||||
const client = useApolloClient();
|
||||
|
||||
useGetArticleByIdQuery({
|
||||
variables: { id: +articleId },
|
||||
onCompleted: ({ article }) => {
|
||||
if (article.__typename === 'Article') {
|
||||
const { __typename, id, ...other } = article;
|
||||
setArticle(other as IArticle);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [editArticle] = useEditArticleMutation({
|
||||
onCompleted() {
|
||||
client.refetchQueries({
|
||||
include: ['GetArticles', GetArticleByIdDocument],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { handleSubmit, control, setValue, register, getValues, watch } =
|
||||
useForm<Pick<IArticleFormInput, 'blocks'>>();
|
||||
|
||||
useEffect(() => {
|
||||
setValue('blocks', article?.blocks as Block[]);
|
||||
}, [article, setValue]);
|
||||
|
||||
const { fields, append, swap, remove } = useFieldArray({
|
||||
control,
|
||||
name: 'blocks',
|
||||
});
|
||||
|
||||
if (!article) return <div>not found</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<form
|
||||
className="space-y-5"
|
||||
onSubmit={handleSubmit(formData => {
|
||||
editArticle({
|
||||
variables: {
|
||||
id: +articleId,
|
||||
input: {
|
||||
...article,
|
||||
blocks: JSON.stringify(formData.blocks),
|
||||
},
|
||||
},
|
||||
});
|
||||
})}
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
append({ type: 'Slider', images: [] });
|
||||
}}
|
||||
>
|
||||
Добавить слайдер
|
||||
</button>
|
||||
<button
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
append({
|
||||
type: 'Content',
|
||||
content: '',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Добавить разметку
|
||||
</button>
|
||||
</div>
|
||||
<Reorder.Group
|
||||
axis="y"
|
||||
values={fields}
|
||||
onReorder={reorderFields(fields, swap)}
|
||||
className="space-y-5"
|
||||
>
|
||||
{fields.map((item, index) =>
|
||||
item.type === 'Slider' ? (
|
||||
<ArticleSliderInput
|
||||
{...{ control, index, register, item, setValue }}
|
||||
removeSlider={remove}
|
||||
key={item.id}
|
||||
/>
|
||||
) : (
|
||||
<ArticleContentInput
|
||||
{...{ control, index, item, setValue, getValues, watch }}
|
||||
removeSlider={remove}
|
||||
key={item.id}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</Reorder.Group>
|
||||
<div className="flex gap-x-4">
|
||||
<Button type="submit" onClick={() => push(`/blog/${articleId}`)}>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowPreview(true);
|
||||
}}
|
||||
>
|
||||
Предварительный просмотр
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{showPreview && (
|
||||
<div className="w-full bg-[#14161F] absolute -top-4 left-0 z-[10] p-4">
|
||||
<ArticleContent {...article} blocks={getValues('blocks')} />
|
||||
<button
|
||||
onClick={() => setShowPreview(false)}
|
||||
className="absolute top-4 right-4"
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return <section>{children}</section>;
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ArticleContentInput } from '@/components/articleInputs/ArticleContentInput';
|
||||
import { ArticleQuoteInput } from '@/components/articleInputs/ArticleQuoteInput';
|
||||
import { ArticleSliderInput } from '@/components/articleInputs/ArticleSliderInput';
|
||||
import { ArticleVideoInput } from '@/components/articleInputs/ArticleVideoInput';
|
||||
import { IArticleFormInput } from '@/components/modals/ArticleFormModal';
|
||||
import { useEditArticleMutation } from '@/queries/articles/editArticle';
|
||||
import {
|
||||
GetArticleByIdDocument,
|
||||
useGetArticleByIdQuery,
|
||||
} from '@/queries/articles/getArticleById';
|
||||
import { Block } from '@/types/IPost';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { reorderFields } from '@/utils/reorderFields';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { Reorder } from 'framer-motion';
|
||||
import { useEffect } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
|
||||
export default function DashboardArticlePage({
|
||||
params: { articleId },
|
||||
}: {
|
||||
params: { articleId: string };
|
||||
}) {
|
||||
const client = useApolloClient();
|
||||
|
||||
const { data } = useGetArticleByIdQuery({
|
||||
variables: { id: +articleId },
|
||||
});
|
||||
|
||||
const [editArticle] = useEditArticleMutation({
|
||||
onCompleted() {
|
||||
client.refetchQueries({
|
||||
include: ['GetArticles', GetArticleByIdDocument],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { handleSubmit, control, setValue, register } =
|
||||
useForm<Pick<IArticleFormInput, 'blocks'>>();
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.article.__typename === 'Article') {
|
||||
const { blocks } = data.article;
|
||||
setValue('blocks', blocks as Block[]);
|
||||
}
|
||||
}, [data, setValue]);
|
||||
|
||||
const { fields, append, remove, swap } = useFieldArray({
|
||||
control,
|
||||
name: 'blocks',
|
||||
});
|
||||
|
||||
if (!data) return <div>not found</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form
|
||||
className="space-y-5"
|
||||
onSubmit={handleSubmit(formData => {
|
||||
if (data.article.__typename !== 'Article') return;
|
||||
const { id, __typename, ...article } = data.article;
|
||||
|
||||
editArticle({
|
||||
variables: {
|
||||
id: +articleId,
|
||||
input: {
|
||||
...article,
|
||||
blocks: JSON.stringify(formData.blocks),
|
||||
},
|
||||
},
|
||||
});
|
||||
})}
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
append({ type: 'Video', video: '', caption: '' });
|
||||
}}
|
||||
>
|
||||
Добавить видео
|
||||
</button>
|
||||
<button
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
append({ type: 'Slider', images: [] });
|
||||
}}
|
||||
>
|
||||
Добавить слайдер
|
||||
</button>
|
||||
<button
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
append({
|
||||
type: 'Quote',
|
||||
authorAvatar: '',
|
||||
authorName: '',
|
||||
authorRole: '',
|
||||
text: '',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Добавить цитату
|
||||
</button>
|
||||
<button
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
append({
|
||||
type: 'Content',
|
||||
content: '',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Добавить разметку
|
||||
</button>
|
||||
</div>
|
||||
<Reorder.Group
|
||||
axis="y"
|
||||
values={fields}
|
||||
onReorder={reorderFields(fields, swap)}
|
||||
className="space-y-5"
|
||||
>
|
||||
{fields.map((item, index) => {
|
||||
return item.type === 'Video' ? (
|
||||
<ArticleVideoInput
|
||||
key={item.id}
|
||||
{...{ register, setValue, remove, index, item }}
|
||||
/>
|
||||
) : item.type === 'Slider' ? (
|
||||
<ArticleSliderInput
|
||||
{...{ control, index, register, item, setValue }}
|
||||
key={item.id}
|
||||
/>
|
||||
) : item.type === 'Quote' ? (
|
||||
<ArticleQuoteInput
|
||||
{...{ register, index, item, setValue }}
|
||||
key={item.id}
|
||||
/>
|
||||
) : (
|
||||
<ArticleContentInput
|
||||
key={item.id}
|
||||
{...{ control, index, item }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Reorder.Group>
|
||||
<Button>Сохранить</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ArticleFormModal } from '@/components/modals/ArticleFormModal';
|
||||
import { DeleteArticleModal } from '@/components/modals/DeleteArticleModal';
|
||||
import { ArticleCard } from '@/components/pages/BlogPage/ArticleCard';
|
||||
import { useAddArticleMutation } from '@/queries/articles/addArticle';
|
||||
import { useEditArticleMutation } from '@/queries/articles/editArticle';
|
||||
import { useGetArticlesQuery } from '@/queries/articles/getArticles';
|
||||
import { useModalStore } from '@/stores/useModalStore';
|
||||
import { Block } from '@/types/IPost';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
export default function DashboardBlogPage() {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const client = useApolloClient();
|
||||
|
||||
const { data } = useGetArticlesQuery({
|
||||
variables: { tags: [] },
|
||||
});
|
||||
|
||||
const [addArticle] = useAddArticleMutation({
|
||||
onCompleted() {
|
||||
client.refetchQueries({ include: ['GetArticles'] });
|
||||
setModal(null, '');
|
||||
},
|
||||
});
|
||||
|
||||
const [editArticle] = useEditArticleMutation({
|
||||
onCompleted() {
|
||||
client.refetchQueries({ include: ['GetArticles'] });
|
||||
setModal(null, '');
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ArticleFormModal
|
||||
action="create"
|
||||
onSubmit={({ blocks, ...data }) => {
|
||||
addArticle({
|
||||
variables: {
|
||||
input: { blocks: JSON.stringify(blocks), ...data },
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>,
|
||||
'articleFormModal',
|
||||
)
|
||||
}
|
||||
>
|
||||
Создать статью
|
||||
</Button>
|
||||
<div>
|
||||
{data?.articles.__typename === 'Articles' &&
|
||||
data?.articles.articles.map(({ __typename, id, ...article }) => (
|
||||
<div className="relative" key={id}>
|
||||
<ArticleCard id={id} {...article} />
|
||||
<div className="absolute top-0 right-0 p-4 flex gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ArticleFormModal
|
||||
action="edit"
|
||||
onSubmit={({ blocks, ...data }) => {
|
||||
editArticle({
|
||||
variables: {
|
||||
id,
|
||||
input: {
|
||||
...data,
|
||||
blocks: JSON.stringify(blocks),
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
defaultValues={{
|
||||
...article,
|
||||
blocks: article.blocks.map(
|
||||
({ __typename, ...block }) => block,
|
||||
) as Block[],
|
||||
}}
|
||||
/>,
|
||||
'editProject',
|
||||
)
|
||||
}
|
||||
className="group relative p-2 bg-black bg-opacity-60 hover:bg-opacity-70 transition-opacity rounded-full"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
<span className="pointer-events-none group-hover:opacity-100 opacity-0 transition-opacity absolute -bottom-[90%] left-[50%] -translate-x-[50%] bg-neutral-900 px-2 py-1 text-sm rounded-lg">
|
||||
Редактировать
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModal(<DeleteArticleModal id={id} />, '')}
|
||||
className="group relative p-2 bg-black bg-opacity-60 hover:bg-opacity-70 transition-opacity rounded-full"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
<span className="pointer-events-none group-hover:opacity-100 opacity-0 transition-opacity absolute -bottom-[90%] left-[50%] -translate-x-[50%] bg-neutral-900 px-2 py-1 text-sm rounded-lg">
|
||||
Удалить
|
||||
</span>
|
||||
</button>
|
||||
</div>{' '}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useCheckAuthQuery } from '@/queries/auth/checkAuth';
|
||||
import { useLogoutLazyQuery } from '@/queries/auth/logout';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
@@ -37,16 +36,12 @@ export default function DashboardLayout({
|
||||
|
||||
return (
|
||||
<section className="space-y-5 p-4">
|
||||
<div className="flex gap-4 items-center">
|
||||
<Link href={'/dashboard/projects'}>Проекты</Link>
|
||||
<Link href={'/dashboard/blog'}>Блог</Link>
|
||||
<button
|
||||
className="bg-[red] p-2 rounded-lg font-bold"
|
||||
onClick={() => logout()}
|
||||
>
|
||||
Выйти
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="bg-[red] p-2 rounded-lg font-bold"
|
||||
onClick={() => logout()}
|
||||
>
|
||||
Выйти
|
||||
</button>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function ProjectsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <section>{children}</section>;
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { DeleteProjectModal } from '@/components/modals/DeleteProjectModal';
|
||||
import {
|
||||
IAddProjectFormInput,
|
||||
ProjectFormModal,
|
||||
} from '@/components/modals/ProjectFormModal';
|
||||
import { ProjectCard } from '@/components/pages/ProjectsPage/ProjectCard';
|
||||
import { useAddProjectMutation } from '@/queries/projects/addProject';
|
||||
import { useEditProjectMutation } from '@/queries/projects/editProject';
|
||||
import { useGetProjectsQuery } from '@/queries/projects/getProjects';
|
||||
import { useModalStore } from '@/stores/useModalStore';
|
||||
import { Device, IProject } from '@/types/IProject';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
export default function DashboardProjectsPage() {
|
||||
const { data } = useGetProjectsQuery({ variables: { devices: [] } });
|
||||
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const client = useApolloClient();
|
||||
|
||||
const [addProject] = useAddProjectMutation({
|
||||
onCompleted() {
|
||||
client.refetchQueries({ include: ['GetProjects'] });
|
||||
setModal(null, '');
|
||||
},
|
||||
});
|
||||
|
||||
const [editProject] = useEditProjectMutation({
|
||||
onCompleted() {
|
||||
setModal(null, '');
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<Button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ProjectFormModal
|
||||
action={'create'}
|
||||
onSubmit={(data: IAddProjectFormInput) => {
|
||||
addProject({ variables: { input: data } });
|
||||
}}
|
||||
/>,
|
||||
'addProject',
|
||||
)
|
||||
}
|
||||
>
|
||||
Добавить проект
|
||||
</Button>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{data?.projects.__typename === 'Projects' &&
|
||||
data?.projects.projects.map(({ __typename, id, ...project }) => (
|
||||
<div key={id} className="relative">
|
||||
<ProjectCard {...(project as IProject)} />
|
||||
<div className="absolute top-0 right-0 p-4 flex gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ProjectFormModal
|
||||
action="edit"
|
||||
onSubmit={(data: IAddProjectFormInput) => {
|
||||
editProject({
|
||||
variables: { id, input: data },
|
||||
});
|
||||
}}
|
||||
defaultValues={{
|
||||
...project,
|
||||
devices: project.devices as Device[],
|
||||
}}
|
||||
/>,
|
||||
'editProject',
|
||||
)
|
||||
}
|
||||
className="group relative p-2 bg-black bg-opacity-60 hover:bg-opacity-70 transition-opacity rounded-full"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
<span className="pointer-events-none group-hover:opacity-100 opacity-0 transition-opacity absolute -bottom-[90%] left-[50%] -translate-x-[50%] bg-neutral-900 px-2 py-1 text-sm rounded-lg">
|
||||
Редактировать
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModal(<DeleteProjectModal id={id} />, '')}
|
||||
className="group relative p-2 bg-black bg-opacity-60 hover:bg-opacity-70 transition-opacity rounded-full"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
<span className="pointer-events-none group-hover:opacity-100 opacity-0 transition-opacity absolute -bottom-[90%] left-[50%] -translate-x-[50%] bg-neutral-900 px-2 py-1 text-sm rounded-lg">
|
||||
Удалить
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { ArticleContent } from '@/components/pages/BlogPage/ArticleContent';
|
||||
import { useGetArticleByIdQuery } from '@/queries/articles/getArticleById';
|
||||
import { IArticle } from '@/types/IPost';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function ArticlePage({
|
||||
params: { articleId },
|
||||
}: {
|
||||
params: { articleId: string };
|
||||
}) {
|
||||
const [article, setArticle] = useState<IArticle>();
|
||||
|
||||
useGetArticleByIdQuery({
|
||||
variables: { id: +articleId },
|
||||
onCompleted: ({ article }) => {
|
||||
if (article.__typename === 'Article') {
|
||||
setArticle(article as IArticle);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return <div>{article && <ArticleContent {...article} />}</div>;
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import { Posts } from '@/consts/Posts';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
export default function PostPage({
|
||||
params: { postId },
|
||||
}: {
|
||||
params: { postId: string };
|
||||
}) {
|
||||
const post = Posts.find(post => post.id === +postId);
|
||||
const [date = '', month = '', year = ''] = post!.createdAt.split(' ');
|
||||
|
||||
const DynamicPostSlider = dynamic(
|
||||
() =>
|
||||
import('@/components/pages/BlogPage/PostSlider').then(
|
||||
mod => mod.PostSlider,
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
post && (
|
||||
<>
|
||||
{/* <div className="sm:pb-7 pb-4 flex max-lg:flex-col justify-between sm:gap-4 gap-y-8 border-b border-[#3D425C]">
|
||||
<div className="min-w-[calc(25vw-56px)] flex lg:flex-col justify-between max-sm:flex-col-reverse gap-y-2">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{post.tags.map(tag => (
|
||||
<PostTag key={tag} text={tag} />
|
||||
))}
|
||||
</div>
|
||||
<PostDate
|
||||
date={date}
|
||||
month={month}
|
||||
year={year}
|
||||
inPostCard={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col relative">
|
||||
<h2 className="h2 font-medium lg:mb-5 mb-8">{post.title}</h2>
|
||||
{post.mainImage && (
|
||||
<Image
|
||||
src={post.mainImage}
|
||||
alt={post.title}
|
||||
fill
|
||||
className="object-cover !static min-h-[400px] self-end max-lg:rounded-2xl"
|
||||
sizes="(min-height: 400px)"
|
||||
priority
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:max-w-[624px] lg:mt-[70px] mt-14 lg:ml-[25vw] lg:mb-[60px] sm:mx-6 mx-4 sm:mt-14 sm:mb-12 mb-8">
|
||||
<h3 className="accent font-medium lg:mb-8 mb-6">{post.title}</h3>
|
||||
<p className="mb-3 m-text">{post.description}</p>
|
||||
<p className="m-text">{post.extraDesc}</p>
|
||||
{post.video && (
|
||||
<div
|
||||
className="flex items-center justify-center bg-no-repeat bg-cover sm:min-h-[375px] min-h-[220px] lg:mt-[60px] sm:mt-12 mt-8 mb-5 max-lg:rounded-2xl"
|
||||
style={{ backgroundImage: `url(${post.video})` }}
|
||||
>
|
||||
<button className="absolute mx-auto">
|
||||
<PlayIcon />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<p className="mb-3 m-text">{post.description}</p>
|
||||
<p className="m-text">{post.extraDesc}</p>
|
||||
</div>
|
||||
<DynamicPostSlider {...post} />
|
||||
{post.review && (
|
||||
<div className="flex gap-4 max-lg:flex-col lg:mx-10 lg:py-14 sm:mx-6 py-10 mx-4 border-y border-[#3D425C]">
|
||||
<div className="flex gap-x-6 lg:max-h-16 lg:min-w-[calc(23vw)] max-w-[311px] relative">
|
||||
<Image
|
||||
src={post.review.author.avatar}
|
||||
alt={post.review.author.name}
|
||||
fill
|
||||
className="rounded-3xl object-cover !static lg:max-w-16 lg:max-h-16 max-w-[72px] max-h-[72px]"
|
||||
sizes="100%, 100%"
|
||||
/>
|
||||
<div className="flex flex-col justify-between gap-1">
|
||||
<h3 className="h3 font-semibold">{post.review.author.name}</h3>
|
||||
<p className="m-caption font-medium opacity-80">
|
||||
{post.review.author.position}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="accent lg:max-w-[55vw]">{post.review.text}</p>
|
||||
</div>
|
||||
)}
|
||||
{post.extraVideo && (
|
||||
<div className="border-y border-[#3D425C] lg:py-10 py-6 lg:mx-10 sm:mx-6 mx-4">
|
||||
<div
|
||||
className="flex justify-center items-center bg-no-repeat bg-cover aspect-[1520/787] max-lg:rounded-2xl"
|
||||
style={{ backgroundImage: `url(${post.extraVideo})` }}
|
||||
>
|
||||
<button className="absolute mx-auto">
|
||||
<PlayIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-between mt-4">
|
||||
<p className="md:text-[clamp(14px,14px+(100vw-768px)/832*2,16px)]">
|
||||
Пояснялка
|
||||
</p>
|
||||
<Link
|
||||
href={'/'}
|
||||
className="flex gap-2 md:text-[clamp(14px,14px+(100vw-768px)/832*2,16px)]"
|
||||
>
|
||||
YouTube <ArrowMoreIcon />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)} */}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PostsFilters } from '@/components/pages/BlogPage/PostsFilters';
|
||||
import { PostsList } from '@/components/pages/BlogPage/PostsList';
|
||||
import { ArticlesFilters } from '@/components/pages/BlogPage/ArticlesFilters';
|
||||
import { ArticlesList } from '@/components/pages/BlogPage/ArticlesList';
|
||||
import { Title } from '@/ui/Title';
|
||||
|
||||
export default function BlogPage() {
|
||||
@@ -7,9 +7,9 @@ export default function BlogPage() {
|
||||
<div>
|
||||
<div className="sm:pb-5 pb-4 flex flex-col justify-between border-b border-[#3D425C]">
|
||||
<Title className="lg:mb-14 sm:mb-8 mb-4">Блог</Title>
|
||||
<PostsFilters />
|
||||
<ArticlesFilters />
|
||||
</div>
|
||||
<PostsList />
|
||||
<ArticlesList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
IAddProjectFormInput,
|
||||
ProjectFormModal,
|
||||
} from '@/components/modals/ProjectFormModal';
|
||||
import { ProjectsFilters } from '@/components/pages/ProjectsPage/ProjectsFilters';
|
||||
import { ProjectsList } from '@/components/pages/ProjectsPage/ProjectsList';
|
||||
import { useCheckAuthQuery } from '@/queries/auth/checkAuth';
|
||||
import { useAddProjectMutation } from '@/queries/projects/addProject';
|
||||
import { useModalStore } from '@/stores/useModalStore';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Title } from '@/ui/Title';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const client = useApolloClient();
|
||||
|
||||
const { data } = useCheckAuthQuery();
|
||||
|
||||
const [addProject] = useAddProjectMutation({
|
||||
onCompleted() {
|
||||
client.refetchQueries({ include: ['GetProjects'] });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="lg:pt-20 pt-14">
|
||||
<div className="sm:pb-5 pb-4 flex flex-col justify-between border-b border-[#3D425C]">
|
||||
<Title className="lg:mb-14 sm:mb-8 mb-4">Проекты</Title>
|
||||
<Title className="lg:mb-14 sm:mb-8 mb-4">Проекты</Title>
|
||||
<div className="sm:pb-5 pb-4 flex gap-4 flex-wrap justify-between border-b border-[#3D425C]">
|
||||
<ProjectsFilters />
|
||||
{data?.checkAuth.__typename === 'CheckAuthResponse' &&
|
||||
data.checkAuth.isAuth && (
|
||||
<Button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ProjectFormModal
|
||||
action={'create'}
|
||||
onSubmit={(data: IAddProjectFormInput) => {
|
||||
addProject({ variables: { input: data } });
|
||||
setModal(null, '');
|
||||
}}
|
||||
/>,
|
||||
'addProject',
|
||||
)
|
||||
}
|
||||
>
|
||||
Добавить проект
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<ProjectsList />
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useModalStore } from '@/stores/useModalStore';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function ModalContainer() {
|
||||
const { modal, name } = useModalStore();
|
||||
const { modal, name, setModal } = useModalStore();
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setModal(null, '');
|
||||
};
|
||||
document.addEventListener('keydown', listener);
|
||||
|
||||
return () => document.removeEventListener('keydown', listener);
|
||||
}, [setModal]);
|
||||
|
||||
return (
|
||||
modal && (
|
||||
|
||||
@@ -77,20 +77,17 @@ export function MediaUploader<
|
||||
{isImage(item)
|
||||
? (previewFile || item.img) && (
|
||||
<Image
|
||||
src={
|
||||
previewFile ||
|
||||
process.env.NEXT_PUBLIC_S3_BUCKET + '/' + item.img
|
||||
}
|
||||
src={previewFile || process.env.NEXT_PUBLIC_S3_BUCKET + item.img}
|
||||
width={300}
|
||||
height={300}
|
||||
alt={''}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
)
|
||||
: (previewFile || item.video) && (
|
||||
<video
|
||||
src={
|
||||
previewFile ||
|
||||
process.env.NEXT_PUBLIC_S3_BUCKET + '/' + item.video
|
||||
previewFile || process.env.NEXT_PUBLIC_S3_BUCKET + item.video
|
||||
}
|
||||
width={300}
|
||||
height={300}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { YearFilterItem } from './YearFilterItem';
|
||||
import { ArrowDownIcon } from './icons/ArrowDownIcon';
|
||||
|
||||
export function YearFilterDropdown({ years }: { years: string[] }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const params = new URLSearchParams(useSearchParams());
|
||||
|
||||
return (
|
||||
<div className="sm:hidden w-full relative">
|
||||
<div
|
||||
className="flex justify-between border border-[#3D425C] px-6 py-4"
|
||||
onClick={() => setIsOpen(prev => !prev)}
|
||||
>
|
||||
{!params.has('year') ? 'Все время' : params.get('year')}
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div
|
||||
className="absolute z-10 flex flex-col w-full"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<YearFilterItem
|
||||
text={'Все время'}
|
||||
isAll
|
||||
inSelect
|
||||
chosen={!params.has('year')}
|
||||
/>
|
||||
{years.map(year => (
|
||||
<YearFilterItem
|
||||
key={year}
|
||||
text={year}
|
||||
inSelect
|
||||
chosen={params.get('year') === year}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
export function YearFilterItem({
|
||||
text,
|
||||
chosen = false,
|
||||
isAll = false,
|
||||
inSelect = false,
|
||||
}: {
|
||||
text: string;
|
||||
chosen?: boolean;
|
||||
isAll?: boolean;
|
||||
inSelect?: boolean;
|
||||
}) {
|
||||
const { push } = useRouter();
|
||||
const pathname = usePathname();
|
||||
const params = new URLSearchParams(useSearchParams());
|
||||
|
||||
function clickHandler() {
|
||||
if (isAll || (params.get('year') === text && !inSelect))
|
||||
params.delete('year');
|
||||
else params.set('year', text);
|
||||
push(pathname + '?' + params.toString());
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={
|
||||
'max-sm:w-full max-sm:border-b max-sm:border-x max-sm:border-[#3D425C] max-sm:px-6 max-sm:py-4 max-sm:bg-[#14161f] btn-text font-semibold ' +
|
||||
(chosen ? 'text-white' : 'text-[#737AA1]')
|
||||
}
|
||||
onClick={clickHandler}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +1,104 @@
|
||||
import { useModalStore } from '@/stores/useModalStore';
|
||||
import { IContent } from '@/types/IPost';
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
import { Reorder } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import { Editor as TinyMCEEditor } from 'tinymce';
|
||||
import parse from 'html-react-parser';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Control,
|
||||
UseFieldArrayRemove,
|
||||
UseFormGetValues,
|
||||
UseFormSetValue,
|
||||
UseFormWatch,
|
||||
} from 'react-hook-form';
|
||||
import { ArticleContentEditorModal } from '../modals/ArticleContentEditorModal';
|
||||
import { IArticleFormInput } from '../modals/ArticleFormModal';
|
||||
|
||||
interface IArticleContentInputProps {
|
||||
export interface IArticleContentInputProps {
|
||||
item: IContent & Record<'id', string>;
|
||||
index: number;
|
||||
control: Control<Pick<IArticleFormInput, 'blocks'>, any>;
|
||||
setValue: UseFormSetValue<Pick<IArticleFormInput, 'blocks'>>;
|
||||
getValues: UseFormGetValues<Pick<IArticleFormInput, 'blocks'>>;
|
||||
watch: UseFormWatch<Pick<IArticleFormInput, 'blocks'>>;
|
||||
removeSlider: UseFieldArrayRemove;
|
||||
}
|
||||
|
||||
export function ArticleContentInput({
|
||||
control,
|
||||
index,
|
||||
item,
|
||||
setValue,
|
||||
getValues,
|
||||
watch,
|
||||
removeSlider,
|
||||
}: IArticleContentInputProps) {
|
||||
const editorRef = useRef<TinyMCEEditor | null>(null);
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const [content, setContent] = useState(item.content);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = watch(value => {
|
||||
if (value.blocks?.[index]?.type === 'Content')
|
||||
setContent(value.blocks[index].content!);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [index, watch]);
|
||||
|
||||
return (
|
||||
<Reorder.Item value={item}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`blocks.${index}.content`}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Editor
|
||||
id={item.id}
|
||||
inline
|
||||
onEditorChange={onChange}
|
||||
value={value}
|
||||
onInit={(_, editor) => {
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
init={{
|
||||
draggable_modal: true,
|
||||
plugins: [
|
||||
'anchor',
|
||||
'autolink',
|
||||
'charmap',
|
||||
'codesample',
|
||||
'emoticons',
|
||||
'image',
|
||||
'link',
|
||||
'lists',
|
||||
'media',
|
||||
'searchreplace',
|
||||
'table',
|
||||
'visualblocks',
|
||||
'wordcount',
|
||||
'checklist',
|
||||
'mediaembed',
|
||||
'casechange',
|
||||
'export',
|
||||
'formatpainter',
|
||||
'pageembed',
|
||||
'a11ychecker',
|
||||
'tinymcespellchecker',
|
||||
'permanentpen',
|
||||
'powerpaste',
|
||||
'advtable',
|
||||
'advcode',
|
||||
'editimage',
|
||||
'advtemplate',
|
||||
'mentions',
|
||||
'tinycomments',
|
||||
'tableofcontents',
|
||||
'footnotes',
|
||||
'mergetags',
|
||||
'autocorrect',
|
||||
'typography',
|
||||
'inlinecss',
|
||||
'markdown',
|
||||
],
|
||||
toolbar:
|
||||
'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | link image media table mergetags | addcomment showcomments | spellcheckdialog a11ycheck typography | align lineheight | checklist numlist bullist indent outdent | emoticons charmap | removeformat',
|
||||
tinycomments_mode: 'embedded',
|
||||
tinycomments_author: 'Author name',
|
||||
mergetags_list: [
|
||||
{ value: 'First.Name', title: 'First Name' },
|
||||
{ value: 'Email', title: 'Email' },
|
||||
],
|
||||
}}
|
||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div>{parse(content)}</div>
|
||||
<div className="flex gap-x-4">
|
||||
<button
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setModal(
|
||||
<ArticleContentEditorModal
|
||||
item={item}
|
||||
index={index}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
getValues={getValues}
|
||||
watch={watch}
|
||||
/>,
|
||||
'content',
|
||||
);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onClick={() => removeSlider(index)}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</Reorder.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { IQuote } from '@/types/IPost';
|
||||
import { Reorder } from 'framer-motion';
|
||||
import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
|
||||
import { MediaUploader } from '../MediaUploader';
|
||||
import { IArticleFormInput } from '../modals/ArticleFormModal';
|
||||
|
||||
interface IArticleQuoteInputProps {
|
||||
register: UseFormRegister<Pick<IArticleFormInput, 'blocks'>>;
|
||||
item: IQuote & Record<'id', string>;
|
||||
index: number;
|
||||
setValue: UseFormSetValue<Pick<IArticleFormInput, 'blocks'>>;
|
||||
}
|
||||
|
||||
export function ArticleQuoteInput({
|
||||
index,
|
||||
item,
|
||||
register,
|
||||
setValue,
|
||||
}: IArticleQuoteInputProps) {
|
||||
return (
|
||||
<Reorder.Item value={item}>
|
||||
<div className="flex flex-col w-fit">
|
||||
<label htmlFor={`authorName-${item.id}`}>Имя автора</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`authorName-${item.id}`}
|
||||
className="text-black"
|
||||
{...register(`blocks.${index}.authorName`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor={`authorRole-${item.id}`}>Должность автора</label>
|
||||
<input
|
||||
type="text"
|
||||
className="text-black"
|
||||
id={`authorRole-${item.id}`}
|
||||
{...register(`blocks.${index}.authorRole`)}
|
||||
/>
|
||||
</div>
|
||||
<MediaUploader
|
||||
dest="blog"
|
||||
fieldName={`blocks.${index}.authorAvatar`}
|
||||
setValue={setValue}
|
||||
item={{ id: item.id, img: item.authorAvatar }}
|
||||
media="img"
|
||||
label="Выберите аватар"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor={`quote-text-${item.id}`}>Текст</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`quote-text-${item.id}`}
|
||||
className="text-black"
|
||||
{...register(`blocks.${index}.text`)}
|
||||
/>
|
||||
</div>
|
||||
</Reorder.Item>
|
||||
);
|
||||
}
|
||||
@@ -37,17 +37,14 @@ export function ArticleSliderImageInput({
|
||||
label="Выберите изображение"
|
||||
media="img"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor={`slider-image-${imgIndex}`}>Подпись</label>
|
||||
<input
|
||||
type="text"
|
||||
className="text-black outline-none pl-1"
|
||||
id={`slider-image-${imgIndex}`}
|
||||
{...register(`blocks.${blockIndex}.images.${imgIndex}.caption`)}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="text-black outline-none pl-1 w-full"
|
||||
{...register(`blocks.${blockIndex}.images.${imgIndex}.caption`)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="self-start z-[1]"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
remove(imgIndex);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Reorder } from 'framer-motion';
|
||||
import {
|
||||
Control,
|
||||
useFieldArray,
|
||||
UseFieldArrayRemove,
|
||||
UseFormRegister,
|
||||
UseFormSetValue,
|
||||
} from 'react-hook-form';
|
||||
@@ -16,6 +17,7 @@ interface IArticleSliderInputProps {
|
||||
item: ISlider & Record<'id', string>;
|
||||
index: number;
|
||||
control: Control<Pick<IArticleFormInput, 'blocks'>>;
|
||||
removeSlider: UseFieldArrayRemove;
|
||||
}
|
||||
|
||||
export function ArticleSliderInput({
|
||||
@@ -24,6 +26,7 @@ export function ArticleSliderInput({
|
||||
register,
|
||||
setValue,
|
||||
control,
|
||||
removeSlider,
|
||||
}: IArticleSliderInputProps) {
|
||||
const { swap, append, remove, fields } = useFieldArray({
|
||||
control,
|
||||
@@ -32,14 +35,17 @@ export function ArticleSliderInput({
|
||||
|
||||
return (
|
||||
<Reorder.Item value={item}>
|
||||
<button
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
append({ img: '', caption: '' });
|
||||
}}
|
||||
>
|
||||
Добавить слайд
|
||||
</button>
|
||||
<div className="flex gap-x-4">
|
||||
<button
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
append({ img: '', caption: '' });
|
||||
}}
|
||||
>
|
||||
Добавить слайд
|
||||
</button>
|
||||
<button onClick={() => removeSlider(index)}>Удалить слайдер</button>
|
||||
</div>
|
||||
<Reorder.Group
|
||||
axis="x"
|
||||
values={fields}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { IVideo } from '@/types/IPost';
|
||||
import { Reorder } from 'framer-motion';
|
||||
import {
|
||||
UseFieldArrayRemove,
|
||||
UseFormRegister,
|
||||
UseFormSetValue,
|
||||
} from 'react-hook-form';
|
||||
import { CloseIcon } from '../icons/CloseIcon';
|
||||
import { MediaUploader } from '../MediaUploader';
|
||||
import { IArticleFormInput } from '../modals/ArticleFormModal';
|
||||
|
||||
interface IArticleVideoInputProps {
|
||||
item: IVideo & Record<'id', string>;
|
||||
index: number;
|
||||
remove: UseFieldArrayRemove;
|
||||
register: UseFormRegister<Pick<IArticleFormInput, 'blocks'>>;
|
||||
setValue: UseFormSetValue<Pick<IArticleFormInput, 'blocks'>>;
|
||||
}
|
||||
|
||||
export function ArticleVideoInput({
|
||||
register,
|
||||
setValue,
|
||||
remove,
|
||||
index,
|
||||
item,
|
||||
}: IArticleVideoInputProps) {
|
||||
return (
|
||||
<Reorder.Item value={item} className="w-fit">
|
||||
<MediaUploader
|
||||
dest="blog"
|
||||
setValue={setValue}
|
||||
fieldName={`blocks.${index}.video`}
|
||||
media="video"
|
||||
item={item}
|
||||
label="Выберите видео"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Описание видео (необязательно)"
|
||||
className="text-black"
|
||||
{...register(`blocks.${index}.caption`, {
|
||||
required: false,
|
||||
})}
|
||||
/>
|
||||
<button
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
remove(index);
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</Reorder.Item>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export function DeleteIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export function EditContentIcon() {
|
||||
return (
|
||||
<svg
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M17 8H7V6H15L17 8Z" fill="white" />
|
||||
<path d="M7 11H17V13H7V11Z" fill="white" />
|
||||
<path d="M7 16H17V18H9L7 16Z" fill="white" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3 22V2H19L21 4V22H3ZM19 4H18H5V20H19V5V4Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M19 4H18L19 5V4Z" fill="white" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export function EditIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { api } from '@/api';
|
||||
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
|
||||
import { useModalStore } from '@/stores/useModalStore';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Editor } from '@tinymce/tinymce-react';
|
||||
import { useRef } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { Editor as Tinymce } from 'tinymce';
|
||||
import { IArticleContentInputProps } from '../articleInputs/ArticleContentInput';
|
||||
import { CloseIcon } from '../icons/CloseIcon';
|
||||
|
||||
export function ArticleContentEditorModal({
|
||||
control,
|
||||
index,
|
||||
item,
|
||||
setValue,
|
||||
getValues,
|
||||
}: Omit<IArticleContentInputProps, 'removeSlider'>) {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const editorRef = useRef<Tinymce | null>(null);
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`blocks.${index}.content`}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Editor
|
||||
id={item.id}
|
||||
onEditorChange={onChange}
|
||||
value={value}
|
||||
onInit={(_, editor) => (editorRef.current = editor)}
|
||||
init={{
|
||||
images_upload_credentials: true,
|
||||
images_upload_handler: async blobInfo => {
|
||||
const formData = new FormData();
|
||||
formData.append('files', blobInfo.blob(), blobInfo.filename());
|
||||
formData.append('dest', 'blog');
|
||||
|
||||
const res = await api
|
||||
.post('upload', { body: formData })
|
||||
.json<{ files: string[] }>();
|
||||
|
||||
console.log(res);
|
||||
return (
|
||||
'https://storage.yandexcloud.net/dult-faib-knac-fint/' +
|
||||
res.files[0]
|
||||
);
|
||||
},
|
||||
automatic_uploads: true,
|
||||
plugins: [
|
||||
'anchor',
|
||||
'autolink',
|
||||
'charmap',
|
||||
'codesample',
|
||||
'emoticons',
|
||||
'image',
|
||||
'link',
|
||||
'lists',
|
||||
'media',
|
||||
'searchreplace',
|
||||
'table',
|
||||
'visualblocks',
|
||||
'wordcount',
|
||||
'checklist',
|
||||
'mediaembed',
|
||||
'casechange',
|
||||
'export',
|
||||
'formatpainter',
|
||||
'pageembed',
|
||||
'a11ychecker',
|
||||
'tinymcespellchecker',
|
||||
'permanentpen',
|
||||
'powerpaste',
|
||||
'advtable',
|
||||
'advcode',
|
||||
'editimage',
|
||||
'advtemplate',
|
||||
'mentions',
|
||||
'tinycomments',
|
||||
'tableofcontents',
|
||||
'footnotes',
|
||||
'mergetags',
|
||||
'autocorrect',
|
||||
'typography',
|
||||
'inlinecss',
|
||||
'markdown',
|
||||
],
|
||||
toolbar:
|
||||
'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | link image media table mergetags | addcomment showcomments | spellcheckdialog a11ycheck typography | align lineheight | checklist numlist bullist indent outdent | emoticons charmap | removeformat',
|
||||
tinycomments_mode: 'embedded',
|
||||
tinycomments_author: 'Author name',
|
||||
mergetags_list: [
|
||||
{ value: 'First.Name', title: 'First Name' },
|
||||
{ value: 'Email', title: 'Email' },
|
||||
],
|
||||
}}
|
||||
apiKey={process.env.NEXT_PUBLIC_TINYMCE_API_KEY}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
className="absolute -bottom-4 left-4 z-[2]"
|
||||
onClick={() => {
|
||||
setValue(
|
||||
`blocks.${index}.content`,
|
||||
editorRef.current?.getContent() || '',
|
||||
);
|
||||
console.log(getValues(`blocks.${index}.content`));
|
||||
setModal(null, '');
|
||||
}}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
<button
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setModal(null, '');
|
||||
}}
|
||||
className="absolute top-4 right-4 z-[2]"
|
||||
>
|
||||
<ClassNameWrapper element={<CloseIcon />} className="text-black" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export function ArticleFormModal({
|
||||
<div className="text-black bg-white p-4 rounded-lg relative top-[100px] space-y-4">
|
||||
<div className="flex justify-between items-center border-b border-[#ccc] pb-4 gap-4">
|
||||
<p className="text-xl">
|
||||
{action === 'create' ? 'Создание проекта' : 'Изменение проекта'}
|
||||
{action === 'create' ? 'Создание статьи' : 'Изменение статьи'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setModal(null, '')}
|
||||
@@ -137,9 +137,7 @@ export function ArticleFormModal({
|
||||
className="relative"
|
||||
src={
|
||||
previewCardImage ??
|
||||
process.env.NEXT_PUBLIC_S3_BUCKET! +
|
||||
'/' +
|
||||
defaultValues?.cardImage
|
||||
process.env.NEXT_PUBLIC_S3_BUCKET! + defaultValues?.cardImage
|
||||
}
|
||||
alt={defaultValues?.title!}
|
||||
/>
|
||||
@@ -164,9 +162,7 @@ export function ArticleFormModal({
|
||||
className="relative"
|
||||
src={
|
||||
previewPosterImage ??
|
||||
process.env.NEXT_PUBLIC_S3_BUCKET! +
|
||||
'/' +
|
||||
defaultValues?.posterImage
|
||||
process.env.NEXT_PUBLIC_S3_BUCKET! + defaultValues?.posterImage
|
||||
}
|
||||
alt={defaultValues?.title!}
|
||||
/>
|
||||
|
||||
@@ -32,17 +32,18 @@ export function ProjectFormModal({
|
||||
}: IProjectFormModalProps) {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const { register, handleSubmit, setValue } = useForm<IAddProjectFormInput>({
|
||||
defaultValues:
|
||||
action === 'create'
|
||||
? { devices: [], stage: 1 }
|
||||
: {
|
||||
...defaultValues,
|
||||
releaseDate: new Date(defaultValues?.releaseDate!)
|
||||
.toISOString()
|
||||
.split('T')[0],
|
||||
},
|
||||
});
|
||||
const { register, handleSubmit, setValue, control } =
|
||||
useForm<IAddProjectFormInput>({
|
||||
defaultValues:
|
||||
action === 'create'
|
||||
? { devices: [], stage: 1 }
|
||||
: {
|
||||
...defaultValues,
|
||||
releaseDate: new Date(defaultValues?.releaseDate!)
|
||||
.toISOString()
|
||||
.split('T')[0],
|
||||
},
|
||||
});
|
||||
|
||||
const [file, setFile] = useState<File>();
|
||||
const [previewFile, setPreviewFile] = useState<string>();
|
||||
@@ -145,9 +146,7 @@ export function ProjectFormModal({
|
||||
className="relative"
|
||||
src={
|
||||
previewFile ??
|
||||
process.env.NEXT_PUBLIC_S3_BUCKET! +
|
||||
'/' +
|
||||
defaultValues?.image
|
||||
process.env.NEXT_PUBLIC_S3_BUCKET! + defaultValues?.image
|
||||
}
|
||||
alt={defaultValues?.name!}
|
||||
/>
|
||||
@@ -209,7 +208,7 @@ export function ProjectFormModal({
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button className="text-white">
|
||||
<Button className="text-white" type="submit">
|
||||
{action === 'create' ? 'Добавить проект' : 'Сохранить изменения'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { ArrowMoreIcon } from '@/components/icons/ArrowMoreIcon';
|
||||
import { DeleteIcon } from '@/components/icons/DeleteIcon';
|
||||
import { EditContentIcon } from '@/components/icons/EditContentIcon';
|
||||
import { EditIcon } from '@/components/icons/EditIcon';
|
||||
import {
|
||||
ArticleFormModal,
|
||||
IArticleFormInput,
|
||||
} from '@/components/modals/ArticleFormModal';
|
||||
import { DeleteArticleModal } from '@/components/modals/DeleteArticleModal';
|
||||
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
|
||||
import { useEditArticleMutation } from '@/queries/articles/editArticle';
|
||||
import { useCheckAuthQuery } from '@/queries/auth/checkAuth';
|
||||
import { useModalStore } from '@/stores/useModalStore';
|
||||
import type { IArticle } from '@/types/IPost';
|
||||
import { PostDate } from '@/ui/PostDate';
|
||||
import { PostTag } from '@/ui/PostTag';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
@@ -17,75 +28,140 @@ export function ArticleCard({
|
||||
cardImage,
|
||||
tags,
|
||||
id,
|
||||
}: Omit<IArticle, 'blocks' | 'posterImage'>) {
|
||||
const [date, month, year] = createdAt.split('-');
|
||||
blocks,
|
||||
posterImage,
|
||||
}: IArticle) {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const [year, month, date] = createdAt.split('-');
|
||||
|
||||
const params = useSearchParams();
|
||||
|
||||
const client = useApolloClient();
|
||||
|
||||
const { data } = useCheckAuthQuery();
|
||||
|
||||
const [editArticle] = useEditArticleMutation({
|
||||
onCompleted() {
|
||||
client.refetchQueries({ include: ['GetArticles'] });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={
|
||||
`${data?.checkAuth.__typename === 'CheckAuthResponse' && data?.checkAuth.isAuth ? '/dashboard' : ''}/blog/` +
|
||||
id
|
||||
}
|
||||
className="border-[#3D425C] [&:not(:last-of-type)]:border-b py-6 flex items-center gap-x-4 gap-y-3 max-sm:flex-col w-full relative"
|
||||
>
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_S3_BUCKET}/${cardImage}`}
|
||||
alt={title}
|
||||
fill
|
||||
className="sm:hidden !static object-cover"
|
||||
sizes="100%"
|
||||
priority
|
||||
/>
|
||||
<PostDate
|
||||
className="max-lg:hidden"
|
||||
date={date}
|
||||
month={month}
|
||||
year={year}
|
||||
/>
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_S3_BUCKET}/${cardImage}`}
|
||||
alt={title}
|
||||
fill
|
||||
className="max-lg:hidden !static max-w-[31vw] object-cover"
|
||||
sizes="(max-width: 31vw) 100%"
|
||||
priority
|
||||
/>
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_S3_BUCKET}/${cardImage}`}
|
||||
alt={title}
|
||||
fill
|
||||
className="hidden sm:max-lg:block !static max-w-[34.4vw] min-h-60 object-cover"
|
||||
sizes="(max-width: 34.4vw) 100%, (min-height: 240px) 100%"
|
||||
priority
|
||||
/>
|
||||
<div className="flex justify-between flex-col flex-1 sm-max-lg:flex-1 self-stretch max-sm:gap-y-3">
|
||||
<div>
|
||||
<div className="flex gap-2 mb-4">
|
||||
{tags.map(tag => (
|
||||
<PostTag
|
||||
text={tag}
|
||||
key={tag}
|
||||
active={params.getAll('tags').includes(tag)}
|
||||
/>
|
||||
))}
|
||||
<div className="border-[#3D425C] border-b w-full relative">
|
||||
<Link
|
||||
href={'/blog/' + id}
|
||||
className="py-6 grid lg:grid-cols-4 sm:grid-cols-8 gap-x-4 gap-y-3 max-sm:flex-col"
|
||||
>
|
||||
<Image
|
||||
src={process.env.NEXT_PUBLIC_S3_BUCKET + cardImage}
|
||||
alt={title}
|
||||
fill
|
||||
className="sm:hidden !static object-cover"
|
||||
sizes="100%"
|
||||
priority
|
||||
/>
|
||||
<PostDate
|
||||
className="max-lg:hidden self-end"
|
||||
date={date}
|
||||
month={month}
|
||||
year={year}
|
||||
/>
|
||||
<Image
|
||||
src={process.env.NEXT_PUBLIC_S3_BUCKET + cardImage}
|
||||
alt={title}
|
||||
fill
|
||||
className="max-lg:hidden !relative object-cover"
|
||||
priority
|
||||
/>
|
||||
<Image
|
||||
src={process.env.NEXT_PUBLIC_S3_BUCKET + cardImage}
|
||||
alt={title}
|
||||
fill
|
||||
className="hidden sm:max-lg:block !relative object-cover sm:max-lg:col-span-3"
|
||||
priority
|
||||
/>
|
||||
<div className="flex justify-between flex-col flex-1 sm-max-lg:flex-1 self-stretch max-sm:gap-y-3 lg:col-span-2 sm:col-span-5">
|
||||
<div>
|
||||
<div className="flex gap-2 mb-4">
|
||||
{tags.map(tag => (
|
||||
<PostTag
|
||||
text={tag}
|
||||
key={tag}
|
||||
active={params.getAll('tags').includes(tag)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="accent font-medium mb-5">{title}</p>
|
||||
<p className="m-text opacity-80">{description}</p>
|
||||
</div>
|
||||
<div className="flex max-lg:justify-between lg:justify-end items-end max-sm:mt-2">
|
||||
<PostDate
|
||||
className="lg:hidden"
|
||||
date={date}
|
||||
month={month}
|
||||
year={year}
|
||||
/>
|
||||
<ClassNameWrapper
|
||||
className="self-end"
|
||||
element={<ArrowMoreIcon />}
|
||||
/>
|
||||
</div>
|
||||
<p className="accent font-medium mb-5">{title}</p>
|
||||
<p className="m-text opacity-80">{description}</p>
|
||||
</div>
|
||||
<div className="flex max-lg:justify-between lg:justify-end items-end max-sm:mt-2">
|
||||
<PostDate
|
||||
className="lg:hidden"
|
||||
date={date}
|
||||
month={month}
|
||||
year={year}
|
||||
/>
|
||||
<ClassNameWrapper className="self-end" element={<ArrowMoreIcon />} />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</Link>
|
||||
{data?.checkAuth.__typename === 'CheckAuthResponse' &&
|
||||
data?.checkAuth.isAuth && (
|
||||
<div className="absolute top-0 right-0 p-4 flex gap-2 z-[9]">
|
||||
<button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ArticleFormModal
|
||||
action="edit"
|
||||
onSubmit={({ blocks, ...data }: IArticleFormInput) => {
|
||||
editArticle({
|
||||
variables: {
|
||||
id,
|
||||
input: {
|
||||
blocks: JSON.stringify(blocks),
|
||||
...data,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
defaultValues={{
|
||||
cardImage,
|
||||
createdAt,
|
||||
description,
|
||||
tags,
|
||||
title,
|
||||
posterImage,
|
||||
blocks,
|
||||
}}
|
||||
/>,
|
||||
'editArticle',
|
||||
)
|
||||
}
|
||||
className="relative p-2 bg-black bg-opacity-60 hover:bg-opacity-70 transition-opacity rounded-full"
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
<Link
|
||||
href={`/dashboard/${id}`}
|
||||
className="relative p-2 bg-black bg-opacity-60 hover:bg-opacity-70 transition-opacity rounded-full"
|
||||
>
|
||||
<EditContentIcon />
|
||||
</Link>
|
||||
<button
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setModal(<DeleteArticleModal id={id} />, 'deleteArticle');
|
||||
}}
|
||||
className="relative p-2 bg-black bg-opacity-60 hover:bg-opacity-70 transition-opacity rounded-full"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { IArticle } from '@/types/IPost';
|
||||
import { PostDate } from '@/ui/PostDate';
|
||||
import parse from 'html-react-parser';
|
||||
import Image from 'next/image';
|
||||
import { ArticleSlider } from './ArticleSlider';
|
||||
|
||||
export function ArticleContent({
|
||||
blocks,
|
||||
createdAt,
|
||||
description,
|
||||
posterImage,
|
||||
tags,
|
||||
title,
|
||||
}: IArticle) {
|
||||
const [year, month, date] = createdAt.split('T')[0].split('-');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-4 gap-x-4 border-b border-[#3D425C] pb-6">
|
||||
<div className="flex flex-col justify-between items-start col-start-1 col-span-1">
|
||||
<div className="flex gap-x-2">
|
||||
{tags.map(tag => (
|
||||
<p
|
||||
key={tag}
|
||||
className="px-3 py-[5px] bg-[#3D425C] m-caption rounded-3xl"
|
||||
>
|
||||
{tag}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<PostDate date={date} month={month} year={year} />
|
||||
</div>
|
||||
<div className="space-y-12 col-span-2">
|
||||
<div className="space-y-6">
|
||||
<h1 className="h2 font-medium">{title}</h1>
|
||||
<p className="h4 font-medium">{description}</p>
|
||||
</div>
|
||||
<div className="aspect-[768/400] relative">
|
||||
<Image
|
||||
fill
|
||||
src={process.env.NEXT_PUBLIC_S3_BUCKET + posterImage}
|
||||
alt={title}
|
||||
className="!relative object-cover object-center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{blocks.map((block, index) =>
|
||||
block.type === 'Content' ? (
|
||||
<div key={index}>{parse(block.content)}</div>
|
||||
) : (
|
||||
<ArticleSlider key={index} slides={block.images} />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+8
-8
@@ -1,18 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { useWindowWidth } from '@/hooks/useWindowWidth';
|
||||
import { IArticle } from '@/types/IPost';
|
||||
import { InfinitySlider } from '@/ui/InfinitySlider';
|
||||
import { PostSliderImage } from '@/ui/PostSlideImage';
|
||||
|
||||
export function PostSlider({ title, slides }: IArticle) {
|
||||
export function ArticleSlider({
|
||||
slides,
|
||||
}: {
|
||||
slides: { img: string; capture?: string }[];
|
||||
}) {
|
||||
const width = useWindowWidth();
|
||||
|
||||
return (
|
||||
<div className="lg:mb-[58px] sm:mb-[68px] mb-6">
|
||||
<h3 className="accent lg:mb-8 lg:ml-[25vw] lg:max-w-[624px] sm:max-w-[84vw] mb-6 sm:ml-6 ml-4">
|
||||
{title}
|
||||
</h3>
|
||||
{slides &&
|
||||
width &&
|
||||
(width >= 1024 ? (
|
||||
@@ -21,7 +21,7 @@ export function PostSlider({ title, slides }: IArticle) {
|
||||
slideWidth={width * 0.47}
|
||||
offset={'25vw'}
|
||||
slides={slides.map(slide => (
|
||||
<PostSliderImage key={slide} slide={slide} title={title} />
|
||||
<PostSliderImage key={slide.img} slide={slide.img} />
|
||||
))}
|
||||
/>
|
||||
) : width >= 640 ? (
|
||||
@@ -31,7 +31,7 @@ export function PostSlider({ title, slides }: IArticle) {
|
||||
className="px-6"
|
||||
offset={'25vw'}
|
||||
slides={slides.map(slide => (
|
||||
<PostSliderImage key={slide} slide={slide} title={title} />
|
||||
<PostSliderImage key={slide.img} slide={slide.img} />
|
||||
))}
|
||||
/>
|
||||
) : (
|
||||
@@ -41,7 +41,7 @@ export function PostSlider({ title, slides }: IArticle) {
|
||||
className="px-4"
|
||||
offset={'25vw'}
|
||||
slides={slides.map(slide => (
|
||||
<PostSliderImage key={slide} slide={slide} title={title} />
|
||||
<PostSliderImage key={slide.img} slide={slide.img} />
|
||||
))}
|
||||
/>
|
||||
))}
|
||||
+1
-13
@@ -1,17 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { TagFilterItem } from '@/components/TagFilterItem';
|
||||
import { YearFilterDropdown } from '@/components/YearFilterDropdown';
|
||||
import { YearFilterItem } from '@/components/YearFilterItem';
|
||||
import { PostTags } from '@/consts/PostTags';
|
||||
import { PostYears } from '@/consts/PostYears';
|
||||
import { Vertical } from '@/ui/Vertical';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export function PostsFilters() {
|
||||
export function ArticlesFilters() {
|
||||
const params = useSearchParams();
|
||||
const chosedTags = params.getAll('tags');
|
||||
const chosenYear = params.get('year');
|
||||
|
||||
return (
|
||||
<div className="flex xl:items-center max-xl:flex-col gap-y-4 justify-between">
|
||||
@@ -33,14 +29,6 @@ export function PostsFilters() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4 h-fit max-sm:hidden">
|
||||
{PostYears.map(year => (
|
||||
<YearFilterItem key={year} text={year} chosen={year === chosenYear} />
|
||||
))}
|
||||
<Vertical />
|
||||
<YearFilterItem text="Все время" isAll chosen={!chosenYear} />
|
||||
</div>
|
||||
<YearFilterDropdown years={PostYears} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import { useGetArticlesQuery } from '@/queries/articles/getArticles';
|
||||
import { IArticle } from '@/types/IPost';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ArticleCard } from './ArticleCard';
|
||||
|
||||
export function ArticlesList() {
|
||||
const params = useSearchParams();
|
||||
|
||||
const [articles, setArticles] = useState<IArticle[]>();
|
||||
const [limit, setLimit] = useState(5);
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setTags(params.getAll('tags'));
|
||||
}, [params]);
|
||||
|
||||
useGetArticlesQuery({
|
||||
variables: { tags, limit, offset: 0 },
|
||||
onCompleted(data) {
|
||||
if (data.articles.__typename === 'Articles') {
|
||||
setArticles(data.articles.articles as IArticle[]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="lg:pb-6">
|
||||
{articles?.length === 0 ? (
|
||||
<p>No posts found</p>
|
||||
) : (
|
||||
<div className="lg:mb-6 sm:mb-5 mb-4 lg:space-y-6 sm:space-y-5 space-y-4">
|
||||
{articles?.map(post => <ArticleCard key={post.id} {...post} />)}
|
||||
<div className="lg:pl-[calc(25vw-40px)]">
|
||||
<button
|
||||
onClick={() => setLimit(limit + 5)}
|
||||
className="lg:opacity-80 lg:hover:opacity-100 btn-text font-semibold border border-[#3D425C] rounded-[32px] lg:py-[14.5px] sm:py-6 py-4 w-full"
|
||||
>
|
||||
Показать еще
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Posts } from '@/consts/Posts';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ArticleCard } from './ArticleCard';
|
||||
|
||||
export function PostsList() {
|
||||
const params = useSearchParams();
|
||||
|
||||
const [posts, setPosts] = useState(Posts);
|
||||
|
||||
useEffect(() => {
|
||||
setPosts(
|
||||
Posts.filter(
|
||||
post =>
|
||||
(!params.has('tags') ||
|
||||
params.getAll('tags').every(tag => post.tags.includes(tag))) &&
|
||||
(!params.has('year') ||
|
||||
params.get('year') === post.createdAt.split(' ')[2]),
|
||||
),
|
||||
);
|
||||
}, [params]);
|
||||
|
||||
// const { data } = useGetArticlesQuery({
|
||||
// variables: { tags: params.getAll('tags'), limit: 10, offset: 0 },
|
||||
// });
|
||||
|
||||
return (
|
||||
<div className="lg:pb-6">
|
||||
{posts.length === 0 ? (
|
||||
<p>No posts found</p>
|
||||
) : (
|
||||
<div className="lg:mb-6 sm:mb-5 mb-4">
|
||||
{posts.map(post => (
|
||||
<ArticleCard key={post.id} {...post} />
|
||||
))}
|
||||
<div className="lg:pl-[calc(25vw-40px)]">
|
||||
<button className="lg:opacity-80 lg:hover:opacity-100 btn-text font-semibold border border-[#3D425C] rounded-[32px] lg:py-[14.5px] sm:py-6 py-4 w-full">
|
||||
Показать еще
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { AppearanceHr } from '@/ui/AppearanceHr';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export function RelevantPost({ image, title, id }: IArticle) {
|
||||
export function RelevantArticles({ image, title, id }: IArticle) {
|
||||
const { push } = useRouter();
|
||||
|
||||
return (
|
||||
@@ -6,7 +6,7 @@ import { Posts } from '@/consts/Posts';
|
||||
import { useWindowWidth } from '@/hooks/useWindowWidth';
|
||||
import { InfinitySlider } from '@/ui/InfinitySlider';
|
||||
import { useRef } from 'react';
|
||||
import { RelevantPost } from './RelevantPost';
|
||||
import { RelevantArticles } from './RelevantArticles';
|
||||
|
||||
export function RelevantSlider() {
|
||||
const left = useRef<HTMLButtonElement>(null);
|
||||
@@ -37,28 +37,31 @@ export function RelevantSlider() {
|
||||
<InfinitySlider
|
||||
slideType="link"
|
||||
slidesGap={16}
|
||||
slides={Posts.map(RelevantPost)}
|
||||
slides={Posts.map(RelevantArticles)}
|
||||
slideWidth={width * 0.31}
|
||||
className="px-10"
|
||||
controlsRefs={{ left, right }}
|
||||
className="px-10"
|
||||
withControls={false}
|
||||
/>
|
||||
) : width >= 640 ? (
|
||||
<InfinitySlider
|
||||
slideType="link"
|
||||
slidesGap={12}
|
||||
slides={Posts.map(RelevantPost)}
|
||||
slides={Posts.map(RelevantArticles)}
|
||||
slideWidth={width * 0.57}
|
||||
className="px-8"
|
||||
controlsRefs={{ left, right }}
|
||||
withControls={false}
|
||||
/>
|
||||
) : (
|
||||
<InfinitySlider
|
||||
slideType="link"
|
||||
slidesGap={8}
|
||||
slides={Posts.map(RelevantPost)}
|
||||
slides={Posts.map(RelevantArticles)}
|
||||
slideWidth={328}
|
||||
className="px-4"
|
||||
controlsRefs={{ left, right }}
|
||||
withControls={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { ArrowMoreIcon } from '@/components/icons/ArrowMoreIcon';
|
||||
import {
|
||||
IAddProjectFormInput,
|
||||
ProjectFormModal,
|
||||
} from '@/components/modals/ProjectFormModal';
|
||||
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
|
||||
import { useCheckAuthQuery } from '@/queries/auth/checkAuth';
|
||||
import { useAddProjectMutation } from '@/queries/projects/addProject';
|
||||
import { useGetProjectsQuery } from '@/queries/projects/getProjects';
|
||||
import { useModalStore } from '@/stores/useModalStore';
|
||||
import { IProject } from '@/types/IProject';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Descriptor } from '@/ui/Descriptor';
|
||||
import { getProjectsCount } from '@/utils/getProjectsCount';
|
||||
import { getSortedProjects } from '@/utils/getSortedProjects';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import Link from 'next/link';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ProjectsSection } from '../ProjectsPage/ProjectsSection';
|
||||
|
||||
export function Projects() {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const [all, setAll] = useState(false);
|
||||
const [sortedProjects, setSortedProjects] =
|
||||
useState<Map<string | number, IProject[]>>();
|
||||
|
||||
const client = useApolloClient();
|
||||
|
||||
const { data } = useGetProjectsQuery({ variables: { devices: [] } });
|
||||
|
||||
const { data: authData } = useCheckAuthQuery();
|
||||
|
||||
const getProjects = useCallback(async () => {
|
||||
try {
|
||||
setSortedProjects(
|
||||
@@ -34,6 +49,14 @@ export function Projects() {
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const [addProject] = useAddProjectMutation({
|
||||
onCompleted() {
|
||||
client.refetchQueries({
|
||||
include: ['GetProjects'],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const projectsInProccess = sortedProjects?.get('В работе') ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
@@ -56,6 +79,27 @@ export function Projects() {
|
||||
в разных городах России и мира
|
||||
</p>
|
||||
</div>
|
||||
{authData?.checkAuth.__typename === 'CheckAuthResponse' &&
|
||||
authData?.checkAuth.isAuth && (
|
||||
<div className="m-auto">
|
||||
<Button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ProjectFormModal
|
||||
action={'create'}
|
||||
onSubmit={(data: IAddProjectFormInput) => {
|
||||
addProject({ variables: { input: data } });
|
||||
setModal(null, '');
|
||||
}}
|
||||
/>,
|
||||
'addProject',
|
||||
)
|
||||
}
|
||||
>
|
||||
Добавить проект
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Link
|
||||
href={'/projects'}
|
||||
className="lg:col-start-4 self-end w-full bg-[#14161F] btn-text flex justify-between items-center rounded-full border border-[#3D425C] py-5 px-6"
|
||||
|
||||
@@ -7,7 +7,7 @@ import Image from 'next/image';
|
||||
|
||||
export function ProjectsMap() {
|
||||
return (
|
||||
<div className="relative w-fit h-fit origin-top-left">
|
||||
<div className="relative w-fit h-fit origin-top-left max-md:hidden z-[9]">
|
||||
<Image
|
||||
src={'/img/pages/home/stats/map.jpg'}
|
||||
alt={'Карта проектов'}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function Showreel() {
|
||||
className="w-full lg:aspect-[1552/616] object-cover self-stretch"
|
||||
/>
|
||||
<button
|
||||
className="absolute z-10 p-8 rounded-full border group-hover:block hidden bg-[#14161F33]"
|
||||
className="absolute z-[9] p-8 rounded-full border group-hover:block hidden bg-[#14161F33]"
|
||||
onClick={() => {
|
||||
setModal(
|
||||
<VideoModal
|
||||
|
||||
@@ -43,8 +43,14 @@ export function Statistics() {
|
||||
Мы собрали статистику за 13 лет работы c застройщиками,
|
||||
реализовав {getProjectsCount(projects.length)}
|
||||
</div>
|
||||
<div className="lg:col-start-4 lg:col-span-full sm:col-span-2 lg:py-10 lg:pl-4 border-b border-[#3D425C] aspect-[1176/570] max-md:hidden">
|
||||
<div className="lg:col-start-4 lg:col-span-full col-span-2 lg:py-10 lg:pl-4 border-b border-[#3D425C] aspect-[1176/570]">
|
||||
<ProjectsMap />
|
||||
<Image
|
||||
src={'/img/pages/home/stats/map_with_points.png'}
|
||||
alt="map"
|
||||
fill
|
||||
className="!relative md:hidden"
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:col-span-3 md:col-span-1 col-span-full lg:py-10 sm:max-lg:pt-8 pt-6 lg:flex flex-col justify-between sm:max-lg:space-y-5 max-sm:space-y-3 lg:border-b lg:border-r border-[#3D425C]">
|
||||
<Descriptor title="экономическая эффективность" />
|
||||
|
||||
@@ -46,11 +46,13 @@ export function Streaming() {
|
||||
city="Екатеринбург"
|
||||
name="Re:volution Towers"
|
||||
image="/img/pages/home/streaming/revolution.png"
|
||||
href="https://stream.graff.tech"
|
||||
/>
|
||||
<StreamingProject
|
||||
city="Тюмень"
|
||||
image="/img/pages/home/streaming/aivazovsky.png"
|
||||
name="Айвазовский City"
|
||||
href="https://stream.graff.tech/?build=Ivazowsky&location=a1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,10 +64,11 @@ function StreamingProject({
|
||||
city,
|
||||
name,
|
||||
image,
|
||||
}: Pick<IProject, 'city' | 'name' | 'image'>) {
|
||||
href,
|
||||
}: Pick<IProject, 'city' | 'name' | 'image'> & { href: string }) {
|
||||
return (
|
||||
<Link
|
||||
href={'https://stream.graff.tech'}
|
||||
href={href}
|
||||
className="flex flex-col justify-between gap-y-12 p-6 [background:center/cover_url(/img/pages/home/stats/highlight.svg)_no-repeat,#14161F] col-span-1"
|
||||
>
|
||||
<Image
|
||||
|
||||
@@ -52,7 +52,7 @@ export function ProjectCard({
|
||||
>
|
||||
{data?.checkAuth.__typename === 'CheckAuthResponse' &&
|
||||
data.checkAuth.isAuth && (
|
||||
<div className="absolute top-0 right-0 p-4 flex gap-2 z-10">
|
||||
<div className="absolute top-0 right-0 p-4 flex gap-2 z-[9]">
|
||||
<button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
@@ -76,7 +76,7 @@ export function ProjectCard({
|
||||
'editProject',
|
||||
)
|
||||
}
|
||||
className="group relative p-2 bg-black bg-opacity-60 hover:bg-opacity-70 transition-opacity rounded-full"
|
||||
className="relative p-2 bg-black bg-opacity-60 hover:bg-opacity-70 transition-opacity rounded-full"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -92,13 +92,10 @@ export function ProjectCard({
|
||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
<span className="pointer-events-none group-hover:opacity-100 opacity-0 transition-opacity absolute -bottom-[90%] left-[50%] -translate-x-[50%] bg-neutral-900 px-2 py-1 text-sm rounded-lg">
|
||||
Редактировать
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModal(<DeleteProjectModal id={id} />, '')}
|
||||
className="group relative p-2 bg-black bg-opacity-60 hover:bg-opacity-70 transition-opacity rounded-full"
|
||||
className="relative p-2 bg-black bg-opacity-60 hover:bg-opacity-70 transition-opacity rounded-full"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -114,16 +111,13 @@ export function ProjectCard({
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||
/>
|
||||
</svg>
|
||||
<span className="pointer-events-none group-hover:opacity-100 opacity-0 transition-opacity absolute -bottom-[90%] left-[50%] -translate-x-[50%] bg-neutral-900 px-2 py-1 text-sm rounded-lg">
|
||||
Удалить
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="group-hover:scale-110 transition-transform duration-500 absolute top-0 left-0 w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url(${process.env.NEXT_PUBLIC_S3_BUCKET}/${image})`,
|
||||
backgroundImage: `url(${process.env.NEXT_PUBLIC_S3_BUCKET}${image})`,
|
||||
}}
|
||||
/>
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-gradient-card" />
|
||||
|
||||
@@ -24,14 +24,6 @@ export function ProjectsFilters() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* <div className="flex flex-wrap gap-4 h-fit max-sm:hidden">
|
||||
{ProjectYears.map(year => (
|
||||
<YearFilterItem key={year} text={year} chosen={year === chosenYear} />
|
||||
))}
|
||||
<Vertical />
|
||||
<YearFilterItem text="Все время" isAll chosen={!params.has('year')} />
|
||||
</div>
|
||||
<YearFilterDropdown years={ProjectYears} /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const PostTags = ['недвижимость', 'награды', 'выставки'];
|
||||
export const PostTags = ['Недвижимость', 'Награды', 'Выставки'];
|
||||
|
||||
@@ -52,7 +52,7 @@ export type AuthResponse = {
|
||||
|
||||
export type AuthResult = AuthResponse | Error;
|
||||
|
||||
export type Block = Content | Quote | Slider | Video;
|
||||
export type Block = Content | Slider;
|
||||
|
||||
export type CheckAuthResponse = {
|
||||
__typename?: 'CheckAuthResponse';
|
||||
@@ -211,15 +211,6 @@ export type QueryProjectsArgs = {
|
||||
input?: InputMaybe<ProjectInput>;
|
||||
};
|
||||
|
||||
export type Quote = {
|
||||
__typename?: 'Quote';
|
||||
authorAvatar: Scalars['String']['output'];
|
||||
authorName: Scalars['String']['output'];
|
||||
authorRole: Scalars['String']['output'];
|
||||
text: Scalars['String']['output'];
|
||||
type: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type Region = {
|
||||
__typename?: 'Region';
|
||||
regionName: Scalars['String']['output'];
|
||||
@@ -232,10 +223,3 @@ export type Slider = {
|
||||
images: Array<Image>;
|
||||
type: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type Video = {
|
||||
__typename?: 'Video';
|
||||
caption?: Maybe<Scalars['String']['output']>;
|
||||
type: Scalars['String']['output'];
|
||||
video: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ export type AddArticleMutationVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type AddArticleMutation = { __typename?: 'Mutation', createArticle: { __typename?: 'Article', id: number, title: string, description: string, posterImage: string, cardImage: string, createdAt: any, tags: Array<string>, blocks: Array<{ __typename?: 'Content', type: string, content: string } | { __typename?: 'Quote', type: string, text: string, authorName: string, authorAvatar: string, authorRole: string } | { __typename?: 'Slider', type: string, images: Array<{ __typename?: 'Image', img: string, caption?: string | null }> } | { __typename?: 'Video', type: string, video: string, caption?: string | null }> } | { __typename?: 'Error' } };
|
||||
export type AddArticleMutation = { __typename?: 'Mutation', createArticle: { __typename?: 'Article', id: number, title: string, description: string, posterImage: string, cardImage: string, createdAt: any, tags: Array<string>, blocks: Array<{ __typename?: 'Content', type: string, content: string } | { __typename?: 'Slider', type: string, images: Array<{ __typename?: 'Image', img: string, caption?: string | null }> }> } | { __typename?: 'Error' } };
|
||||
|
||||
|
||||
export const AddArticleDocument = gql`
|
||||
@@ -27,13 +27,6 @@ export const AddArticleDocument = gql`
|
||||
type
|
||||
content
|
||||
}
|
||||
... on Quote {
|
||||
type
|
||||
text
|
||||
authorName
|
||||
authorAvatar
|
||||
authorRole
|
||||
}
|
||||
... on Slider {
|
||||
type
|
||||
images {
|
||||
@@ -41,11 +34,6 @@ export const AddArticleDocument = gql`
|
||||
caption
|
||||
}
|
||||
}
|
||||
... on Video {
|
||||
type
|
||||
video
|
||||
caption
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,6 @@ mutation AddArticle($input: CreateArticleInput!) {
|
||||
type
|
||||
content
|
||||
}
|
||||
... on Quote {
|
||||
type
|
||||
text
|
||||
authorName
|
||||
authorAvatar
|
||||
authorRole
|
||||
}
|
||||
... on Slider {
|
||||
type
|
||||
images {
|
||||
@@ -27,11 +20,6 @@ mutation AddArticle($input: CreateArticleInput!) {
|
||||
caption
|
||||
}
|
||||
}
|
||||
... on Video {
|
||||
type
|
||||
video
|
||||
caption
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export type DeleteArticleMutationVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteArticleMutation = { __typename?: 'Mutation', deleteArticle: { __typename?: 'Article', id: number, title: string, description: string, createdAt: any, cardImage: string, posterImage: string, tags: Array<string>, blocks: Array<{ __typename?: 'Content', type: string, content: string } | { __typename?: 'Quote', type: string, text: string, authorName: string, authorAvatar: string, authorRole: string } | { __typename?: 'Slider', type: string, images: Array<{ __typename?: 'Image', img: string, caption?: string | null }> } | { __typename?: 'Video', type: string, video: string, caption?: string | null }> } | { __typename?: 'Error', message: string } };
|
||||
export type DeleteArticleMutation = { __typename?: 'Mutation', deleteArticle: { __typename?: 'Article', id: number, title: string, description: string, createdAt: any, cardImage: string, posterImage: string, tags: Array<string>, blocks: Array<{ __typename?: 'Content', type: string, content: string } | { __typename?: 'Slider', type: string, images: Array<{ __typename?: 'Image', img: string, caption?: string | null }> }> } | { __typename?: 'Error', message: string } };
|
||||
|
||||
|
||||
export const DeleteArticleDocument = gql`
|
||||
@@ -30,13 +30,6 @@ export const DeleteArticleDocument = gql`
|
||||
type
|
||||
content
|
||||
}
|
||||
... on Quote {
|
||||
type
|
||||
text
|
||||
authorName
|
||||
authorAvatar
|
||||
authorRole
|
||||
}
|
||||
... on Slider {
|
||||
type
|
||||
images {
|
||||
@@ -44,11 +37,6 @@ export const DeleteArticleDocument = gql`
|
||||
caption
|
||||
}
|
||||
}
|
||||
... on Video {
|
||||
type
|
||||
video
|
||||
caption
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,6 @@ mutation DeleteArticle($id: Int!) {
|
||||
type
|
||||
content
|
||||
}
|
||||
... on Quote {
|
||||
type
|
||||
text
|
||||
authorName
|
||||
authorAvatar
|
||||
authorRole
|
||||
}
|
||||
... on Slider {
|
||||
type
|
||||
images {
|
||||
@@ -30,11 +23,6 @@ mutation DeleteArticle($id: Int!) {
|
||||
caption
|
||||
}
|
||||
}
|
||||
... on Video {
|
||||
type
|
||||
video
|
||||
caption
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export type EditArticleMutationVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type EditArticleMutation = { __typename?: 'Mutation', updateArticle: { __typename?: 'Article', id: number, title: string, description: string, posterImage: string, cardImage: string, createdAt: any, tags: Array<string>, blocks: Array<{ __typename?: 'Content', type: string, content: string } | { __typename?: 'Quote', type: string, text: string, authorName: string, authorAvatar: string, authorRole: string } | { __typename?: 'Slider', type: string, images: Array<{ __typename?: 'Image', img: string, caption?: string | null }> } | { __typename?: 'Video', type: string, video: string, caption?: string | null }> } | { __typename?: 'Error', message: string } };
|
||||
export type EditArticleMutation = { __typename?: 'Mutation', updateArticle: { __typename?: 'Article', id: number, title: string, description: string, posterImage: string, cardImage: string, createdAt: any, tags: Array<string>, blocks: Array<{ __typename?: 'Content', type: string, content: string } | { __typename?: 'Slider', type: string, images: Array<{ __typename?: 'Image', img: string, caption?: string | null }> }> } | { __typename?: 'Error', message: string } };
|
||||
|
||||
|
||||
export const EditArticleDocument = gql`
|
||||
@@ -31,13 +31,6 @@ export const EditArticleDocument = gql`
|
||||
type
|
||||
content
|
||||
}
|
||||
... on Quote {
|
||||
type
|
||||
text
|
||||
authorName
|
||||
authorAvatar
|
||||
authorRole
|
||||
}
|
||||
... on Slider {
|
||||
type
|
||||
images {
|
||||
@@ -45,11 +38,6 @@ export const EditArticleDocument = gql`
|
||||
caption
|
||||
}
|
||||
}
|
||||
... on Video {
|
||||
type
|
||||
video
|
||||
caption
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,6 @@ mutation EditArticle($id: Int!, $input: CreateArticleInput!) {
|
||||
type
|
||||
content
|
||||
}
|
||||
... on Quote {
|
||||
type
|
||||
text
|
||||
authorName
|
||||
authorAvatar
|
||||
authorRole
|
||||
}
|
||||
... on Slider {
|
||||
type
|
||||
images {
|
||||
@@ -30,11 +23,6 @@ mutation EditArticle($id: Int!, $input: CreateArticleInput!) {
|
||||
caption
|
||||
}
|
||||
}
|
||||
... on Video {
|
||||
type
|
||||
video
|
||||
caption
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export type GetArticleByIdQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetArticleByIdQuery = { __typename?: 'Query', article: { __typename?: 'Article', id: number, title: string, description: string, createdAt: any, cardImage: string, posterImage: string, tags: Array<string>, blocks: Array<{ __typename?: 'Content', type: string, content: string } | { __typename?: 'Quote', type: string, text: string, authorName: string, authorAvatar: string, authorRole: string } | { __typename?: 'Slider', type: string, images: Array<{ __typename?: 'Image', img: string, caption?: string | null }> } | { __typename?: 'Video', type: string, video: string, caption?: string | null }> } | { __typename?: 'Error', message: string } };
|
||||
export type GetArticleByIdQuery = { __typename?: 'Query', article: { __typename?: 'Article', id: number, title: string, description: string, createdAt: any, cardImage: string, posterImage: string, tags: Array<string>, blocks: Array<{ __typename?: 'Content', type: string, content: string } | { __typename?: 'Slider', type: string, images: Array<{ __typename?: 'Image', img: string, caption?: string | null }> }> } | { __typename?: 'Error', message: string } };
|
||||
|
||||
|
||||
export const GetArticleByIdDocument = gql`
|
||||
@@ -30,13 +30,6 @@ export const GetArticleByIdDocument = gql`
|
||||
type
|
||||
content
|
||||
}
|
||||
... on Quote {
|
||||
type
|
||||
text
|
||||
authorName
|
||||
authorAvatar
|
||||
authorRole
|
||||
}
|
||||
... on Slider {
|
||||
type
|
||||
images {
|
||||
@@ -44,11 +37,6 @@ export const GetArticleByIdDocument = gql`
|
||||
caption
|
||||
}
|
||||
}
|
||||
... on Video {
|
||||
type
|
||||
video
|
||||
caption
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,6 @@ query GetArticleById($id: Int!) {
|
||||
type
|
||||
content
|
||||
}
|
||||
... on Quote {
|
||||
type
|
||||
text
|
||||
authorName
|
||||
authorAvatar
|
||||
authorRole
|
||||
}
|
||||
... on Slider {
|
||||
type
|
||||
images {
|
||||
@@ -30,11 +23,6 @@ query GetArticleById($id: Int!) {
|
||||
caption
|
||||
}
|
||||
}
|
||||
... on Video {
|
||||
type
|
||||
video
|
||||
caption
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export type GetArticlesQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetArticlesQuery = { __typename?: 'Query', articles: { __typename?: 'Articles', articles: Array<{ __typename?: 'Article', id: number, title: string, description: string, tags: Array<string>, createdAt: any, posterImage: string, cardImage: string, blocks: Array<{ __typename?: 'Content', content: string } | { __typename?: 'Quote', text: string, authorName: string, authorAvatar: string, authorRole: string } | { __typename?: 'Slider', images: Array<{ __typename?: 'Image', img: string, caption?: string | null }> } | { __typename?: 'Video', video: string, caption?: string | null }> }> } | { __typename?: 'Error', message: string } };
|
||||
export type GetArticlesQuery = { __typename?: 'Query', articles: { __typename?: 'Articles', articles: Array<{ __typename?: 'Article', id: number, title: string, description: string, tags: Array<string>, createdAt: any, posterImage: string, cardImage: string, blocks: Array<{ __typename?: 'Content', content: string } | { __typename?: 'Slider', images: Array<{ __typename?: 'Image', img: string, caption?: string | null }> }> }> } | { __typename?: 'Error', message: string } };
|
||||
|
||||
|
||||
export const GetArticlesDocument = gql`
|
||||
@@ -32,22 +32,12 @@ export const GetArticlesDocument = gql`
|
||||
... on Content {
|
||||
content
|
||||
}
|
||||
... on Quote {
|
||||
text
|
||||
authorName
|
||||
authorAvatar
|
||||
authorRole
|
||||
}
|
||||
... on Slider {
|
||||
images {
|
||||
img
|
||||
caption
|
||||
}
|
||||
}
|
||||
... on Video {
|
||||
video
|
||||
caption
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,22 +16,12 @@ query GetArticles($tags: [String!]!, $limit: Int, $offset: Int) {
|
||||
... on Content {
|
||||
content
|
||||
}
|
||||
... on Quote {
|
||||
text
|
||||
authorName
|
||||
authorAvatar
|
||||
authorRole
|
||||
}
|
||||
... on Slider {
|
||||
images {
|
||||
img
|
||||
caption
|
||||
}
|
||||
}
|
||||
... on Video {
|
||||
video
|
||||
caption
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-15
@@ -13,21 +13,7 @@ export interface ISlider {
|
||||
images: IImage[];
|
||||
}
|
||||
|
||||
export interface IVideo {
|
||||
type: 'Video';
|
||||
video: string;
|
||||
caption?: string | null;
|
||||
}
|
||||
|
||||
export interface IQuote {
|
||||
type: 'Quote';
|
||||
text: string;
|
||||
authorName: string;
|
||||
authorAvatar: string;
|
||||
authorRole: string;
|
||||
}
|
||||
|
||||
export type Block = IContent | ISlider | IVideo | IQuote;
|
||||
export type Block = IContent | ISlider;
|
||||
|
||||
export interface IArticle {
|
||||
id: number;
|
||||
|
||||
+7
-1
@@ -8,6 +8,7 @@ interface ButtonProps {
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
type?: 'submit' | 'reset' | 'button';
|
||||
}
|
||||
|
||||
export function Button({
|
||||
@@ -18,11 +19,16 @@ export function Button({
|
||||
disabled = false,
|
||||
className,
|
||||
onClick,
|
||||
type,
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
onClick={e => {
|
||||
if (type !== 'submit') e.preventDefault();
|
||||
onClick?.();
|
||||
}}
|
||||
className={`group relative px-6 py-2 rounded-full min-w-fit ${
|
||||
(color === 'primary'
|
||||
? 'bg-gradient-to-r from-[#798FFF] to-[#D375FF]'
|
||||
|
||||
+12
-25
@@ -1,5 +1,3 @@
|
||||
import { ArrowLeftIcon } from '@/components/icons/ArrowLeftIcon';
|
||||
import { ArrowRightIcon } from '@/components/icons/ArrowRightIcon';
|
||||
import { useWindowWidth } from '@/hooks/useWindowWidth';
|
||||
import {
|
||||
ReactNode,
|
||||
@@ -11,6 +9,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useSwipeable } from 'react-swipeable';
|
||||
import { SliderControls } from './SliderControls';
|
||||
|
||||
export function InfinitySlider({
|
||||
slides,
|
||||
@@ -18,23 +17,23 @@ export function InfinitySlider({
|
||||
offset = 0,
|
||||
className = '',
|
||||
slidesGap = 16,
|
||||
progressMT = 16,
|
||||
slideType = 'image',
|
||||
controlsRefs = undefined,
|
||||
withDarkness = false,
|
||||
withControls = true,
|
||||
}: {
|
||||
slides: ReactNode[];
|
||||
slideWidth: number;
|
||||
offset?: string | number;
|
||||
className?: string;
|
||||
slidesGap?: number;
|
||||
progressMT?: number;
|
||||
slideType?: 'image' | 'link';
|
||||
controlsRefs?: {
|
||||
left: RefObject<HTMLButtonElement>;
|
||||
right: RefObject<HTMLButtonElement>;
|
||||
};
|
||||
withDarkness?: boolean;
|
||||
withControls?: boolean;
|
||||
}) {
|
||||
const width = useWindowWidth();
|
||||
const baseOffset = slideWidth + slidesGap;
|
||||
@@ -143,27 +142,15 @@ export function InfinitySlider({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{!controlsRefs && (
|
||||
<div
|
||||
style={{
|
||||
marginLeft: width >= 1024 ? offset : 'auto',
|
||||
marginTop: progressMT,
|
||||
}}
|
||||
className="flex items-center gap-4 lg:w-[clamp(720px,100vw-465px,1136px)] desktop-figma:w-[71vw]"
|
||||
>
|
||||
<button onClick={prevSlide} className="max-sm:hidden">
|
||||
<ArrowLeftIcon />
|
||||
</button>
|
||||
<div className="h-1 bg-[#3D425C] w-full">
|
||||
<div
|
||||
className="bg-[#ffffff] h-1 duration-500"
|
||||
style={{ width: `${((slide + 1) / 3) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<button onClick={nextSlide} className="max-sm:hidden">
|
||||
<ArrowRightIcon />
|
||||
</button>
|
||||
</div>
|
||||
{withControls && (
|
||||
<SliderControls
|
||||
slidesCount={slides.length}
|
||||
width={width >= 640 ? 132 : (width / 360) * 196}
|
||||
height={width >= 640 ? 66 : 58}
|
||||
slide={slide}
|
||||
onLeftClick={prevSlide}
|
||||
onRightClick={nextSlide}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
+5
-11
@@ -1,28 +1,22 @@
|
||||
import { getMonthTitle } from '@/utils/getMonthTitle';
|
||||
|
||||
export function PostDate({
|
||||
date,
|
||||
month,
|
||||
year,
|
||||
inPostCard = true,
|
||||
className = '',
|
||||
}: {
|
||||
date: string;
|
||||
month: string;
|
||||
year: string;
|
||||
inPostCard?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'lg:min-w-[105px] lg:max-w-[calc(25vw-56px)] lg:w-[23vw]' +
|
||||
(inPostCard ? ' flex-1 self-end ' : ' ') +
|
||||
className
|
||||
}
|
||||
>
|
||||
<div className={className}>
|
||||
<h2 className="h2 font-medium mb-2 max-lg:hidden">{date}</h2>
|
||||
<p className="descriptor font-medium max-lg:opacity-70">
|
||||
<p className="descriptor font-medium max-lg:opacity-70 uppercase">
|
||||
<span className="lg:hidden">{date} </span>
|
||||
{month} {year}
|
||||
{getMonthTitle(month)} {year}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
export function PostSliderImage({
|
||||
slide,
|
||||
title,
|
||||
}: {
|
||||
slide: string;
|
||||
title: string;
|
||||
}) {
|
||||
export function PostSliderImage({ slide }: { slide: string }) {
|
||||
return (
|
||||
<Image
|
||||
src={slide}
|
||||
alt={title}
|
||||
src={process.env.NEXT_PUBLIC_S3_BUCKET + slide}
|
||||
alt={slide}
|
||||
fill
|
||||
className="!static object-cover max-lg:rounded-2xl"
|
||||
sizes="100%"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export function getMonthTitle(month: string) {
|
||||
return new Map([
|
||||
['1', 'Января'],
|
||||
['2', 'Февраля'],
|
||||
['3', 'Марта'],
|
||||
['4', 'Апреля'],
|
||||
['5', 'Мая'],
|
||||
['6', 'Июня'],
|
||||
['7', 'Июля'],
|
||||
['8', 'Августа'],
|
||||
['9', 'Сентября'],
|
||||
['10', 'Октября'],
|
||||
['11', 'Ноября'],
|
||||
['12', 'Декабря'],
|
||||
]).get(month);
|
||||
}
|
||||
@@ -2400,6 +2400,36 @@ dom-helpers@^5.0.1:
|
||||
"@babel/runtime" "^7.8.7"
|
||||
csstype "^3.0.2"
|
||||
|
||||
dom-serializer@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
|
||||
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.2"
|
||||
entities "^4.2.0"
|
||||
|
||||
domelementtype@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
|
||||
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
|
||||
|
||||
domhandler@5.0.3, domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
|
||||
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
domutils@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
|
||||
integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
|
||||
dependencies:
|
||||
dom-serializer "^2.0.0"
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
|
||||
dot-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
|
||||
@@ -2451,6 +2481,11 @@ enhanced-resolve@^5.12.0:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
entities@^4.2.0, entities@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||
|
||||
env-paths@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
|
||||
@@ -3266,6 +3301,34 @@ hoist-non-react-statics@^3.3.2:
|
||||
dependencies:
|
||||
react-is "^16.7.0"
|
||||
|
||||
html-dom-parser@5.0.10:
|
||||
version "5.0.10"
|
||||
resolved "https://registry.yarnpkg.com/html-dom-parser/-/html-dom-parser-5.0.10.tgz#bf46b05c50f35c2fcadfc8e91566c54d3caf9bd7"
|
||||
integrity sha512-GwArYL3V3V8yU/mLKoFF7HlLBv80BZ2Ey1BzfVNRpAci0cEKhFHI/Qh8o8oyt3qlAMLlK250wsxLdYX4viedvg==
|
||||
dependencies:
|
||||
domhandler "5.0.3"
|
||||
htmlparser2 "9.1.0"
|
||||
|
||||
html-react-parser@^5.1.18:
|
||||
version "5.1.18"
|
||||
resolved "https://registry.yarnpkg.com/html-react-parser/-/html-react-parser-5.1.18.tgz#a07ff6d95fcaa6de45244386a12dddb981434915"
|
||||
integrity sha512-65BwC0zzrdeW96jB2FRr5f1ovBhRMpLPJNvwkY5kA8Ay5xdL9t/RH2/uUTM7p+cl5iM88i6dDk4LXtfMnRmaJQ==
|
||||
dependencies:
|
||||
domhandler "5.0.3"
|
||||
html-dom-parser "5.0.10"
|
||||
react-property "2.0.2"
|
||||
style-to-js "1.1.16"
|
||||
|
||||
htmlparser2@9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23"
|
||||
integrity sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
domhandler "^5.0.3"
|
||||
domutils "^3.1.0"
|
||||
entities "^4.5.0"
|
||||
|
||||
http-proxy-agent@^7.0.0:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
|
||||
@@ -3340,6 +3403,11 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4:
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
inline-style-parser@0.2.4:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.4.tgz#f4af5fe72e612839fcd453d989a586566d695f22"
|
||||
integrity sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==
|
||||
|
||||
input-format@^0.3.10:
|
||||
version "0.3.10"
|
||||
resolved "https://registry.yarnpkg.com/input-format/-/input-format-0.3.10.tgz#e8a8855e2e89e3b1cd995333f6277c14865f0e35"
|
||||
@@ -4527,6 +4595,11 @@ react-phone-number-input@*, react-phone-number-input@^3.4.5:
|
||||
libphonenumber-js "^1.11.5"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-property@2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.2.tgz#d5ac9e244cef564880a610bc8d868bd6f60fdda6"
|
||||
integrity sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==
|
||||
|
||||
react-rangeslider@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-rangeslider/-/react-rangeslider-2.2.0.tgz#4362b01f4f5a455f0815d371d496f69ca4c6b5aa"
|
||||
@@ -5052,6 +5125,20 @@ strip-json-comments@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
style-to-js@1.1.16:
|
||||
version "1.1.16"
|
||||
resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.16.tgz#e6bd6cd29e250bcf8fa5e6591d07ced7575dbe7a"
|
||||
integrity sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==
|
||||
dependencies:
|
||||
style-to-object "1.0.8"
|
||||
|
||||
style-to-object@1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.8.tgz#67a29bca47eaa587db18118d68f9d95955e81292"
|
||||
integrity sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==
|
||||
dependencies:
|
||||
inline-style-parser "0.2.4"
|
||||
|
||||
styled-jsx@5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f"
|
||||
|
||||
Reference in New Issue
Block a user