03c10eab6e
- SeriesGrid: карточки серий с иконками (Sparkles/Plug/Zap/Layers) и цветовыми темами - /series/[slug]: страница серии с интро и сеткой статей в порядке из article_ids - Stats: count-up анимация (easeOutQuart 1.2s) при появлении в viewport через IntersectionObserver - sitemap.xml: добавлены /notes и все серии
84 lines
2.9 KiB
JavaScript
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 />
|
|
</>
|
|
);
|
|
}
|