upd
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.62.7",
|
||||
"@tanstack/react-query-devtools": "^5.64.2",
|
||||
"@tinymce/tinymce-react": "^5.1.1",
|
||||
"countries-phone-masks": "^1.1.0",
|
||||
"date-fns": "^3.6.0",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ArticleActions } from '@/components/ArticleActions';
|
||||
import { ArticleHeader } from '@/components/ArticleHeader';
|
||||
import { ArticleContentInput } from '@/components/articleInputs/ArticleContentInput';
|
||||
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 { IArticle } from '@/types/IArticle';
|
||||
import { Button } from '@/ui/Button';
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { api } from '@/api';
|
||||
import { CloseIcon } from '@/components/icons/CloseIcon';
|
||||
import { RelevantArticlesPreview } from '@/components/pages/ArticlePage/RelevantArticlesPreview';
|
||||
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
|
||||
import { IArticle } from '@/types/IArticle';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -41,10 +44,19 @@ export default async function Layout({
|
||||
params: Promise<{ articleId: string }>;
|
||||
}) {
|
||||
const { articleId } = await params;
|
||||
|
||||
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} />
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { api } from '@/api';
|
||||
import { ArticleSyncPage } from '@/components/pages/ArticlePage/ArticleSyncPage';
|
||||
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({
|
||||
params: { articleId },
|
||||
@@ -15,5 +20,9 @@ export default async function ArticlePage({
|
||||
await api.get(`articles/${articleId}`).json<IArticle>(),
|
||||
});
|
||||
|
||||
return <HydrationBoundary queryClient={queryClient}></HydrationBoundary>;
|
||||
return (
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<ArticleSyncPage articleId={articleId} />
|
||||
</HydrationBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArticlesPageHeader } from '@/components/pages/BlogPage/ArticlesPageHeader';
|
||||
import { Title } from '@/ui/Title';
|
||||
import { Metadata, ResolvingMetadata } from 'next';
|
||||
|
||||
export async function generateMetadata(
|
||||
@@ -16,9 +16,12 @@ export default async function BlogLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
<ArticlesPageHeader />
|
||||
{children}
|
||||
<section className="space-y-12">
|
||||
<Title className="text-center row-start-1" headerLevel={2}>
|
||||
В блоге собраны все публикации о работе компании:
|
||||
<span className="text-[#7A7A7A]"> новости, статьи и видео</span>
|
||||
</Title>
|
||||
<div className="grid grid-cols-6">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { api } from '@/api';
|
||||
import { ArticlesList } from '@/components/pages/BlogPage/ArticlesList';
|
||||
import { ArticlesPageActions } from '@/components/pages/BlogPage/ArticlesPageActions';
|
||||
import { IArticle } from '@/types/IArticle';
|
||||
import { HydrationBoundary, QueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
dehydrate,
|
||||
HydrationBoundary,
|
||||
QueryClient,
|
||||
} from '@tanstack/react-query';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
export default async function BlogPage({
|
||||
@@ -19,7 +24,8 @@ export default async function BlogPage({
|
||||
});
|
||||
|
||||
return (
|
||||
<HydrationBoundary queryClient={queryClient}>
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<ArticlesPageActions tags={tags} />
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ArticlesList tags={tags} />
|
||||
</Suspense>
|
||||
|
||||
@@ -7,10 +7,7 @@ import { usePathname, useRouter } from 'next/navigation';
|
||||
import { UseFormSetValue } from 'react-hook-form';
|
||||
import { DeleteIcon } from './icons/DeleteIcon';
|
||||
import { EditIcon } from './icons/EditIcon';
|
||||
import {
|
||||
ArticleFormModal,
|
||||
IArticleFormInput,
|
||||
} from './modals/ArticleHeaderFormModal';
|
||||
import { ArticleFormModal, IArticleFormInput } from './modals/ArticleFormModal';
|
||||
import { DeleteArticleModal } from './modals/DeleteArticleModal';
|
||||
|
||||
export function ArticleActions({
|
||||
|
||||
@@ -10,7 +10,7 @@ import { EditIcon } from './icons/EditIcon';
|
||||
import { PlusIcon } from './icons/PlusIcon';
|
||||
import { TrashIcon } from './icons/TrashIcon';
|
||||
import { ArticleContentEditorModal } from './modals/ArticleContentEditorModal';
|
||||
import { IArticleFormInput } from './modals/ArticleHeaderFormModal';
|
||||
import { IArticleFormInput } from './modals/ArticleFormModal';
|
||||
|
||||
interface IBlockActionsProps {
|
||||
item: IContent & Record<'id', string>;
|
||||
|
||||
@@ -9,7 +9,7 @@ export function DeleteItemModal({
|
||||
id,
|
||||
}: {
|
||||
title: string;
|
||||
entity: 'projects' | 'companies';
|
||||
entity: 'projects' | 'companies' | 'articles';
|
||||
id: string;
|
||||
}) {
|
||||
const { setModal } = useModalStore();
|
||||
@@ -27,9 +27,13 @@ export function DeleteItemModal({
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button onClick={remove} className="self-end text-white outline-none">
|
||||
Удалить {entity === 'projects' ? 'проект' : 'компанию'}
|
||||
Удалить{' '}
|
||||
{entity === 'projects'
|
||||
? 'проект'
|
||||
: entity === 'companies'
|
||||
? 'компанию'
|
||||
: 'статью'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,50 +3,80 @@
|
||||
import { ClassNameWrapper } from '@/hocs/ClassNameWrapper';
|
||||
import { useCheckAuthQuery } from '@/queries/checkAuth';
|
||||
import { useModalStore } from '@/stores/useModalStore';
|
||||
import { IArticle } from '@/types/IArticle';
|
||||
import { ICompany } from '@/types/ICompany';
|
||||
import { IProject } from '@/types/IProject';
|
||||
import { isCompany } from '@/utils/isCompany';
|
||||
import { isProject } from '@/utils/isProject';
|
||||
import { SyntheticEvent } from 'react';
|
||||
import { DeleteItemModal } from './DeleteItemModal';
|
||||
import { DeleteIcon } from './icons/DeleteIcon';
|
||||
import { EditIcon } from './icons/EditIcon';
|
||||
import { ArticleFormModal } from './modals/ArticleFormModal';
|
||||
import { CompanyFormModal } from './modals/CompanyFormModal';
|
||||
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 { setModal } = useModalStore();
|
||||
|
||||
function handleEdit() {
|
||||
function handleEdit(e: SyntheticEvent) {
|
||||
e.stopPropagation();
|
||||
if (isProject(item)) {
|
||||
const { company, ...project } = item;
|
||||
setModal(
|
||||
<ProjectFormModal action="edit" defaultValues={project} />,
|
||||
'editProjectForm'
|
||||
);
|
||||
} else {
|
||||
} else if (isCompany(item)) {
|
||||
const { projects, ...company } = item;
|
||||
setModal(
|
||||
<CompanyFormModal action="edit" defaultValues={company} />,
|
||||
'editCompanyForm'
|
||||
);
|
||||
} else {
|
||||
setModal(
|
||||
<ArticleFormModal action="edit" defaultValues={item} />,
|
||||
'editArticleForm'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
function handleDelete(e: SyntheticEvent) {
|
||||
e.stopPropagation();
|
||||
setModal(
|
||||
<DeleteItemModal
|
||||
id={item.id}
|
||||
title={'Удаление ' + (isProject(item) ? 'проекта' : 'компании')}
|
||||
entity={isProject(item) ? 'projects' : 'companies'}
|
||||
title={
|
||||
'Удаление ' +
|
||||
(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 (
|
||||
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
|
||||
onClick={handleEdit}
|
||||
className="relative px-3 py-2 bg-[#37393B99] backdrop-blur-sm rounded-full outline-none"
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
UseFormWatch,
|
||||
} from 'react-hook-form';
|
||||
import { BlockActions } from '../BlockActions';
|
||||
import { IArticleFormInput } from '../modals/ArticleHeaderFormModal';
|
||||
import { IArticleFormInput } from '../modals/ArticleFormModal';
|
||||
|
||||
export interface IArticleContentInputProps {
|
||||
item: IContent & Record<'id', string>;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SyntheticEvent } from 'react';
|
||||
import { UseFieldArrayRemove, UseFormSetValue } from 'react-hook-form';
|
||||
import { CloseIcon } from '../icons/CloseIcon';
|
||||
import { MediaUploader } from '../MediaUploader';
|
||||
import { IArticleFormInput } from '../modals/ArticleHeaderFormModal';
|
||||
import { IArticleFormInput } from '../modals/ArticleFormModal';
|
||||
|
||||
interface IArticleSliderImageInputProps {
|
||||
setValue: UseFormSetValue<IArticleFormInput>;
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'react-hook-form';
|
||||
import { PlusIcon } from '../icons/PlusIcon';
|
||||
import { TrashIcon } from '../icons/TrashIcon';
|
||||
import { IArticleFormInput } from '../modals/ArticleHeaderFormModal';
|
||||
import { IArticleFormInput } from '../modals/ArticleFormModal';
|
||||
import { ArticleSliderImageInput } from './ArticleSliderImageInput';
|
||||
|
||||
interface IArticleSliderInputProps {
|
||||
|
||||
@@ -6,22 +6,18 @@ import { useRef } from 'react';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import { Editor } from 'tinymce';
|
||||
import { EditIcon } from '../icons/EditIcon';
|
||||
import { IArticleFormInput } from './ArticleHeaderFormModal';
|
||||
|
||||
export default function ArticleContentFormModal({
|
||||
drafted,
|
||||
cardImage,
|
||||
createdAt,
|
||||
id,
|
||||
posterImage,
|
||||
tags,
|
||||
title,
|
||||
control,
|
||||
}: IArticleFormInput & { control: Control<IArticle> }) {
|
||||
blocks,
|
||||
}: IArticle & { control: Control<IArticle> }) {
|
||||
const ref = useRef<Editor | null>(null);
|
||||
|
||||
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
|
||||
className="absolute top-3 right-4 bg-[#37393B99] backdrop-blur-sm p-4 btnm"
|
||||
color="secondary"
|
||||
@@ -51,16 +47,17 @@ export default function ArticleContentFormModal({
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-10">
|
||||
{blocks.map((block, index) => (
|
||||
<Controller
|
||||
name="blocks"
|
||||
key={index}
|
||||
name={`blocks.${index}`}
|
||||
defaultValue={block}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<BundledEditor
|
||||
onChange={onChange}
|
||||
value={
|
||||
value[0].type === 'Content'
|
||||
? value[0].content
|
||||
: value[0].images[0].img
|
||||
value.type === 'Content' ? value.content : value.images[0].img
|
||||
}
|
||||
onInit={(_, editor) => {
|
||||
ref.current = editor;
|
||||
@@ -72,6 +69,7 @@ export default function ArticleContentFormModal({
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+5
-4
@@ -10,8 +10,6 @@ import { MediaUploader } from '../MediaUploader';
|
||||
import ArticleContentFormModal from './ArticleContentFormModal';
|
||||
import { FormModalHeader } from './FormModalHeader';
|
||||
|
||||
export interface IArticleFormInput extends Omit<IArticle, 'blocks'> {}
|
||||
|
||||
interface IArticleFormModalProps<TAction extends 'create' | 'edit'> {
|
||||
action: TAction;
|
||||
defaultValues?: TAction extends 'edit' ? IArticle : undefined;
|
||||
@@ -25,11 +23,14 @@ export function ArticleFormModal<TAction extends 'create' | 'edit'>({
|
||||
|
||||
const { handleSubmit, register, setValue, getValues, control } =
|
||||
useForm<IArticle>({
|
||||
defaultValues: { ...defaultValues, blocks: [], drafted: true },
|
||||
defaultValues: { ...defaultValues, blocks: [], tags: [], drafted: true },
|
||||
});
|
||||
|
||||
async function onSubmit(data: IArticle) {
|
||||
const article = await mutateAsync(data);
|
||||
const article = await mutateAsync({
|
||||
...data,
|
||||
blocks: JSON.stringify(data.blocks),
|
||||
});
|
||||
setModal(
|
||||
<ArticleContentFormModal {...article} control={control} />,
|
||||
'articleContentFormModal'
|
||||
@@ -1,5 +1,41 @@
|
||||
'use client';
|
||||
|
||||
export function ArticleSyncPage() {
|
||||
return <div></div>;
|
||||
import { useGetArticleById } from '@/queries/getArticleById';
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { ItemActions } from '@/components/ItemActions';
|
||||
import { useCheckAuthQuery } from '@/queries/checkAuth';
|
||||
import { IArticle } from '@/types/IArticle';
|
||||
import { PostTag } from '@/ui/PostTag';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
export function ArticleCard({
|
||||
id,
|
||||
title,
|
||||
cardImage,
|
||||
tags,
|
||||
blocks,
|
||||
drafted,
|
||||
posterImage,
|
||||
createdAt,
|
||||
className,
|
||||
}: IArticle & { className?: string }) {
|
||||
const params = useSearchParams();
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
const { data: auth } = useCheckAuthQuery();
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/blog/${id}`}
|
||||
className={`relative space-y-3${
|
||||
<div
|
||||
onClick={(e) => push('/blog/' + id)}
|
||||
className={`space-y-3${
|
||||
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
|
||||
src={process.env.NEXT_PUBLIC_S3_BUCKET + cardImage}
|
||||
alt={title}
|
||||
fill
|
||||
className="!relative object-cover"
|
||||
priority
|
||||
className="!relative object-cover rounded-xl z-[1]"
|
||||
/>
|
||||
{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>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{tags.map((tag) => (
|
||||
@@ -39,6 +52,18 @@ export function ArticleCard({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
<ItemActions
|
||||
item={{
|
||||
id,
|
||||
title,
|
||||
cardImage,
|
||||
tags,
|
||||
blocks,
|
||||
drafted,
|
||||
posterImage,
|
||||
createdAt,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { useGetArticlesQuery } from '@/queries/getArticles';
|
||||
import { TagsFilters } from '../ProjectsPage/TagsFilters';
|
||||
import { useGetDraftedArticlesQuery } from '@/queries/getDraftedArticles';
|
||||
import { ArticleCard } from './ArticleCard';
|
||||
|
||||
export function ArticlesList({ tags }: { tags: string[] }) {
|
||||
const { data: articles } = useGetArticlesQuery(tags);
|
||||
|
||||
const { data: drafted } = useGetDraftedArticlesQuery(tags);
|
||||
|
||||
return (
|
||||
<div className="grid lg:grid-cols-[1fr_4fr_1fr] items-start gap-x-[34px] relative">
|
||||
<TagsFilters type="article" tags={tags} />
|
||||
<div className="mt-12">
|
||||
<div className="space-y-6 col-span-4">
|
||||
{drafted && drafted.length > 0 ? (
|
||||
<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 ? (
|
||||
<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">
|
||||
{articles?.map((article, index) => (
|
||||
<ArticleCard
|
||||
@@ -25,10 +43,10 @@ export function ArticlesList({ tags }: { tags: string[] }) {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="heading1 font-medium text-center">Статьи не найдены</p>
|
||||
)}
|
||||
</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 (
|
||||
<>
|
||||
<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(
|
||||
(tag) => (
|
||||
<TagFilter
|
||||
|
||||
@@ -9,7 +9,7 @@ export function useArticleMutation({
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (json: IArticle) =>
|
||||
mutationFn: async (json: Omit<IArticle, 'blocks'> & { blocks: string }) =>
|
||||
action === 'create'
|
||||
? await api.post('articles', { json }).json<IArticle>()
|
||||
: await api.put(`articles/${id}`, { json }).json<IArticle>(),
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { api } from '@/api';
|
||||
import { useModalStore } from '@/stores/useModalStore';
|
||||
import { IProject } from '@/types/IProject';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export function useDeleteMutation(
|
||||
entity: 'projects' | 'companies',
|
||||
entity: 'projects' | 'companies' | 'articles',
|
||||
id: string
|
||||
) {
|
||||
const { setModal } = useModalStore();
|
||||
@@ -12,10 +11,13 @@ export function useDeleteMutation(
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutateAsync: remove } = useMutation({
|
||||
mutationFn: async () =>
|
||||
await api.delete(`${entity}/${id}`).json<IProject>(),
|
||||
mutationFn: async () => await api.delete(`${entity}/${id}`).json(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [entity] });
|
||||
|
||||
if (entity === 'articles')
|
||||
queryClient.setQueryData(['articles', 'drafted', []], []);
|
||||
|
||||
setModal(null, '');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { useState } from 'react';
|
||||
import { getQueryClient } from './queryClient';
|
||||
|
||||
@@ -8,6 +9,9 @@ export const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
const [queryClient] = useState(getQueryClient);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<ReactQueryDevtools />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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[]>(),
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { IArticle } from '@/types/IArticle';
|
||||
import { ICompany } from '@/types/ICompany';
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -335,6 +335,18 @@
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.62.7.tgz#c7f6d0131c08cd2f60e73ec6e7b70e2e9e335def"
|
||||
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":
|
||||
version "5.62.7"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.62.7.tgz#8f253439a38ad6ce820bc6d42d89ca2556574d1a"
|
||||
|
||||
Reference in New Issue
Block a user