import { useState, useEffect, useCallback } from 'react' import { Link } from 'react-router-dom' import { api } from '../../lib/api' import { useDocumentTitle } from '../../hooks/useDocumentTitle' import { useFocusTrap } from '../../hooks/useFocusTrap' import { useAdmin } from '../../hooks/useAdmin' import { useConfirm } from '../../hooks/useConfirm' import { useToast } from '../../hooks/useToast' import Dropdown from '../../components/Dropdown' import NumberInput from '../../components/NumberInput' import IconPicker from '../../components/IconPicker' import BoardIcon from '../../components/BoardIcon' interface Board { id: string slug: string name: string description: string _count?: { posts: number } isArchived: boolean iconName: string | null iconColor: string | null voteBudget: number voteBudgetReset: string rssEnabled: boolean rssFeedCount: number staleDays: number position: number } export default function AdminBoards() { useDocumentTitle('Manage Boards') const { isSuperAdmin } = useAdmin() const confirm = useConfirm() const toast = useToast() const [boards, setBoards] = useState([]) const [loading, setLoading] = useState(true) const [editBoard, setEditBoard] = useState(null) const [dragIdx, setDragIdx] = useState(null) const [insertAt, setInsertAt] = useState(null) const [showCreate, setShowCreate] = useState(false) const [form, setForm] = useState({ name: '', slug: '', description: '', iconName: null as string | null, iconColor: null as string | null, voteBudget: 10, voteBudgetReset: 'monthly', rssEnabled: true, rssFeedCount: 50, staleDays: 0, }) const [saving, setSaving] = useState(false) const boardTrapRef = useFocusTrap(showCreate) const fetchBoards = async () => { try { const b = await api.get('/admin/boards') setBoards(b) } catch {} finally { setLoading(false) } } useEffect(() => { fetchBoards() }, []) const handleDrop = useCallback(() => { if (dragIdx === null || insertAt === null) return setBoards((prev) => { const next = [...prev] const [moved] = next.splice(dragIdx, 1) next.splice(insertAt > dragIdx ? insertAt - 1 : insertAt, 0, moved) api.put('/admin/boards/reorder', { boardIds: next.map((b) => b.id) }).catch(() => { toast.error('Failed to save order') }) return next }) setDragIdx(null) setInsertAt(null) }, [dragIdx, insertAt, toast]) const resetForm = () => { setForm({ name: '', slug: '', description: '', iconName: null, iconColor: null, voteBudget: 10, voteBudgetReset: 'monthly', rssEnabled: true, rssFeedCount: 50, staleDays: 0 }) setEditBoard(null) setShowCreate(false) } const openEdit = (b: Board) => { setEditBoard(b) setForm({ name: b.name, slug: b.slug, description: b.description, iconName: b.iconName, iconColor: b.iconColor, voteBudget: b.voteBudget, voteBudgetReset: b.voteBudgetReset, rssEnabled: b.rssEnabled, rssFeedCount: b.rssFeedCount, staleDays: b.staleDays ?? 0, }) setShowCreate(true) } const handleSave = async () => { setSaving(true) try { if (editBoard) { await api.put(`/admin/boards/${editBoard.id}`, form) toast.success('Board updated') } else { await api.post('/admin/boards', form) toast.success('Board created') } resetForm() fetchBoards() } catch { toast.error('Failed to save board') } finally { setSaving(false) } } const handleArchive = async (id: string, isArchived: boolean) => { try { await api.put(`/admin/boards/${id}`, { isArchived: !isArchived }) fetchBoards() toast.info(isArchived ? 'Board restored' : 'Board archived') } catch { toast.error('Failed to update board') } } const handleDelete = async (board: Board) => { const ok = await confirm( `Permanently delete "${board.name}" and all its posts, comments, and votes? This cannot be undone.` ) if (!ok) return try { await api.delete(`/admin/boards/${board.id}`) fetchBoards() toast.success('Board deleted') } catch { toast.error('Failed to delete board') } } const slugify = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') return (

Boards

Back
{loading ? (
{[0, 1, 2].map((i) => (
))}
) : (
{boards.map((board, i) => (
{dragIdx !== null && insertAt === i && dragIdx !== i && dragIdx !== i - 1 && (
)}
setDragIdx(i)} onDragOver={(e) => { e.preventDefault(); setInsertAt(i) }} onDragEnd={handleDrop} >

{board.name}

{board.isArchived && ( archived )}

/{board.slug} - {board._count?.posts ?? 0} posts - Budget: {board.voteBudget}/{board.voteBudgetReset}

{isSuperAdmin && ( )}
{dragIdx !== null && insertAt === boards.length && i === boards.length - 1 && dragIdx !== i && (
)}
))} {boards.length === 0 && (

No boards yet

)}
)} {/* Create/Edit modal */} {showCreate && (
e.stopPropagation()} onKeyDown={(e) => e.key === 'Escape' && resetForm()} >

{editBoard ? 'Edit Board' : 'New Board'}

{ setForm((f) => ({ ...f, name: e.target.value, slug: editBoard ? f.slug : slugify(e.target.value), })) }} placeholder="Feature Requests" />
setForm((f) => ({ ...f, slug: e.target.value }))} placeholder="feature-requests" />