Files
zeropost-web/app/series/[slug]/page.js
T
Alexey Pavlov 03c10eab6e feat: серии + count-up в Stats
- SeriesGrid: карточки серий с иконками (Sparkles/Plug/Zap/Layers) и цветовыми темами
- /series/[slug]: страница серии с интро и сеткой статей в порядке из article_ids
- Stats: count-up анимация (easeOutQuart 1.2s) при появлении в viewport через IntersectionObserver
- sitemap.xml: добавлены /notes и все серии
2026-05-31 10:10:18 +03:00

84 lines
2.9 KiB
JavaScript

import { notFound } from 'next/navigation';
import Link from 'next/link';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import ArticleCard from '@/components/ArticleCard';
import { getSeries } from '@/lib/engine';
import { Sparkles, Plug, Zap, Layers, ArrowLeft } from 'lucide-react';
const ICONS = { Sparkles, Plug, Zap, Layers };
const COLORS = {
emerald: { bg: 'rgb(16 185 129 / 0.1)', text: '#10b981' },
teal: { bg: 'rgb(20 184 166 / 0.1)', text: '#14b8a6' },
amber: { bg: 'rgb(245 158 11 / 0.1)', text: '#f59e0b' },
indigo: { bg: 'rgb(99 102 241 / 0.1)', text: '#6366f1' },
};
export const dynamic = 'force-dynamic';
export async function generateMetadata({ params }) {
const { slug } = await params;
const s = await getSeries(slug);
if (!s) return { title: 'Серия не найдена' };
return {
title: s.title,
description: s.intro,
};
}
export default async function SeriesPage({ params }) {
const { slug } = await params;
const s = await getSeries(slug);
if (!s) notFound();
const Icon = ICONS[s.icon] || Layers;
const color = COLORS[s.color] || COLORS.emerald;
return (
<>
<Header />
<main className="pt-8 pb-16">
<div className="container-wide mb-8">
<Link href="/" className="btn btn-ghost text-sm mb-6 -ml-2">
<ArrowLeft className="w-4 h-4" /> Все статьи
</Link>
<div className="flex items-center gap-3 mb-4">
<div
className="w-12 h-12 sm:w-14 sm:h-14 rounded-xl flex items-center justify-center"
style={{ background: color.bg }}
>
<Icon className="w-6 h-6 sm:w-7 sm:h-7" style={{ color: color.text }} />
</div>
<div className="text-xs mute uppercase tracking-widest">Серия</div>
</div>
<h1 className="text-3xl sm:text-5xl font-bold ink leading-tight mb-3">
{s.title}
</h1>
{s.intro && (
<p className="mute text-base sm:text-lg max-w-2xl leading-relaxed">{s.intro}</p>
)}
<div className="text-xs mute mt-4">
{s.articles?.length || 0} {(s.articles?.length || 0) === 1 ? 'материал' : 'материалов'} в серии
</div>
</div>
{s.articles?.length > 0 ? (
<div className="container-wide">
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
{s.articles.map(a => <ArticleCard key={a.id} article={a} />)}
</div>
</div>
) : (
<div className="container-wide">
<div className="article-card p-8 text-center mute">
Скоро здесь появятся статьи. Серия только формируется.
</div>
</div>
)}
</main>
<Footer />
</>
);
}