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": {
"@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';
+13 -1
View File
@@ -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>
);
}
+11 -2
View File
@@ -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>
);
}
+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';
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>
);
}
+8 -2
View File
@@ -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>
+1 -4
View File
@@ -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({
+1 -1
View File
@@ -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>;
+7 -3
View File
@@ -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>
);
+38 -8
View File
@@ -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>
);
@@ -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>
);
}
+34 -9
View File
@@ -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>
);
}
+23 -5
View File
@@ -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
+1 -1
View File
@@ -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>(),
+6 -4
View File
@@ -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, '');
},
});
+5 -1
View File
@@ -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>
);
};
+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 { 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;
}
+12
View File
@@ -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"