1
0
forked from mixa/67
Files
67/apps/web/src/pages/cabinet/EditorPage.jsx
2026-06-22 20:13:14 +03:00

105 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { FilePlus2, MoreHorizontal } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import { useSession } from "../../app/store/session";
import { contentApi } from "../../shared/api/endpoints";
import { normalizeContentList } from "../../shared/api/normalize";
import { queryKeys } from "../../shared/api/queryKeys";
import { Button } from "../../shared/ui/Button";
import { EmptyState, ErrorState, Skeleton } from "../../shared/ui/States";
import { StatusBadge } from "../../shared/ui/StatusBadge";
const statusFilters = [
["all", "Все"],
["draft", "Черновики"],
["moderation", "На модерации"],
["published", "Опубликованные"],
["returned", "Возвращенные"],
];
function formatDate(value) {
if (!value) return "Нет даты";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString("ru-RU", { day: "numeric", month: "long", hour: "2-digit", minute: "2-digit" });
}
export function EditorPage() {
const user = useSession((state) => state.user);
const queryParams = { mine: true, authorId: user?.id };
const materialsQuery = useQuery({
queryKey: queryKeys.content(queryParams),
queryFn: () => contentApi.list(queryParams),
enabled: Boolean(user?.id),
});
const materials = normalizeContentList(materialsQuery.data);
return (
<div>
<div className="flex flex-wrap items-end justify-between gap-5">
<div>
<p className="text-xs font-bold uppercase tracking-widest text-accent">Редакция</p>
<h1 className="mt-2 font-serif text-4xl">Мои материалы</h1>
<p className="mt-3 text-muted">Черновики, публикации и замечания модераторов загружаются из backend.</p>
</div>
<Link to="/cabinet/editor/new">
<Button icon={<FilePlus2 className="size-4" />}>Новый материал</Button>
</Link>
</div>
<div className="mt-7 flex gap-2 overflow-x-auto pb-2">
{statusFilters.map(([status, label], index) => (
<Button key={status} size="sm" variant={index === 0 ? "primary" : "secondary"}>
{label}
</Button>
))}
</div>
<div className="mt-5 overflow-hidden rounded-lg border border-line bg-surface">
<div className="hidden grid-cols-[minmax(0,4fr)_minmax(8rem,1fr)_minmax(8rem,1fr)_auto] border-b border-line bg-paper px-5 py-3 text-xs font-bold uppercase tracking-wider text-muted md:grid">
<span>Материал</span>
<span>Статус</span>
<span>Обновлен</span>
<span />
</div>
{materialsQuery.isLoading && (
<div className="space-y-3 p-5">
<Skeleton className="h-24" />
<Skeleton className="h-24" />
<Skeleton className="h-24" />
</div>
)}
{materialsQuery.isError && (
<div className="p-5">
<ErrorState retry={() => materialsQuery.refetch()} />
</div>
)}
{materialsQuery.isSuccess && materials.length === 0 && (
<div className="p-5">
<EmptyState title="Материалов пока нет" text="Созданные через backend записи появятся в этом списке." />
</div>
)}
{materialsQuery.isSuccess && materials.map((material) => (
<article
key={material.id}
className="grid gap-4 border-b border-line p-5 last:border-0 md:grid-cols-[minmax(0,4fr)_minmax(8rem,1fr)_minmax(8rem,1fr)_auto] md:items-center"
>
<div>
<p className="text-xs text-muted">{material.type ?? "Материал"} · {material.category}</p>
<h2 className="mt-1 font-serif text-xl">{material.title}</h2>
{(material.moderatorComment || material.reviewComment) && (
<p className="mt-2 text-sm text-danger">{material.moderatorComment ?? material.reviewComment}</p>
)}
</div>
<StatusBadge status={material.status ?? "draft"} />
<span className="text-sm text-muted">{formatDate(material.updatedAt ?? material.publishedAt)}</span>
<button className="grid size-9 place-items-center rounded-md hover:bg-paper" aria-label="Действия">
<MoreHorizontal className="size-5" />
</button>
</article>
))}
</div>
</div>
);
}