@@ -90,6 +91,11 @@ export default function Header() {
onClick={() => setOpen(false)}
className="text-2xl font-semibold ink py-3 border-b-soft"
>Статьи
+
setOpen(false)}
+ className="text-2xl font-semibold ink py-3 border-b-soft"
+ >Заметки
setOpen(false)}
diff --git a/components/NotesBlock.js b/components/NotesBlock.js
new file mode 100644
index 0000000..9752362
--- /dev/null
+++ b/components/NotesBlock.js
@@ -0,0 +1,53 @@
+import Link from 'next/link';
+import { Pin, ArrowRight, MessageCircle } from 'lucide-react';
+import { formatDate } from '@/lib/markdown';
+
+function NoteCard({ note }) {
+ return (
+
+ {note.is_pinned && (
+
+ )}
+ {note.title && (
+
+ {note.title}
+
+ )}
+
+ {note.content}
+
+
+ {note.author}
+ {formatDate(note.created_at)}
+
+
+ );
+}
+
+export default function NotesBlock({ notes, compact = false }) {
+ if (!notes || notes.length === 0) return null;
+ const items = compact ? notes.slice(0, 3) : notes;
+
+ return (
+
+
+
+
+
+ Заметки редактора
+
+
+ {compact && notes.length > 3 && (
+
+ Все
+
+ )}
+
+
+ {items.map(n => )}
+
+
+ );
+}
diff --git a/components/NowBlock.js b/components/NowBlock.js
new file mode 100644
index 0000000..7a5d46a
--- /dev/null
+++ b/components/NowBlock.js
@@ -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 (
+
+
+
+
+
+
+
Сейчас
+
+
+
+ {/* Главная карточка — последняя статья / процесс */}
+
+ {processing ? (
+ <>
+
+
+ ИИ пишет статью
+
+
+ «{processing.topic}»
+
+
+ Началось {timeAgo(processing.created_at)} · подожди немного, скоро появится
+
+ >
+ ) : latest ? (
+
+
+
+
Последний материал — {timeAgo(latest.published_at)}
+
+
+ {latest.title}
+
+
+
+ claude-sonnet-4-6
+
+ {latest.tokens_out && (
+
+ {latest.tokens_out.toLocaleString('ru-RU')} токенов
+
+ )}
+
+
+ ) : (
+
Скоро здесь что-нибудь появится…
+ )}
+
+
+ {/* Bar-чарт за 7 дней */}
+
+
+
+ Активность за неделю
+
+
+ {week.map((d, i) => {
+ const h = d.cnt > 0 ? Math.max(10, (d.cnt / maxCnt) * 100) : 4;
+ const isToday = i === week.length - 1;
+ return (
+
+
0
+ ? (isToday ? 'rgb(var(--accent))' : 'rgb(var(--accent) / 0.45)')
+ : 'rgb(var(--surface-2))',
+ }}
+ title={`${d.day}: ${d.cnt} ${d.cnt === 1 ? 'статья' : 'статей'}`}
+ />
+
+ );
+ })}
+
+
+ {week.map((d, i) => {
+ const date = new Date(d.day);
+ const dow = ['вс','пн','вт','ср','чт','пт','сб'][date.getDay()];
+ return (
+
+ {dow}
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/lib/engine.js b/lib/engine.js
index a05c868..f63c2ba 100644
--- a/lib/engine.js
+++ b/lib/engine.js
@@ -37,6 +37,16 @@ export async function listTags() {
return call('/api/articles/tags', { next: { revalidate: 300 } });
}
+export async function getLive() {
+ try { return await call('/api/stats/live', { cache: 'no-store' }); }
+ catch { return null; }
+}
+
+export async function listNotes({ limit = 20 } = {}) {
+ try { return await call(`/api/notes?limit=${limit}`, { cache: 'no-store' }); }
+ catch { return []; }
+}
+
export async function getStats() {
try {
return await call('/api/stats', { cache: 'no-store' });