206 lines
9.1 KiB
JavaScript
206 lines
9.1 KiB
JavaScript
import { ArrowRight, CalendarDays, ChevronRight } from "lucide-react";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { Link } from "react-router-dom";
|
||
import { contentApi, directoriesApi } from "../shared/api/endpoints";
|
||
import { normalizeContentList, toList } from "../shared/api/normalize";
|
||
import { queryKeys } from "../shared/api/queryKeys";
|
||
import { Badge } from "../shared/ui/Badge";
|
||
import { Button } from "../shared/ui/Button";
|
||
import { MaterialCard } from "../shared/ui/MaterialCard";
|
||
import { ResponsiveImage } from "../shared/ui/ResponsiveImage";
|
||
import { EmptyState, ErrorState, Skeleton } from "../shared/ui/States";
|
||
|
||
function eventDateParts(value = "") {
|
||
const date = new Date(value);
|
||
if (!Number.isNaN(date.getTime())) {
|
||
return [
|
||
date.toLocaleDateString("ru-RU", { day: "numeric" }),
|
||
date.toLocaleDateString("ru-RU", { month: "short" }),
|
||
];
|
||
}
|
||
const [day = "", month = ""] = String(value).split(" ");
|
||
return [day, month];
|
||
}
|
||
|
||
export function HomePage() {
|
||
const { data: contentPayload, isLoading, isError, refetch } = useQuery({
|
||
queryKey: queryKeys.content({ page: "home" }),
|
||
queryFn: () => contentApi.list({ limit: 6 }),
|
||
});
|
||
const { data: eventsPayload } = useQuery({
|
||
queryKey: queryKeys.events({ page: "home" }),
|
||
queryFn: () => directoriesApi.events({ limit: 3 }),
|
||
});
|
||
const { data: categoriesPayload } = useQuery({
|
||
queryKey: queryKeys.categories,
|
||
queryFn: directoriesApi.categories,
|
||
});
|
||
|
||
const pageMaterials = normalizeContentList(contentPayload);
|
||
const pageEvents = toList(eventsPayload);
|
||
const pageCategories = toList(categoriesPayload);
|
||
const featured = pageMaterials.find((item) => item.featured) ?? pageMaterials[0];
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="page-shell grid gap-6 py-10">
|
||
<Skeleton className="h-96 w-full" />
|
||
<Skeleton className="h-44 w-full" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (isError) {
|
||
return (
|
||
<div className="page-shell py-16">
|
||
<ErrorState retry={refetch} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!featured) {
|
||
return (
|
||
<div className="page-shell py-16">
|
||
<EmptyState title="Материалы не найдены" text="Backend пока не вернул опубликованные записи." />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<section className="border-b border-line">
|
||
<div className="page-shell grid gap-8 py-8 md:py-12 lg:grid-cols-12">
|
||
<article className="relative overflow-hidden rounded-xl bg-primary lg:col-span-8">
|
||
<ResponsiveImage
|
||
src={featured.image}
|
||
alt=""
|
||
eager
|
||
className="absolute inset-0 h-full w-full object-cover opacity-45 mix-blend-luminosity"
|
||
/>
|
||
<div className="relative flex min-h-[clamp(26rem,60vh,36rem)] flex-col justify-end p-6 text-white sm:p-10 lg:p-12">
|
||
<Badge className="w-fit bg-white/90 text-primary">{featured.category}</Badge>
|
||
<h1 className="mt-5 max-w-4xl font-serif text-4xl leading-[1.04] tracking-[-0.03em] sm:text-5xl lg:text-6xl">
|
||
{featured.title}
|
||
</h1>
|
||
<p className="mt-5 max-w-2xl text-base leading-7 text-white/80 sm:text-lg">{featured.excerpt}</p>
|
||
<Link className="mt-7 w-fit" to={`/materials/${featured.slug}`} viewTransition>
|
||
<Button variant="accent" size="lg" icon={<ArrowRight className="size-4" />}>
|
||
Читать материал
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
</article>
|
||
|
||
<aside className="flex flex-col rounded-xl border border-line bg-surface p-6 lg:col-span-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<span className="text-xs font-bold uppercase tracking-widest text-accent">Сегодня</span>
|
||
<h2 className="mt-2 font-serif text-3xl">В университете</h2>
|
||
</div>
|
||
</div>
|
||
<div className="mt-7 divide-y divide-line">
|
||
{pageMaterials.slice(1, 4).map((item) => (
|
||
<Link key={item.id} to={`/materials/${item.slug}`} viewTransition className="group block py-5 first:pt-0">
|
||
<span className="text-xs font-bold uppercase tracking-wider text-muted">
|
||
{item.type} · {item.publishedAt}
|
||
</span>
|
||
<h3 className="mt-2 font-serif text-xl leading-snug transition group-hover:text-primary">{item.title}</h3>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
<Link className="mt-auto flex items-center gap-2 border-t border-line pt-5 text-sm font-bold text-primary" to="/materials">
|
||
Все материалы <ChevronRight className="size-4" />
|
||
</Link>
|
||
</aside>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="page-shell py-16">
|
||
<div className="flex items-end justify-between gap-6">
|
||
<div>
|
||
<p className="text-xs font-bold uppercase tracking-widest text-accent">Новая лента</p>
|
||
<h2 className="mt-2 font-serif text-4xl">Последние публикации</h2>
|
||
</div>
|
||
<Link className="hidden text-sm font-bold text-primary sm:block" to="/materials">
|
||
Смотреть все →
|
||
</Link>
|
||
</div>
|
||
<div className="mt-8 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||
{pageMaterials.slice(1, 4).map((material) => (
|
||
<MaterialCard key={material.id} material={material} />
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
<section className="bg-primary text-white">
|
||
<div className="page-shell grid gap-10 py-16 lg:grid-cols-[minmax(16rem,3fr)_minmax(0,5fr)]">
|
||
<div>
|
||
<p className="text-xs font-bold uppercase tracking-widest text-white/55">Календарь</p>
|
||
<h2 className="mt-3 font-serif text-4xl">Ближайшие события</h2>
|
||
<p className="mt-4 max-w-md leading-7 text-white/65">
|
||
Лекции, выставки, защиты проектов и встречи университетского сообщества.
|
||
</p>
|
||
<Link to="/events">
|
||
<Button className="mt-7 border-white/25 text-white hover:bg-white/10" variant="secondary">
|
||
Открыть календарь
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
<div className="divide-y divide-white/15 border-y border-white/15">
|
||
{pageEvents.length === 0 && (
|
||
<div className="py-8 text-white/65">Backend пока не вернул ближайшие события.</div>
|
||
)}
|
||
{pageEvents.map((event) => {
|
||
const [day, month] = eventDateParts(event.date ?? event.startsAt);
|
||
return (
|
||
<article
|
||
key={event.id}
|
||
className="grid gap-4 py-5 sm:grid-cols-[minmax(5rem,1fr)_minmax(0,5fr)_auto] sm:items-center"
|
||
>
|
||
<div>
|
||
<span className="block font-serif text-2xl">{day}</span>
|
||
<span className="text-xs uppercase tracking-wider text-white/50">{month}</span>
|
||
</div>
|
||
<div>
|
||
<h3 className="font-serif text-xl">{event.title}</h3>
|
||
<p className="mt-1 text-sm text-white/55">
|
||
{event.time ?? event.startsAt} · {event.place ?? event.location}
|
||
</p>
|
||
</div>
|
||
<CalendarDays className="hidden size-5 text-white/45 sm:block" />
|
||
</article>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="page-shell py-16">
|
||
<p className="text-xs font-bold uppercase tracking-widest text-accent">Навигация по темам</p>
|
||
<div className="mt-5 grid border-l border-t border-line sm:grid-cols-2 lg:grid-cols-4">
|
||
{pageCategories.map((category, index) => {
|
||
const name = typeof category === "string" ? category : category.name ?? category.title ?? category.label;
|
||
const value = typeof category === "string" ? category : category.slug ?? category.id ?? name;
|
||
const description = typeof category === "string" ? "Материалы этой категории доступны в едином каталоге." : category.description;
|
||
return (
|
||
<Link
|
||
key={category.id ?? value}
|
||
to={`/materials?category=${value}`}
|
||
className="group min-h-56 border-b border-r border-line bg-surface p-6 transition hover:bg-primary hover:text-white"
|
||
>
|
||
<span className="font-serif text-4xl text-line transition group-hover:text-white/25">
|
||
{String(index + 1).padStart(2, "0")}
|
||
</span>
|
||
<h3 className="mt-12 font-serif text-2xl">{name}</h3>
|
||
<p className="mt-3 text-sm leading-6 text-muted transition group-hover:text-white/65">
|
||
{description}
|
||
</p>
|
||
</Link>
|
||
);
|
||
})}
|
||
</div>
|
||
</section>
|
||
</>
|
||
);
|
||
}
|