c27985614e
- NowBlock: live indicator (последняя статья / идёт генерация) + bar-чарт за 7 дней - NotesBlock: карточки заметок редактора с pin - /notes: отдельная страница со всеми заметками - ArticleMeta: раскрывающийся блок «Как сделана эта статья» на странице статьи - В шапку добавлена ссылка «Заметки» (desktop и mobile)
122 lines
5.2 KiB
JavaScript
122 lines
5.2 KiB
JavaScript
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>
|
|
);
|
|
}
|