"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 { categoryLabel, createMeshkeeCategory, deleteMeshkeeCategory, searchMeshkeeCategories, updateMeshkeeCategory, type MeshkeeCategory, } from "@/services/meshkee-categories"; 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 toViewNodes(items: MeshkeeCategory[]): CategoryTreeNode[] { const byId = new Map(); for (const item of items) { byId.set(item.id, { id: item.id, name: categoryLabel(item), hint: item.slug ? `/${item.slug}` : undefined, 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[], source: MeshkeeCategory[]) => { const order = new Map(source.map((item) => [item.id, item.sortOrder])); nodes.sort( (a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0) || a.name.localeCompare(b.name, undefined, { sensitivity: "base" }), ); for (const node of nodes) sortNodes(node.children, source); }; sortNodes(roots, items); return roots; } function collectDescendantIds( rootId: string, items: MeshkeeCategory[], ): 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 BusinessCategoriesPage() { 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(() => { if (!draft?.id) return categories; const blocked = collectDescendantIds(draft.id, categories); return categories.filter((item) => !blocked.has(item.id)); }, [categories, draft?.id]); const reload = useCallback(async () => { setLoading(true); setError(""); try { setCategories(await searchMeshkeeCategories("")); } 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) { const updated = await updateMeshkeeCategory(draft.id, body); setCategories((current) => current.map((item) => (item.id === updated.id ? updated : item)), ); } else { const created = await createMeshkeeCategory(body); setCategories((current) => [...current, created]); } setDraft(null); } 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 { const removed = await deleteMeshkeeCategory(removing.id); const deleted = new Set( (removed.deletedIds ?? [removing.id]).map(String), ); deleted.add(removing.id); setCategories((current) => current.filter((item) => !deleted.has(item.id)), ); setRemoving(null); } catch (err) { setError( err instanceof Error ? err.message : "Could not remove category.", ); } finally { setRemoveBusy(false); } } const removingName = removing ? categoryLabel(removing) : "this category"; return (

Categories

Marketplace categories from Meshkee. 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}