Files
67/apps/web/src/pages/HomePage.jsx
2026-06-22 22:39:08 +03:00

206 lines
9.1 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 { 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>
</>
);
}