feat: блок «Сейчас» + «Заметки редактора» + ArticleMeta
- NowBlock: live indicator (последняя статья / идёт генерация) + bar-чарт за 7 дней - NotesBlock: карточки заметок редактора с pin - /notes: отдельная страница со всеми заметками - ArticleMeta: раскрывающийся блок «Как сделана эта статья» на странице статьи - В шапку добавлена ссылка «Заметки» (desktop и mobile)
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import Link from 'next/link';
|
||||
import { Activity, Cpu, Calendar, Sparkles, Zap } from 'lucide-react';
|
||||
import { formatDate } from '@/lib/markdown';
|
||||
|
||||
// Сколько прошло времени с даты
|
||||
function timeAgo(iso) {
|
||||
if (!iso) return null;
|
||||
const seconds = Math.max(0, Math.floor((Date.now() - new Date(iso).getTime()) / 1000));
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(mins / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
if (days > 0) return `${days} дн назад`;
|
||||
if (hours > 0) return `${hours} ч назад`;
|
||||
if (mins > 0) return `${mins} мин назад`;
|
||||
return 'только что';
|
||||
}
|
||||
|
||||
export default function NowBlock({ live }) {
|
||||
if (!live) return null;
|
||||
const { latest, processing, week } = live;
|
||||
const maxCnt = Math.max(1, ...week.map(d => d.cnt));
|
||||
|
||||
return (
|
||||
<section className="container-wide pb-12">
|
||||
<div className="flex items-center gap-2 mb-4 sm:mb-5">
|
||||
<span className="relative flex h-2.5 w-2.5">
|
||||
<span
|
||||
className="absolute inline-flex h-full w-full rounded-full opacity-75 animate-ping"
|
||||
style={{ background: 'rgb(var(--accent))' }}
|
||||
/>
|
||||
<span
|
||||
className="relative inline-flex rounded-full h-2.5 w-2.5"
|
||||
style={{ background: 'rgb(var(--accent))' }}
|
||||
/>
|
||||
</span>
|
||||
<h2 className="text-xs sm:text-sm font-medium uppercase tracking-widest mute">Сейчас</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-4">
|
||||
{/* Главная карточка — последняя статья / процесс */}
|
||||
<div className="article-card lg:col-span-2 p-5 sm:p-6">
|
||||
{processing ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2 text-sm accent mb-2">
|
||||
<Sparkles className="w-4 h-4 animate-pulse" />
|
||||
<span className="font-medium">ИИ пишет статью</span>
|
||||
</div>
|
||||
<div className="text-lg sm:text-xl ink font-semibold leading-snug mb-2 line-clamp-2">
|
||||
«{processing.topic}»
|
||||
</div>
|
||||
<div className="text-xs mute">
|
||||
Началось {timeAgo(processing.created_at)} · подожди немного, скоро появится
|
||||
</div>
|
||||
</>
|
||||
) : latest ? (
|
||||
<Link href={`/blog/${latest.slug}`} className="block group">
|
||||
<div className="flex items-center gap-2 text-xs mute mb-2">
|
||||
<Activity className="w-3.5 h-3.5" />
|
||||
<span>Последний материал — {timeAgo(latest.published_at)}</span>
|
||||
</div>
|
||||
<div className="text-lg sm:text-xl ink font-semibold leading-snug mb-3 group-hover:accent transition-colors line-clamp-2">
|
||||
{latest.title}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs mute">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Cpu className="w-3 h-3" /> claude-sonnet-4-6
|
||||
</span>
|
||||
{latest.tokens_out && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Zap className="w-3 h-3" /> {latest.tokens_out.toLocaleString('ru-RU')} токенов
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="mute text-sm">Скоро здесь что-нибудь появится…</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bar-чарт за 7 дней */}
|
||||
<div className="article-card p-5 sm:p-6">
|
||||
<div className="flex items-center gap-2 text-xs mute mb-3">
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
<span className="uppercase tracking-wider">Активность за неделю</span>
|
||||
</div>
|
||||
<div className="flex items-end gap-1.5 h-20 mb-2">
|
||||
{week.map((d, i) => {
|
||||
const h = d.cnt > 0 ? Math.max(10, (d.cnt / maxCnt) * 100) : 4;
|
||||
const isToday = i === week.length - 1;
|
||||
return (
|
||||
<div key={d.day} className="flex-1 flex flex-col items-center gap-1 group">
|
||||
<div
|
||||
className="w-full rounded-md transition-all"
|
||||
style={{
|
||||
height: `${h}%`,
|
||||
background: d.cnt > 0
|
||||
? (isToday ? 'rgb(var(--accent))' : 'rgb(var(--accent) / 0.45)')
|
||||
: 'rgb(var(--surface-2))',
|
||||
}}
|
||||
title={`${d.day}: ${d.cnt} ${d.cnt === 1 ? 'статья' : 'статей'}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex justify-between text-[10px] mute">
|
||||
{week.map((d, i) => {
|
||||
const date = new Date(d.day);
|
||||
const dow = ['вс','пн','вт','ср','чт','пт','сб'][date.getDay()];
|
||||
return (
|
||||
<span key={d.day} className={i === week.length - 1 ? 'accent font-medium' : ''}>
|
||||
{dow}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user