feat: серии + count-up в Stats
- SeriesGrid: карточки серий с иконками (Sparkles/Plug/Zap/Layers) и цветовыми темами - /series/[slug]: страница серии с интро и сеткой статей в порядке из article_ids - Stats: count-up анимация (easeOutQuart 1.2s) при появлении в viewport через IntersectionObserver - sitemap.xml: добавлены /notes и все серии
This commit is contained in:
+14
-3
@@ -6,21 +6,23 @@ import HeroImage from '@/components/HeroImage';
|
||||
import Stats from '@/components/Stats';
|
||||
import NowBlock from '@/components/NowBlock';
|
||||
import NotesBlock from '@/components/NotesBlock';
|
||||
import SeriesGrid from '@/components/SeriesGrid';
|
||||
import Reveal from '@/components/Reveal';
|
||||
import { listArticles, listTags, getStats, getLive, listNotes } from '@/lib/engine';
|
||||
import { listArticles, listTags, getStats, getLive, listNotes, listSeries } from '@/lib/engine';
|
||||
import { Sparkles, ArrowRight } from 'lucide-react';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function HomePage() {
|
||||
let articles = [], tags = [], stats = null, live = null, notes = [];
|
||||
let articles = [], tags = [], stats = null, live = null, notes = [], series = [];
|
||||
try {
|
||||
[articles, tags, stats, live, notes] = await Promise.all([
|
||||
[articles, tags, stats, live, notes, series] = await Promise.all([
|
||||
listArticles({ limit: 13 }),
|
||||
listTags(),
|
||||
getStats(),
|
||||
getLive(),
|
||||
listNotes({ limit: 6 }),
|
||||
listSeries(),
|
||||
]);
|
||||
} catch (err) {
|
||||
console.error('Home load failed:', err.message);
|
||||
@@ -88,6 +90,15 @@ export default async function HomePage() {
|
||||
</Reveal>
|
||||
)}
|
||||
|
||||
{/* Серии */}
|
||||
{series.length > 0 && (
|
||||
<Reveal>
|
||||
<div className="reveal">
|
||||
<SeriesGrid series={series} />
|
||||
</div>
|
||||
</Reveal>
|
||||
)}
|
||||
|
||||
{/* Rest */}
|
||||
{rest.length > 0 && (
|
||||
<Reveal>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
+12
-3
@@ -1,16 +1,18 @@
|
||||
import { listArticles, listTags } from '@/lib/engine';
|
||||
import { listArticles, listTags, listSeries } from '@/lib/engine';
|
||||
|
||||
const SITE = 'https://zeropost.ru';
|
||||
|
||||
export default async function sitemap() {
|
||||
const [articles, tags] = await Promise.all([
|
||||
const [articles, tags, series] = await Promise.all([
|
||||
listArticles({ limit: 200 }).catch(() => []),
|
||||
listTags().catch(() => []),
|
||||
listSeries().catch(() => []),
|
||||
]);
|
||||
|
||||
const staticPages = [
|
||||
{ url: `${SITE}/`, lastModified: new Date(), changeFrequency: 'daily', priority: 1 },
|
||||
{ url: `${SITE}/about`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.5 },
|
||||
{ url: `${SITE}/notes`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.6 },
|
||||
];
|
||||
|
||||
const articlePages = articles.map(a => ({
|
||||
@@ -27,5 +29,12 @@ export default async function sitemap() {
|
||||
priority: 0.6,
|
||||
}));
|
||||
|
||||
return [...staticPages, ...articlePages, ...tagPages];
|
||||
const seriesPages = series.map(s => ({
|
||||
url: `${SITE}/series/${s.slug}`,
|
||||
lastModified: s.updated_at ? new Date(s.updated_at) : new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.7,
|
||||
}));
|
||||
|
||||
return [...staticPages, ...articlePages, ...seriesPages, ...tagPages];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user