feat: TOC оглавление + SVG-обложки-фоллбеки + /archive
TOC:
- renderMarkdownWithToc: парсит h2/h3, генерит транслит-якоря для кириллицы, возвращает {html, toc}
- TableOfContents компонент: sticky на десктопе, раскрывающийся блок на мобиле
- IntersectionObserver-free подсветка активной секции через scroll listener
- Двухколоночный layout статьи на lg+: 240px TOC + контент
SVG-обложки:
- ArticleCoverSVG: процедурно сгенерированная композиция (curve/circle/arc/rect) по seed = id статьи
- 6 палитр на выбор (emerald/teal/yellow/blue/purple/orange), seed детерминированный
- Используется в ArticleCard как fallback когда cover_url пусто
- На странице статьи тоже SVG если обложки нет
- Тег статьи отображается лейблом в углу
Архив:
- /archive: все статьи сгруппированы по месяцам, компактный список
- В Header добавлен пункт Архив (desktop+mobile)
- В Footer ссылки на Архив, Заметки, О проекте
- В sitemap.xml включён /archive
This commit is contained in:
@@ -1,43 +1,13 @@
|
||||
import Link from 'next/link';
|
||||
import { formatDate } from '@/lib/markdown';
|
||||
import { Clock } from 'lucide-react';
|
||||
import ArticleCoverSVG from './ArticleCoverSVG';
|
||||
|
||||
function imageUrl(article) {
|
||||
if (!article.cover_url) return null;
|
||||
return article.cover_url;
|
||||
}
|
||||
|
||||
function gradientFor(article) {
|
||||
const seed = (article.id || 0) * 31 + (article.title?.length || 0);
|
||||
const variants = [
|
||||
'linear-gradient(135deg, #10b981 0%, #0ea5e9 100%)',
|
||||
'linear-gradient(135deg, #34d399 0%, #14b8a6 100%)',
|
||||
'linear-gradient(135deg, #059669 0%, #6366f1 100%)',
|
||||
'linear-gradient(135deg, #10b981 0%, #fbbf24 100%)',
|
||||
'linear-gradient(135deg, #14b8a6 0%, #a78bfa 100%)',
|
||||
'linear-gradient(135deg, #047857 0%, #0891b2 100%)',
|
||||
];
|
||||
return variants[seed % variants.length];
|
||||
}
|
||||
|
||||
function CoverPlaceholder({ article, featured = false, className = '' }) {
|
||||
const gradient = gradientFor(article);
|
||||
return (
|
||||
<div
|
||||
className={`relative overflow-hidden ${featured ? 'aspect-[16/9] sm:aspect-[16/10]' : 'aspect-[16/9]'} rounded-xl ${className}`}
|
||||
style={{ background: gradient }}
|
||||
>
|
||||
<div className="absolute -top-10 -right-10 w-40 h-40 rounded-full opacity-30" style={{ background: 'rgba(255,255,255,0.3)' }} />
|
||||
<div className="absolute -bottom-12 -left-8 w-32 h-32 rounded-full opacity-20" style={{ background: 'rgba(0,0,0,0.2)' }} />
|
||||
<div className="absolute inset-0 flex items-end p-4">
|
||||
<div className="text-white/90 text-xs font-mono tracking-wider uppercase">
|
||||
#{(article.tags?.[0] || 'zeropost')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ArticleCard({ article, featured = false }) {
|
||||
const img = imageUrl(article);
|
||||
|
||||
@@ -45,7 +15,6 @@ export default function ArticleCard({ article, featured = false }) {
|
||||
return (
|
||||
<Link href={`/blog/${article.slug}`} className="article-card block group overflow-hidden p-0">
|
||||
<div className="flex flex-col sm:grid sm:grid-cols-5 sm:gap-0">
|
||||
{/* Image: full-width на мобиле, 2/5 на десктопе */}
|
||||
<div className="p-3 sm:p-5 sm:col-span-2">
|
||||
{img ? (
|
||||
<img
|
||||
@@ -55,7 +24,7 @@ export default function ArticleCard({ article, featured = false }) {
|
||||
loading="eager"
|
||||
/>
|
||||
) : (
|
||||
<CoverPlaceholder article={article} className="sm:aspect-square" />
|
||||
<ArticleCoverSVG article={article} aspect="16/9" className="sm:!aspect-square" />
|
||||
)}
|
||||
</div>
|
||||
<div className="p-5 sm:p-8 sm:col-span-3 flex flex-col justify-center">
|
||||
@@ -92,7 +61,7 @@ export default function ArticleCard({ article, featured = false }) {
|
||||
{img ? (
|
||||
<img src={img} alt={article.title} className="w-full aspect-[16/9] object-cover rounded-lg" loading="lazy" />
|
||||
) : (
|
||||
<CoverPlaceholder article={article} />
|
||||
<ArticleCoverSVG article={article} />
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 sm:p-5 pt-2">
|
||||
|
||||
Reference in New Issue
Block a user