{note.content}
+diff --git a/app/api/notes/[id]/route.js b/app/api/notes/[id]/route.js
new file mode 100644
index 0000000..cfe0109
--- /dev/null
+++ b/app/api/notes/[id]/route.js
@@ -0,0 +1,28 @@
+import { NextResponse } from 'next/server';
+import { requireAdmin } from '@/lib/session';
+import { engine } from '@/lib/engine';
+
+export async function PATCH(req, { params }) {
+ const admin = await requireAdmin();
+ if (!admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ try {
+ const { id } = await params;
+ const body = await req.json();
+ const note = await engine.updateNote(id, body);
+ return NextResponse.json(note);
+ } catch (err) {
+ return NextResponse.json({ error: err.message }, { status: 500 });
+ }
+}
+
+export async function DELETE(req, { params }) {
+ const admin = await requireAdmin();
+ if (!admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ try {
+ const { id } = await params;
+ await engine.deleteNote(id);
+ return NextResponse.json({ ok: true });
+ } catch (err) {
+ return NextResponse.json({ error: err.message }, { status: 500 });
+ }
+}
diff --git a/app/api/notes/route.js b/app/api/notes/route.js
new file mode 100644
index 0000000..2252211
--- /dev/null
+++ b/app/api/notes/route.js
@@ -0,0 +1,26 @@
+import { NextResponse } from 'next/server';
+import { requireAdmin } from '@/lib/session';
+import { engine } from '@/lib/engine';
+
+export async function GET(req) {
+ const admin = await requireAdmin();
+ if (!admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ try {
+ const notes = await engine.listNotes();
+ return NextResponse.json(notes);
+ } catch (err) {
+ return NextResponse.json({ error: err.message }, { status: 500 });
+ }
+}
+
+export async function POST(req) {
+ const admin = await requireAdmin();
+ if (!admin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
+ try {
+ const body = await req.json();
+ const note = await engine.createNote(body);
+ return NextResponse.json(note);
+ } catch (err) {
+ return NextResponse.json({ error: err.message }, { status: 500 });
+ }
+}
diff --git a/app/notes/page.js b/app/notes/page.js
new file mode 100644
index 0000000..721fd51
--- /dev/null
+++ b/app/notes/page.js
@@ -0,0 +1,168 @@
+'use client';
+import { useState, useEffect } from 'react';
+import { Pin, PinOff, Trash2, Plus, Save, Eye, EyeOff, Loader2, MessageCircle, Check, ExternalLink } from 'lucide-react';
+
+const EMPTY = { title: '', content: '', author: 'Редактор', is_pinned: false };
+
+export default function NotesPage() {
+ const [notes, setNotes] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [editing, setEditing] = useState(null); // null | 'new' | note object
+ const [form, setForm] = useState(EMPTY);
+ const [saving, setSaving] = useState(false);
+ const [err, setErr] = useState('');
+
+ async function load() {
+ setLoading(true);
+ try {
+ const r = await fetch('/api/notes');
+ setNotes(await r.json());
+ } catch (e) { setErr(e.message); }
+ finally { setLoading(false); }
+ }
+
+ useEffect(() => { load(); }, []);
+
+ function startNew() {
+ setForm(EMPTY);
+ setEditing('new');
+ setErr('');
+ }
+
+ function startEdit(note) {
+ setForm({ title: note.title || '', content: note.content, author: note.author, is_pinned: note.is_pinned });
+ setEditing(note);
+ setErr('');
+ }
+
+ async function save() {
+ if (!form.content.trim()) { setErr('Текст заметки обязателен'); return; }
+ setSaving(true); setErr('');
+ try {
+ const body = { ...form, title: form.title.trim() || null };
+ if (editing === 'new') {
+ await fetch('/api/notes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+ } else {
+ await fetch(`/api/notes/${editing.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+ }
+ setEditing(null);
+ await load();
+ } catch (e) { setErr(e.message); }
+ finally { setSaving(false); }
+ }
+
+ async function togglePublish(note) {
+ await fetch(`/api/notes/${note.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_published: !note.is_published }) });
+ await load();
+ }
+
+ async function togglePin(note) {
+ await fetch(`/api/notes/${note.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_pinned: !note.is_pinned }) });
+ await load();
+ }
+
+ async function del(note) {
+ if (!confirm(`Удалить заметку «${note.title || note.content.slice(0, 40)}»?`)) return;
+ await fetch(`/api/notes/${note.id}`, { method: 'DELETE' });
+ await load();
+ }
+
+ const fmt = (d) => new Date(d).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric' });
+
+ return (
+ {note.content}