diff --git a/app/admin/(protected)/notes/page.js b/app/admin/(protected)/notes/page.js
new file mode 100644
index 0000000..7a44c22
--- /dev/null
+++ b/app/admin/(protected)/notes/page.js
@@ -0,0 +1,139 @@
+'use client';
+import { useState, useEffect } from 'react';
+import { Pin, PinOff, Trash2, Plus, Save, Eye, EyeOff, Loader2, Check } from 'lucide-react';
+
+const EMPTY = { title: '', content: '', author: 'Редактор', is_pinned: false };
+
+export default function AdminNotesPage() {
+ const [notes, setNotes] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [editing, setEditing] = useState(null);
+ const [form, setForm] = useState(EMPTY);
+ const [saving, setSaving] = useState(false);
+ const [saved, setSaved] = useState(false);
+ const [err, setErr] = useState('');
+
+ async function load() {
+ setLoading(true);
+ try {
+ const r = await fetch('/admin/api/notes');
+ setNotes(await r.json());
+ } catch (e) { setErr(e.message); }
+ finally { setLoading(false); }
+ }
+
+ useEffect(() => { load(); }, []);
+
+ function startNew() { setForm(EMPTY); setEditing('new'); setErr(''); setSaved(false); }
+ function startEdit(n) { setForm({ title: n.title || '', content: n.content, author: n.author, is_pinned: n.is_pinned }); setEditing(n); setErr(''); setSaved(false); }
+
+ async function save() {
+ if (!form.content.trim()) { setErr('Текст обязателен'); return; }
+ setSaving(true); setErr('');
+ try {
+ const body = { ...form, title: form.title.trim() || null };
+ const method = editing === 'new' ? 'POST' : 'PATCH';
+ const url = editing === 'new' ? '/admin/api/notes' : `/admin/api/notes/${editing.id}`;
+ const r = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+ if (!r.ok) throw new Error(await r.text());
+ setSaved(true);
+ setTimeout(() => { setEditing(null); setSaved(false); }, 800);
+ await load();
+ } catch (e) { setErr(e.message); }
+ finally { setSaving(false); }
+ }
+
+ async function toggle(note, field) {
+ await fetch(`/admin/api/notes/${note.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [field]: !note[field] }) });
+ await load();
+ }
+
+ async function del(note) {
+ if (!confirm(`Удалить: «${(note.title || note.content).slice(0, 50)}»?`)) return;
+ await fetch(`/admin/api/notes/${note.id}`, { method: 'DELETE' });
+ await load();
+ }
+
+ const fmt = d => new Date(d).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric' });
+
+ return (
+
+
+
+
Заметки редактора
+
Отображаются на главной странице и на /notes
+
+
+
+
+ {editing && (
+
+
+ {editing === 'new' ? 'Новая заметка' : 'Редактировать'}
+
+
setForm(f => ({ ...f, title: e.target.value }))} />
+
+ )}
+
+ {loading &&
}
+
+ {!loading && notes.length === 0 && !editing && (
+
+ Заметок пока нет — нажми «Новая заметка»
+
+ )}
+
+
+ {notes.map(note => (
+
+
+
+ {note.is_pinned &&
}
+ {note.title &&
{note.title}
}
+
{note.content}
+
{note.author} · {fmt(note.created_at)}{!note.is_published ? ' · скрыта' : ''}
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/app/admin/api/notes/[id]/route.js b/app/admin/api/notes/[id]/route.js
new file mode 100644
index 0000000..7d6a948
--- /dev/null
+++ b/app/admin/api/notes/[id]/route.js
@@ -0,0 +1,26 @@
+import { NextResponse } from 'next/server';
+import { requireAdminAuth } from '@/lib/adminAuth';
+import { updateNote, deleteNote } from '@/lib/engine';
+
+export async function PATCH(req, { params }) {
+ await requireAdminAuth();
+ try {
+ const { id } = await params;
+ const body = await req.json();
+ const note = await updateNote(id, body);
+ return NextResponse.json(note);
+ } catch (err) {
+ return NextResponse.json({ error: err.message }, { status: 500 });
+ }
+}
+
+export async function DELETE(req, { params }) {
+ await requireAdminAuth();
+ try {
+ const { id } = await params;
+ await deleteNote(id);
+ return NextResponse.json({ ok: true });
+ } catch (err) {
+ return NextResponse.json({ error: err.message }, { status: 500 });
+ }
+}
diff --git a/app/admin/api/notes/route.js b/app/admin/api/notes/route.js
new file mode 100644
index 0000000..ca98d93
--- /dev/null
+++ b/app/admin/api/notes/route.js
@@ -0,0 +1,20 @@
+import { NextResponse } from 'next/server';
+import { requireAdminAuth } from '@/lib/adminAuth';
+import { listNotes, createNote } from '@/lib/engine';
+
+export async function GET() {
+ await requireAdminAuth();
+ const notes = await listNotes({ limit: 100 });
+ return NextResponse.json(notes);
+}
+
+export async function POST(req) {
+ await requireAdminAuth();
+ try {
+ const body = await req.json();
+ const note = await createNote(body);
+ return NextResponse.json(note);
+ } catch (err) {
+ return NextResponse.json({ error: err.message }, { status: 500 });
+ }
+}
diff --git a/components/admin/AdminNav.js b/components/admin/AdminNav.js
index ee4a9b9..0af0594 100644
--- a/components/admin/AdminNav.js
+++ b/components/admin/AdminNav.js
@@ -1,13 +1,14 @@
'use client';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
-import { LayoutDashboard, FileText, Radio, Zap, Settings, LogOut, ExternalLink } from 'lucide-react';
+import { LayoutDashboard, FileText, Radio, Zap, Settings, LogOut, ExternalLink, MessageCircle } from 'lucide-react';
const NAV = [
{ href: '/admin', label: 'Дашборд', icon: LayoutDashboard, exact: true },
{ href: '/admin/articles', label: 'Статьи', icon: FileText },
{ href: '/admin/channels', label: 'Каналы', icon: Radio },
{ href: '/admin/autogen', label: 'Автогенерация', icon: Zap },
+ { href: '/admin/notes', label: 'Заметки', icon: MessageCircle },
{ href: '/admin/settings', label: 'Настройки', icon: Settings },
];
diff --git a/lib/engine.js b/lib/engine.js
index 764f91a..115e3c1 100644
--- a/lib/engine.js
+++ b/lib/engine.js
@@ -68,6 +68,16 @@ export async function listNotes({ limit = 20 } = {}) {
catch { return []; }
}
+export async function createNote(data) {
+ return call('/api/notes', { method: 'POST', body: JSON.stringify(data) });
+}
+export async function updateNote(id, data) {
+ return call(`/api/notes/${id}`, { method: 'PATCH', body: JSON.stringify(data) });
+}
+export async function deleteNote(id) {
+ return call(`/api/notes/${id}`, { method: 'DELETE' });
+}
+
export async function getStats() {
try {
return await call('/api/stats', { cache: 'no-store' });