"use client"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; import CategoryTreeSelect from "@/components/CategoryTreeSelect"; import CategoryTreeView, { type CategoryTreeNode, } from "@/components/CategoryTreeView"; import ConfirmDialog from "@/components/ConfirmDialog"; import type { MeshkeeCategory } from "@/services/meshkee-categories"; import { createWebsiteBlogCategory, deleteWebsiteBlogCategory, listWebsiteBlogCategories, updateWebsiteBlogCategory, type WebsiteBlogCategory, } from "@/services/website-blog"; import styles from "./BusinessCategoriesPage.module.css"; type Draft = { id?: string; parentId: string; name: string; nameFa: string; description: string; }; function emptyDraft(parentId = ""): Draft { return { parentId, name: "", nameFa: "", description: "", }; } function categoryLabel(category: { name: string; nameFa?: string | null; }) { if (category.nameFa && category.nameFa !== category.name) { return `${category.name} / ${category.nameFa}`; } return category.name; } function toTreeSelect(items: WebsiteBlogCategory[]): MeshkeeCategory[] { return items.map((item) => ({ id: item.id, parentId: item.parentId, name: item.name, nameFa: item.nameFa ?? null, slug: "", description: item.description ?? null, icon: null, sortOrder: 0, tags: [], })); } function toViewNodes(items: WebsiteBlogCategory[]): CategoryTreeNode[] { const byId = new Map(); for (const item of items) { byId.set(item.id, { id: item.id, name: categoryLabel(item), children: [], }); } const roots: CategoryTreeNode[] = []; for (const item of items) { const node = byId.get(item.id)!; if (item.parentId && byId.has(item.parentId)) { byId.get(item.parentId)!.children.push(node); } else { roots.push(node); } } const sortNodes = (nodes: CategoryTreeNode[]) => { nodes.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }), ); for (const node of nodes) sortNodes(node.children); }; sortNodes(roots); return roots; } function collectDescendantIds( rootId: string, items: WebsiteBlogCategory[], ): Set { const childrenByParent = new Map(); for (const item of items) { if (!item.parentId) continue; const list = childrenByParent.get(item.parentId) ?? []; list.push(item.id); childrenByParent.set(item.parentId, list); } const result = new Set([rootId]); const queue = [rootId]; while (queue.length) { const current = queue.shift()!; for (const childId of childrenByParent.get(current) ?? []) { if (!result.has(childId)) { result.add(childId); queue.push(childId); } } } return result; } export default function BlogCategoriesPage() { const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const [draft, setDraft] = useState(null); const [removing, setRemoving] = useState(null); const [removeBusy, setRemoveBusy] = useState(false); const tree = useMemo(() => toViewNodes(categories), [categories]); const byId = useMemo(() => { const map = new Map(); for (const item of categories) map.set(item.id, item); return map; }, [categories]); const parentOptions = useMemo(() => { const draftId = draft?.id; const source = !draftId ? categories : categories.filter( (item) => !collectDescendantIds(draftId, categories).has(item.id), ); return toTreeSelect(source); }, [categories, draft?.id]); const reload = useCallback(async () => { setLoading(true); setError(""); try { setCategories(await listWebsiteBlogCategories()); } catch (err) { setError( err instanceof Error ? err.message : "Could not load categories.", ); } finally { setLoading(false); } }, []); useEffect(() => { void reload(); }, [reload]); function openCreate(parentId = "") { setDraft(emptyDraft(parentId)); setError(""); } function openEdit(id: string) { const category = byId.get(id); if (!category) return; setDraft({ id: category.id, parentId: category.parentId ?? "", name: category.name, nameFa: category.nameFa ?? "", description: category.description ?? "", }); setError(""); } async function saveDraft() { if (!draft) return; if (!draft.name.trim()) { setError("Enter an English name."); return; } setSaving(true); setError(""); try { const body = { parentId: draft.parentId || null, name: draft.name.trim(), nameFa: draft.nameFa.trim(), description: draft.description.trim(), }; if (draft.id) { await updateWebsiteBlogCategory(draft.id, body); } else { await createWebsiteBlogCategory(body); } setDraft(null); await reload(); } catch (err) { setError( err instanceof Error ? err.message : "Could not save category.", ); } finally { setSaving(false); } } async function confirmRemove() { if (!removing) return; setRemoveBusy(true); setError(""); try { await deleteWebsiteBlogCategory(removing.id); setRemoving(null); await reload(); } catch (err) { setError( err instanceof Error ? err.message : "Could not remove category.", ); } finally { setRemoveBusy(false); } } const removingName = removing ? categoryLabel(removing) : "this category"; return (

Categories

Blog categories. Add, edit, or remove the tree.

{loading ? "Loading…" : `${categories.length} categories`}

{error ? (

{error}

) : null}
{loading ?

Loading categories…

: null} {!loading ? ( { const category = byId.get(id); if (category) setRemoving(category); }, }} /> ) : null}
{draft ? (
{ if (event.target === event.currentTarget && !saving) { setDraft(null); } }} >

Categories

{draft.id ? "Edit category" : "Add category"}

{ event.preventDefault(); void saveDraft(); }} > setDraft((current) => current ? { ...current, parentId: ids[0] ?? "" } : current, ) } /> {draft.parentId ? ( ) : null}