This commit is contained in:
2025-01-22 19:34:38 +05:00
parent 4810b896b7
commit 213d293629
28 changed files with 359 additions and 158 deletions
+1
View File
@@ -13,6 +13,7 @@
}, },
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.62.7", "@tanstack/react-query": "^5.62.7",
"@tanstack/react-query-devtools": "^5.64.2",
"@tinymce/tinymce-react": "^5.1.1", "@tinymce/tinymce-react": "^5.1.1",
"countries-phone-masks": "^1.1.0", "countries-phone-masks": "^1.1.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
@@ -4,7 +4,7 @@ import { ArticleActions } from '@/components/ArticleActions';
import { ArticleHeader } from '@/components/ArticleHeader'; import { ArticleHeader } from '@/components/ArticleHeader';
import { ArticleContentInput } from '@/components/articleInputs/ArticleContentInput'; import { ArticleContentInput } from '@/components/articleInputs/ArticleContentInput';
import { ArticleSliderInput } from '@/components/articleInputs/ArticleSliderInput'; import { ArticleSliderInput } from '@/components/articleInputs/ArticleSliderInput';
import { IArticleFormInput } from '@/components/modals/ArticleHeaderFormModal'; import { IArticleFormInput } from '@/components/modals/ArticleFormModal';
import { ArticleContent } from '@/components/pages/BlogPage/ArticleContent'; import { ArticleContent } from '@/components/pages/BlogPage/ArticleContent';
import { IArticle } from '@/types/IArticle'; import { IArticle } from '@/types/IArticle';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
+13 -1
View File
@@ -1,8 +1,11 @@
import { api } from '@/api'; import { api } from '@/api';
import { CloseIcon } from '@/components/icons/CloseIcon';
import { RelevantArticlesPreview } from '@/components/pages/ArticlePage/RelevantArticlesPreview'; import { RelevantArticlesPreview } from '@/components/pages/ArticlePage/RelevantArticlesPreview';
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
import { IArticle } from '@/types/IArticle'; import { IArticle } from '@/types/IArticle';
import { QueryClient } from '@tanstack/react-query'; import { QueryClient } from '@tanstack/react-query';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import Link from 'next/link';
export async function generateMetadata({ export async function generateMetadata({
params, params,
@@ -41,10 +44,19 @@ export default async function Layout({
params: Promise<{ articleId: string }>; params: Promise<{ articleId: string }>;
}) { }) {
const { articleId } = await params; const { articleId } = await params;
return ( return (
<section className="absolute w-screen h-screen bg-[#0F101199] backdrop-blur-lg"> <section className="fixed top-0 left-0 w-screen h-screen bg-[#0F101199] backdrop-blur-lg z-[14]">
<RelevantArticlesPreview articleId={articleId} /> <RelevantArticlesPreview articleId={articleId} />
{children} {children}
<Link
href={'/blog'}
className="fixed right-5 top-5 rounded-2xl bg-[#37393B99] p-4"
>
<ClassNameWrapper className={'w-4 h-4'}>
<CloseIcon />
</ClassNameWrapper>
</Link>
</section> </section>
); );
} }
+11 -2
View File
@@ -1,6 +1,11 @@
import { api } from '@/api'; import { api } from '@/api';
import { ArticleSyncPage } from '@/components/pages/ArticlePage/ArticleSyncPage';
import { IArticle } from '@/types/IArticle'; import { IArticle } from '@/types/IArticle';
import { HydrationBoundary, QueryClient } from '@tanstack/react-query'; import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query';
export default async function ArticlePage({ export default async function ArticlePage({
params: { articleId }, params: { articleId },
@@ -15,5 +20,9 @@ export default async function ArticlePage({
await api.get(`articles/${articleId}`).json<IArticle>(), await api.get(`articles/${articleId}`).json<IArticle>(),
}); });
return <HydrationBoundary queryClient={queryClient}></HydrationBoundary>; return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ArticleSyncPage articleId={articleId} />
</HydrationBoundary>
);
} }
+7 -4
View File
@@ -1,4 +1,4 @@
import { ArticlesPageHeader } from '@/components/pages/BlogPage/ArticlesPageHeader'; import { Title } from '@/ui/Title';
import { Metadata, ResolvingMetadata } from 'next'; import { Metadata, ResolvingMetadata } from 'next';
export async function generateMetadata( export async function generateMetadata(
@@ -16,9 +16,12 @@ export default async function BlogLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<section> <section className="space-y-12">
<ArticlesPageHeader /> <Title className="text-center row-start-1" headerLevel={2}>
{children} В блоге собраны все публикации о работе компании:
<span className="text-[#7A7A7A]"> новости, статьи и видео</span>
</Title>
<div className="grid grid-cols-6">{children}</div>
</section> </section>
); );
} }
+8 -2
View File
@@ -1,7 +1,12 @@
import { api } from '@/api'; import { api } from '@/api';
import { ArticlesList } from '@/components/pages/BlogPage/ArticlesList'; import { ArticlesList } from '@/components/pages/BlogPage/ArticlesList';
import { ArticlesPageActions } from '@/components/pages/BlogPage/ArticlesPageActions';
import { IArticle } from '@/types/IArticle'; import { IArticle } from '@/types/IArticle';
import { HydrationBoundary, QueryClient } from '@tanstack/react-query'; import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query';
import { Suspense } from 'react'; import { Suspense } from 'react';
export default async function BlogPage({ export default async function BlogPage({
@@ -19,7 +24,8 @@ export default async function BlogPage({
}); });
return ( return (
<HydrationBoundary queryClient={queryClient}> <HydrationBoundary state={dehydrate(queryClient)}>
<ArticlesPageActions tags={tags} />
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<div>Loading...</div>}>
<ArticlesList tags={tags} /> <ArticlesList tags={tags} />
</Suspense> </Suspense>
+1 -4
View File
@@ -7,10 +7,7 @@ import { usePathname, useRouter } from 'next/navigation';
import { UseFormSetValue } from 'react-hook-form'; import { UseFormSetValue } from 'react-hook-form';
import { DeleteIcon } from './icons/DeleteIcon'; import { DeleteIcon } from './icons/DeleteIcon';
import { EditIcon } from './icons/EditIcon'; import { EditIcon } from './icons/EditIcon';
import { import { ArticleFormModal, IArticleFormInput } from './modals/ArticleFormModal';
ArticleFormModal,
IArticleFormInput,
} from './modals/ArticleHeaderFormModal';
import { DeleteArticleModal } from './modals/DeleteArticleModal'; import { DeleteArticleModal } from './modals/DeleteArticleModal';
export function ArticleActions({ export function ArticleActions({
+1 -1
View File
@@ -10,7 +10,7 @@ import { EditIcon } from './icons/EditIcon';
import { PlusIcon } from './icons/PlusIcon'; import { PlusIcon } from './icons/PlusIcon';
import { TrashIcon } from './icons/TrashIcon'; import { TrashIcon } from './icons/TrashIcon';
import { ArticleContentEditorModal } from './modals/ArticleContentEditorModal'; import { ArticleContentEditorModal } from './modals/ArticleContentEditorModal';
import { IArticleFormInput } from './modals/ArticleHeaderFormModal'; import { IArticleFormInput } from './modals/ArticleFormModal';
interface IBlockActionsProps { interface IBlockActionsProps {
item: IContent & Record<'id', string>; item: IContent & Record<'id', string>;
+7 -3
View File
@@ -9,7 +9,7 @@ export function DeleteItemModal({
id, id,
}: { }: {
title: string; title: string;
entity: 'projects' | 'companies'; entity: 'projects' | 'companies' | 'articles';
id: string; id: string;
}) { }) {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
@@ -27,9 +27,13 @@ export function DeleteItemModal({
<CloseIcon /> <CloseIcon />
</button> </button>
</div> </div>
<Button onClick={remove} className="self-end text-white outline-none"> <Button onClick={remove} className="self-end text-white outline-none">
Удалить {entity === 'projects' ? 'проект' : 'компанию'} Удалить{' '}
{entity === 'projects'
? 'проект'
: entity === 'companies'
? 'компанию'
: 'статью'}
</Button> </Button>
</div> </div>
); );
+38 -8
View File
@@ -3,50 +3,80 @@
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper'; import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
import { useCheckAuthQuery } from '@/queries/checkAuth'; import { useCheckAuthQuery } from '@/queries/checkAuth';
import { useModalStore } from '@/stores/useModalStore'; import { useModalStore } from '@/stores/useModalStore';
import { IArticle } from '@/types/IArticle';
import { ICompany } from '@/types/ICompany'; import { ICompany } from '@/types/ICompany';
import { IProject } from '@/types/IProject'; import { IProject } from '@/types/IProject';
import { isCompany } from '@/utils/isCompany';
import { isProject } from '@/utils/isProject'; import { isProject } from '@/utils/isProject';
import { SyntheticEvent } from 'react';
import { DeleteItemModal } from './DeleteItemModal'; import { DeleteItemModal } from './DeleteItemModal';
import { DeleteIcon } from './icons/DeleteIcon'; import { DeleteIcon } from './icons/DeleteIcon';
import { EditIcon } from './icons/EditIcon'; import { EditIcon } from './icons/EditIcon';
import { ArticleFormModal } from './modals/ArticleFormModal';
import { CompanyFormModal } from './modals/CompanyFormModal'; import { CompanyFormModal } from './modals/CompanyFormModal';
import { ProjectFormModal } from './modals/ProjectFormModal'; import { ProjectFormModal } from './modals/ProjectFormModal';
export function ItemActions({ item }: { item: IProject | ICompany }) { export function ItemActions({
item,
}: {
item: IProject | ICompany | IArticle;
}) {
const { data: auth } = useCheckAuthQuery(); const { data: auth } = useCheckAuthQuery();
const { setModal } = useModalStore(); const { setModal } = useModalStore();
function handleEdit() { function handleEdit(e: SyntheticEvent) {
e.stopPropagation();
if (isProject(item)) { if (isProject(item)) {
const { company, ...project } = item; const { company, ...project } = item;
setModal( setModal(
<ProjectFormModal action="edit" defaultValues={project} />, <ProjectFormModal action="edit" defaultValues={project} />,
'editProjectForm' 'editProjectForm'
); );
} else { } else if (isCompany(item)) {
const { projects, ...company } = item; const { projects, ...company } = item;
setModal( setModal(
<CompanyFormModal action="edit" defaultValues={company} />, <CompanyFormModal action="edit" defaultValues={company} />,
'editCompanyForm' 'editCompanyForm'
); );
} else {
setModal(
<ArticleFormModal action="edit" defaultValues={item} />,
'editArticleForm'
);
} }
} }
function handleDelete() { function handleDelete(e: SyntheticEvent) {
e.stopPropagation();
setModal( setModal(
<DeleteItemModal <DeleteItemModal
id={item.id} id={item.id}
title={'Удаление ' + (isProject(item) ? 'проекта' : 'компании')} title={
entity={isProject(item) ? 'projects' : 'companies'} 'Удаление ' +
(isProject(item)
? 'проекта'
: isCompany(item)
? 'компании'
: 'статьи')
}
entity={
isProject(item)
? 'projects'
: isCompany(item)
? 'companies'
: 'articles'
}
/>, />,
`delete${isProject(item) ? 'Project' : 'Company'}` `delete${
isProject(item) ? 'Project' : isCompany(item) ? 'Company' : 'Article'
}`
); );
} }
return ( return (
auth && ( auth && (
<div className="absolute top-0 left-0 p-4 flex opacity-0 gap-1 z-[5] group-hover:opacity-100 transition-opacity"> <div className="absolute top-0 left-0 p-4 flex opacity-0 gap-1 z-[6] group-hover:opacity-100 transition-opacity">
<button <button
onClick={handleEdit} onClick={handleEdit}
className="relative px-3 py-2 bg-[#37393B99] backdrop-blur-sm rounded-full outline-none" className="relative px-3 py-2 bg-[#37393B99] backdrop-blur-sm rounded-full outline-none"
@@ -10,7 +10,7 @@ import {
UseFormWatch, UseFormWatch,
} from 'react-hook-form'; } from 'react-hook-form';
import { BlockActions } from '../BlockActions'; import { BlockActions } from '../BlockActions';
import { IArticleFormInput } from '../modals/ArticleHeaderFormModal'; import { IArticleFormInput } from '../modals/ArticleFormModal';
export interface IArticleContentInputProps { export interface IArticleContentInputProps {
item: IContent & Record<'id', string>; item: IContent & Record<'id', string>;
@@ -3,7 +3,7 @@ import { SyntheticEvent } from 'react';
import { UseFieldArrayRemove, UseFormSetValue } from 'react-hook-form'; import { UseFieldArrayRemove, UseFormSetValue } from 'react-hook-form';
import { CloseIcon } from '../icons/CloseIcon'; import { CloseIcon } from '../icons/CloseIcon';
import { MediaUploader } from '../MediaUploader'; import { MediaUploader } from '../MediaUploader';
import { IArticleFormInput } from '../modals/ArticleHeaderFormModal'; import { IArticleFormInput } from '../modals/ArticleFormModal';
interface IArticleSliderImageInputProps { interface IArticleSliderImageInputProps {
setValue: UseFormSetValue<IArticleFormInput>; setValue: UseFormSetValue<IArticleFormInput>;
@@ -10,7 +10,7 @@ import {
} from 'react-hook-form'; } from 'react-hook-form';
import { PlusIcon } from '../icons/PlusIcon'; import { PlusIcon } from '../icons/PlusIcon';
import { TrashIcon } from '../icons/TrashIcon'; import { TrashIcon } from '../icons/TrashIcon';
import { IArticleFormInput } from '../modals/ArticleHeaderFormModal'; import { IArticleFormInput } from '../modals/ArticleFormModal';
import { ArticleSliderImageInput } from './ArticleSliderImageInput'; import { ArticleSliderImageInput } from './ArticleSliderImageInput';
interface IArticleSliderInputProps { interface IArticleSliderInputProps {
@@ -6,22 +6,18 @@ import { useRef } from 'react';
import { Control, Controller } from 'react-hook-form'; import { Control, Controller } from 'react-hook-form';
import { Editor } from 'tinymce'; import { Editor } from 'tinymce';
import { EditIcon } from '../icons/EditIcon'; import { EditIcon } from '../icons/EditIcon';
import { IArticleFormInput } from './ArticleHeaderFormModal';
export default function ArticleContentFormModal({ export default function ArticleContentFormModal({
drafted,
cardImage,
createdAt,
id,
posterImage, posterImage,
tags, tags,
title, title,
control, control,
}: IArticleFormInput & { control: Control<IArticle> }) { blocks,
}: IArticle & { control: Control<IArticle> }) {
const ref = useRef<Editor | null>(null); const ref = useRef<Editor | null>(null);
return ( return (
<div className="relative space-y-4 bg-[#232425] rounded-[28px] top-5 w-[calc(954/1440*100vw)] max-h-[calc(100vh-40px)] pl-[75px] pr-[55px] overflow-y-auto"> <div className="relative space-y-4 bg-[#232425] rounded-[28px] top-5 w-[calc(954/1440*100vw)] max-h-[calc(100vh-40px)] z-[10] pl-[75px] pr-[55px] overflow-y-auto">
<Button <Button
className="absolute top-3 right-4 bg-[#37393B99] backdrop-blur-sm p-4 btnm" className="absolute top-3 right-4 bg-[#37393B99] backdrop-blur-sm p-4 btnm"
color="secondary" color="secondary"
@@ -51,16 +47,17 @@ export default function ArticleContentFormModal({
</div> </div>
</div> </div>
<div className="pt-10"> <div className="pt-10">
{blocks.map((block, index) => (
<Controller <Controller
name="blocks" key={index}
name={`blocks.${index}`}
defaultValue={block}
control={control} control={control}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<BundledEditor <BundledEditor
onChange={onChange} onChange={onChange}
value={ value={
value[0].type === 'Content' value.type === 'Content' ? value.content : value.images[0].img
? value[0].content
: value[0].images[0].img
} }
onInit={(_, editor) => { onInit={(_, editor) => {
ref.current = editor; ref.current = editor;
@@ -72,6 +69,7 @@ export default function ArticleContentFormModal({
/> />
)} )}
/> />
))}
</div> </div>
</div> </div>
); );
@@ -10,8 +10,6 @@ import { MediaUploader } from '../MediaUploader';
import ArticleContentFormModal from './ArticleContentFormModal'; import ArticleContentFormModal from './ArticleContentFormModal';
import { FormModalHeader } from './FormModalHeader'; import { FormModalHeader } from './FormModalHeader';
export interface IArticleFormInput extends Omit<IArticle, 'blocks'> {}
interface IArticleFormModalProps<TAction extends 'create' | 'edit'> { interface IArticleFormModalProps<TAction extends 'create' | 'edit'> {
action: TAction; action: TAction;
defaultValues?: TAction extends 'edit' ? IArticle : undefined; defaultValues?: TAction extends 'edit' ? IArticle : undefined;
@@ -25,11 +23,14 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
const { handleSubmit, register, setValue, getValues, control } = const { handleSubmit, register, setValue, getValues, control } =
useForm<IArticle>({ useForm<IArticle>({
defaultValues: { ...defaultValues, blocks: [], drafted: true }, defaultValues: { ...defaultValues, blocks: [], tags: [], drafted: true },
}); });
async function onSubmit(data: IArticle) { async function onSubmit(data: IArticle) {
const article = await mutateAsync(data); const article = await mutateAsync({
...data,
blocks: JSON.stringify(data.blocks),
});
setModal( setModal(
<ArticleContentFormModal {...article} control={control} />, <ArticleContentFormModal {...article} control={control} />,
'articleContentFormModal' 'articleContentFormModal'
@@ -1,5 +1,41 @@
'use client'; 'use client';
export function ArticleSyncPage() { import { useGetArticleById } from '@/queries/getArticleById';
return <div></div>; import Image from 'next/image';
export function ArticleSyncPage({ articleId }: { articleId: string }) {
const { data: article } = useGetArticleById(articleId);
if (!article) return null;
return (
<div className="relative min-h-[calc(100vh-40px)] lg:w-[calc(954/1440*100vw)] top-5 rounded-[28px] bg-[#232425] m-auto overflow-hidden">
<div className="w-full relative">
<div className="relative w-full h-full">
<Image
className="!relative object-cover lg:max-h-[calc(261/731*(100vh-40px))]"
src={process.env.NEXT_PUBLIC_S3_BUCKET + article.posterImage}
fill
alt={article.title}
sizes="100%"
/>
</div>
<div className="absolute top-0 w-full h-full bg-[linear-gradient(to_bottom,#00000000,#00000099)] bg-no-repeat" />
<div className="flex justify-between gap-10 absolute bottom-6 left-[75px] right-[75px]">
<p className="heading1 font-medium">{article.title}</p>
<div className="flex flex-wrap gap-1">
{article.tags.map((tag) => (
<div
key={tag}
className="bg-[#37393B99] rounded-[17px] px-3 py-2 btns font-medium"
>
{tag}
</div>
))}
</div>
</div>
</div>
<div className=""></div>
</div>
);
} }
+34 -9
View File
@@ -1,34 +1,47 @@
'use client'; 'use client';
import { ItemActions } from '@/components/ItemActions';
import { useCheckAuthQuery } from '@/queries/checkAuth';
import { IArticle } from '@/types/IArticle'; import { IArticle } from '@/types/IArticle';
import { PostTag } from '@/ui/PostTag'; import { PostTag } from '@/ui/PostTag';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation';
import { useSearchParams } from 'next/navigation';
export function ArticleCard({ export function ArticleCard({
id, id,
title, title,
cardImage, cardImage,
tags, tags,
blocks,
drafted,
posterImage,
createdAt,
className, className,
}: IArticle & { className?: string }) { }: IArticle & { className?: string }) {
const params = useSearchParams(); const params = useSearchParams();
const { push } = useRouter();
const { data: auth } = useCheckAuthQuery();
return ( return (
<Link <div
href={`/blog/${id}`} onClick={(e) => push('/blog/' + id)}
className={`relative space-y-3${ className={`space-y-3${
className ? ' ' + className : '' className ? ' ' + className : ''
} hover:backdrop-blur-[500px] hover:bg-[radial-gradient(ellipse_at_bottom,#7A7A7A,transparent)] bg-cover rounded-2xl relative`} } hover:backdrop-blur-[500px] cursor-pointer group hover:bg-[radial-gradient(ellipse_at_bottom,#7A7A7A99,transparent)] transition-[background-size] bg-[length:100%_0%] bg-bottom hover:bg-[length:100%_100%] bg-no-repeat rounded-2xl p-3 relative h-full`}
> >
<div className="relative">
<Image <Image
src={process.env.NEXT_PUBLIC_S3_BUCKET + cardImage} src={process.env.NEXT_PUBLIC_S3_BUCKET + cardImage}
alt={title} alt={title}
fill fill
className="!relative object-cover" className="!relative object-cover rounded-xl z-[1]"
priority
/> />
{auth && (
<div className="absolute top-0 left-0 w-full h-full z-[2] group-hover:block hidden bg-[#0F1011] bg-opacity-40 rounded-2xl" />
)}
</div>
<p className="heading1 font-medium">{title}</p> <p className="heading1 font-medium">{title}</p>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{tags.map((tag) => ( {tags.map((tag) => (
@@ -39,6 +52,18 @@ export function ArticleCard({
/> />
))} ))}
</div> </div>
</Link> <ItemActions
item={{
id,
title,
cardImage,
tags,
blocks,
drafted,
posterImage,
createdAt,
}}
/>
</div>
); );
} }
+23 -5
View File
@@ -1,17 +1,35 @@
'use client'; 'use client';
import { useGetArticlesQuery } from '@/queries/getArticles'; import { useGetArticlesQuery } from '@/queries/getArticles';
import { TagsFilters } from '../ProjectsPage/TagsFilters'; import { useGetDraftedArticlesQuery } from '@/queries/getDraftedArticles';
import { ArticleCard } from './ArticleCard'; import { ArticleCard } from './ArticleCard';
export function ArticlesList({ tags }: { tags: string[] }) { export function ArticlesList({ tags }: { tags: string[] }) {
const { data: articles } = useGetArticlesQuery(tags); const { data: articles } = useGetArticlesQuery(tags);
const { data: drafted } = useGetDraftedArticlesQuery(tags);
return ( return (
<div className="grid lg:grid-cols-[1fr_4fr_1fr] items-start gap-x-[34px] relative"> <div className="space-y-6 col-span-4">
<TagsFilters type="article" tags={tags} /> {drafted && drafted.length > 0 ? (
<div className="mt-12"> <div className="space-y-2">
<p className="text-[#7A7A7A] text1">Черновики</p>
<div className="gap-x-3 gap-y-6 flex flex-wrap col-start-2">
{drafted?.map((article, index) => (
<ArticleCard
key={article.id}
{...article}
className={index % 5 < 4 ? 'lg:w-1/3 sm:w-1/2' : 'sm:w-1/2'}
/>
))}
</div>
</div>
) : (
<></>
)}
{articles && articles.length > 0 ? ( {articles && articles.length > 0 ? (
<div className="space-y-2 pt-2">
<p className="text-[#7A7A7A] text1">Опубликованое</p>
<div className="gap-x-3 gap-y-6 flex flex-wrap col-start-2"> <div className="gap-x-3 gap-y-6 flex flex-wrap col-start-2">
{articles?.map((article, index) => ( {articles?.map((article, index) => (
<ArticleCard <ArticleCard
@@ -25,10 +43,10 @@ export function ArticlesList({ tags }: { tags: string[] }) {
/> />
))} ))}
</div> </div>
</div>
) : ( ) : (
<p className="heading1 font-medium text-center">Статьи не найдены</p> <p className="heading1 font-medium text-center">Статьи не найдены</p>
)} )}
</div> </div>
</div>
); );
} }
@@ -0,0 +1,75 @@
'use client';
import { PlusIcon } from '@/components/icons/PlusIcon';
import { ArticleFormModal } from '@/components/modals/ArticleFormModal';
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
import { OpenFormModalWrapper } from '@/hocs/OpenFormModalWrapper';
import { useCheckAuthQuery } from '@/queries/checkAuth';
import { useGetDraftedArticlesQuery } from '@/queries/getDraftedArticles';
import { IArticle } from '@/types/IArticle';
import { Button } from '@/ui/Button';
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { TagsFilters } from '../ProjectsPage/TagsFilters';
export function ArticlesPageActions({ tags }: { tags: string[] }) {
const { data: drafts, refetch } = useGetDraftedArticlesQuery(tags);
const { data: auth } = useCheckAuthQuery();
const queryClient = useQueryClient();
const [show, setShow] = useState(false);
const [hiddenDrafts, setDraftedArticles] = useState<IArticle[]>();
useEffect(() => {
setShow(!!drafts);
}, [drafts]);
function handleShowDrafted() {
if (!drafts) {
refetch();
queryClient.setQueryData(['articles', 'drafted', tags], hiddenDrafts);
setDraftedArticles([]);
} else {
setDraftedArticles(drafts);
queryClient.setQueryData(['articles', 'drafted', tags], []);
}
}
return (
<div className="sticky top-12 bottom-5 flex flex-col justify-between gap-y-4 self-stretch">
<div className="space-y-2">
<OpenFormModalWrapper
modalName="addArticle"
modal={<ArticleFormModal action="create" />}
>
<Button
className="px-3 py-2 outline-none btns"
rounded="xl"
icon={
<ClassNameWrapper className="w-4 h-4">
<PlusIcon />
</ClassNameWrapper>
}
>
Добавить статью
</Button>
</OpenFormModalWrapper>
{auth && (
<Button
color="secondary"
className={`active:bg-white active:text-black px-3 transition-colors py-2 btns ${
show ? 'bg-white text-black' : 'bg-[#37393B99]'
}`}
rounded="xl"
onClick={handleShowDrafted}
>
Показать черновики
</Button>
)}
</div>
<TagsFilters type="article" tags={tags} />
</div>
);
}
@@ -1,64 +0,0 @@
'use client';
import { PlusIcon } from '@/components/icons/PlusIcon';
import { ArticleFormModal } from '@/components/modals/ArticleHeaderFormModal';
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
import { OpenFormModalWrapper } from '@/hocs/OpenFormModalWrapper';
import { useCheckAuthQuery } from '@/queries/checkAuth';
import { IArticle } from '@/types/IArticle';
import { Button } from '@/ui/Button';
import { Title } from '@/ui/Title';
import { useQueryClient } from '@tanstack/react-query';
export function ArticlesPageHeader() {
const auth = useCheckAuthQuery();
const queryClient = useQueryClient();
return (
<div className="lg:space-y-14 sm:space-y-8 space-y-10 relative">
<Title className="text-center" headerLevel={2}>
В блоге собраны все публикации о работе компании:
<span className="text-[#7A7A7A]"> новости, статьи и видео</span>
</Title>
<div className="space-y-2">
<OpenFormModalWrapper
modalName="addArticle"
className="sticky top-0"
modal={<ArticleFormModal action="create" />}
>
<Button
className="px-3 py-2 outline-none btns"
rounded="xl"
icon={
<ClassNameWrapper className="w-4 h-4">
<PlusIcon />
</ClassNameWrapper>
}
>
Добавить статью
</Button>
</OpenFormModalWrapper>
{auth && (
<Button
color="secondary"
className="bg-[#37393B99] active:bg-white active:text-black px-3 py-2 btns"
rounded="xl"
onClick={() => {
queryClient.setQueryData(['articles'], () =>
queryClient.setQueryData<IArticle[]>(
['articles'],
queryClient
.getQueryData<IArticle[]>(['articles'])
?.filter((article) => article.drafted)
)
);
}}
>
Показать черновики
</Button>
)}
</div>
</div>
);
}
@@ -39,7 +39,7 @@ export function TagsFilters({
return ( return (
<> <>
<div className="space-y-2 sticky left-5 bottom-6 h-fit max-lg:hidden row-start-1 self-end"> <div className="space-y-2 max-lg:hidden">
{['Все', ...(type === 'project' ? projectsTags : postTags)].map( {['Все', ...(type === 'project' ? projectsTags : postTags)].map(
(tag) => ( (tag) => (
<TagFilter <TagFilter
+1 -1
View File
@@ -9,7 +9,7 @@ export function useArticleMutation({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async (json: IArticle) => mutationFn: async (json: Omit<IArticle, 'blocks'> & { blocks: string }) =>
action === 'create' action === 'create'
? await api.post('articles', { json }).json<IArticle>() ? await api.post('articles', { json }).json<IArticle>()
: await api.put(`articles/${id}`, { json }).json<IArticle>(), : await api.put(`articles/${id}`, { json }).json<IArticle>(),
+6 -4
View File
@@ -1,10 +1,9 @@
import { api } from '@/api'; import { api } from '@/api';
import { useModalStore } from '@/stores/useModalStore'; import { useModalStore } from '@/stores/useModalStore';
import { IProject } from '@/types/IProject';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useDeleteMutation( export function useDeleteMutation(
entity: 'projects' | 'companies', entity: 'projects' | 'companies' | 'articles',
id: string id: string
) { ) {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
@@ -12,10 +11,13 @@ export function useDeleteMutation(
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { mutateAsync: remove } = useMutation({ const { mutateAsync: remove } = useMutation({
mutationFn: async () => mutationFn: async () => await api.delete(`${entity}/${id}`).json(),
await api.delete(`${entity}/${id}`).json<IProject>(),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [entity] }); queryClient.invalidateQueries({ queryKey: [entity] });
if (entity === 'articles')
queryClient.setQueryData(['articles', 'drafted', []], []);
setModal(null, ''); setModal(null, '');
}, },
}); });
+5 -1
View File
@@ -1,6 +1,7 @@
'use client'; 'use client';
import { QueryClientProvider } from '@tanstack/react-query'; import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react'; import { useState } from 'react';
import { getQueryClient } from './queryClient'; import { getQueryClient } from './queryClient';
@@ -8,6 +9,9 @@ export const Providers = ({ children }: { children: React.ReactNode }) => {
const [queryClient] = useState(getQueryClient); const [queryClient] = useState(getQueryClient);
return ( return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider> <QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools />
</QueryClientProvider>
); );
}; };
+20
View File
@@ -0,0 +1,20 @@
import { api } from '@/api';
import { IArticle } from '@/types/IArticle';
import { useQuery } from '@tanstack/react-query';
export function useGetDraftedArticlesQuery(tags: string | string[] = []) {
return useQuery({
enabled: false,
queryKey: ['articles', 'drafted', tags],
queryFn: async () =>
await api
.get(
`articles/drafted?${
Array.isArray(tags)
? tags.map((tag) => `tags=${tag}`).join('&')
: 'tags=' + tags
}`
)
.json<IArticle[]>(),
});
}
+9
View File
@@ -0,0 +1,9 @@
import { IArticle } from '@/types/IArticle';
import { ICompany } from '@/types/ICompany';
import { IProject } from '@/types/IProject';
export function isCompany(
item: IProject | ICompany | IArticle
): item is ICompany {
return 'color' in item;
}
+4 -1
View File
@@ -1,6 +1,9 @@
import { IArticle } from '@/types/IArticle';
import { ICompany } from '@/types/ICompany'; import { ICompany } from '@/types/ICompany';
import { IProject } from '@/types/IProject'; import { IProject } from '@/types/IProject';
export function isProject(item: IProject | ICompany): item is IProject { export function isProject(
item: IProject | ICompany | IArticle
): item is IProject {
return 'image' in item; return 'image' in item;
} }
+12
View File
@@ -335,6 +335,18 @@
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.62.7.tgz#c7f6d0131c08cd2f60e73ec6e7b70e2e9e335def" resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.62.7.tgz#c7f6d0131c08cd2f60e73ec6e7b70e2e9e335def"
integrity sha512-fgpfmwatsrUal6V+8EC2cxZIQVl9xvL7qYa03gsdsCy985UTUlS4N+/3hCzwR0PclYDqisca2AqR1BVgJGpUDA== integrity sha512-fgpfmwatsrUal6V+8EC2cxZIQVl9xvL7qYa03gsdsCy985UTUlS4N+/3hCzwR0PclYDqisca2AqR1BVgJGpUDA==
"@tanstack/query-devtools@5.64.2":
version "5.64.2"
resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.64.2.tgz#3d8f8abb17815a7302482b67713cb933c79ed6ba"
integrity sha512-3DautR5UpVZdk/qNIhioZVF7g8fdQZ1U98sBEEk4Tzz3tihSBNMPgwlP40nzgbPEDBIrn/j/oyyvNBVSo083Vw==
"@tanstack/react-query-devtools@^5.64.2":
version "5.64.2"
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.64.2.tgz#ece61dfad8032305aefd3f0eb044ccd8304ffa1b"
integrity sha512-+ZjJVnPzc8BUV/Eklu2k9T/IAyAyvwoCHqOaOrk2sbU33LFhM52BpX4eyENXn0bx5LwV3DJZgEQlIzucoemfGQ==
dependencies:
"@tanstack/query-devtools" "5.64.2"
"@tanstack/react-query@^5.62.7": "@tanstack/react-query@^5.62.7":
version "5.62.7" version "5.62.7"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.62.7.tgz#8f253439a38ad6ce820bc6d42d89ca2556574d1a" resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.62.7.tgz#8f253439a38ad6ce820bc6d42d89ca2556574d1a"