feat(categories): admin section + dynamic categories everywhere

New /admin/categories page:
  - list with icon, name, slug, color preview, article/topic counters
  - new/edit form (slug locked after creation since it's FK in articles/topics)
  - 12 color palette, sort_order, archive toggle (soft delete)
  - hard delete only when archived AND no articles/topics attached

AdminNav: new pin 'Категории' (FolderPlus) in 'Контент' group, before Статьи.

Dynamic categories — hardcoded CAT_LABELS removed from:
  - components/admin/AutogenPanel.js — now accepts categories prop, builds
    lookup map from DB, supports all 12 palette colors
  - app/page.js — CATEGORY_ORDER hardcode removed; renders categories in
    sort_order from DB, skips empty ones

Plumbing: app/admin/api/categories/[...path]/route.js — catch-all proxy
This commit is contained in:
Aleksei Pavlov
2026-06-19 11:57:35 +03:00
parent 0b4895bb97
commit 3f6cd28798
6 changed files with 371 additions and 20 deletions
+3 -2
View File
@@ -4,7 +4,7 @@ import { usePathname, useRouter } from 'next/navigation';
import { useState } from 'react';
import {
LayoutDashboard, FileText, Radio, Zap, Settings, LogOut, ExternalLink,
MessageCircle, Clock, Coffee, Menu, X,
MessageCircle, Clock, Coffee, Menu, X, FolderPlus,
} from 'lucide-react';
// Группы пунктов меню — масштабируется, без давки сверху
@@ -18,7 +18,8 @@ const GROUPS = [
{
label: 'Контент',
items: [
{ href: '/admin/articles', label: 'Статьи', icon: FileText },
{ href: '/admin/categories', label: 'Категории', icon: FolderPlus },
{ href: '/admin/articles', label: 'Статьи', icon: FileText },
{ href: '/admin/drafts', label: 'Черновики', icon: Clock },
{ href: '/admin/notes', label: 'Заметки', icon: MessageCircle },
{ href: '/admin/zero', label: 'Зеро', icon: Coffee },
+21 -8
View File
@@ -3,18 +3,21 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Play, Plus, Trash2, RefreshCw, Clock, CheckCircle, XCircle, Zap } from 'lucide-react';
const CAT_LABELS = {
'ai-tools': { name: 'ИИ-инструменты', icon: '🤖', color: 'emerald' },
'cybersec': { name: 'Кибербезопасность', icon: '🔒', color: 'red' },
'automation': { name: 'Автоматизация', icon: '⚡', color: 'amber' },
'ai-dev': { name: 'Разработка с ИИ', icon: '💻', color: 'blue' },
};
// Все цвета палитры — synced с /admin/categories.
// Хардкода категорий больше нет: они приходят из БД через prop categories.
const COLOR = {
emerald: 'bg-emerald-50 dark:bg-emerald-950 border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-300',
red: 'bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-800 text-red-700 dark:text-red-300',
amber: 'bg-amber-50 dark:bg-amber-950 border-amber-200 dark:border-amber-800 text-amber-700 dark:text-amber-300',
blue: 'bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300',
purple: 'bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800 text-purple-700 dark:text-purple-300',
pink: 'bg-pink-50 dark:bg-pink-950 border-pink-200 dark:border-pink-800 text-pink-700 dark:text-pink-300',
cyan: 'bg-cyan-50 dark:bg-cyan-950 border-cyan-200 dark:border-cyan-800 text-cyan-700 dark:text-cyan-300',
orange: 'bg-orange-50 dark:bg-orange-950 border-orange-200 dark:border-orange-800 text-orange-700 dark:text-orange-300',
lime: 'bg-lime-50 dark:bg-lime-950 border-lime-200 dark:border-lime-800 text-lime-700 dark:text-lime-300',
rose: 'bg-rose-50 dark:bg-rose-950 border-rose-200 dark:border-rose-800 text-rose-700 dark:text-rose-300',
slate: 'bg-slate-50 dark:bg-slate-900 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300',
neutral: 'bg-neutral-50 dark:bg-neutral-900 border-neutral-200 dark:border-neutral-700 text-neutral-700 dark:text-neutral-300',
};
/**
@@ -63,7 +66,17 @@ function fmtNextRun(date) {
});
}
export default function AutogenPanel({ status, queue, topics }) {
// catMap превращает массив категорий в lookup-объект по slug → { name, icon, color }
function buildCatMap(categories) {
const map = {};
for (const c of (categories || [])) {
map[c.slug] = { name: c.name, icon: c.icon || '📝', color: c.color || 'emerald' };
}
return map;
}
export default function AutogenPanel({ status, queue, topics, categories = [] }) {
const CAT_LABELS = buildCatMap(categories);
const router = useRouter();
const [running, setRunning] = useState({});
const [toast, setToast] = useState(null);