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 ( +
+
+
+ +

Заметки редактора

+ + + +
+ +
+ + {/* Форма создания/редактирования */} + {editing && ( +
+
{editing === 'new' ? 'Новая заметка' : 'Редактировать'}
+ setForm(f => ({ ...f, title: e.target.value }))} + /> +