"use client"; import { useState, useEffect, useCallback } from "react"; import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { Card, Toggle } from "@/shared/components"; import { cn } from "@/shared/utils/cn"; import { useTranslations } from "next-intl"; import { HIDDEN_SIDEBAR_GROUP_LABELS_SETTING_KEY, HIDEABLE_SIDEBAR_GROUP_IDS, normalizeHiddenSidebarGroupLabels, type HideableSidebarGroupId, } from "@/shared/constants/sidebarGroupVisibility"; import { HIDDEN_SIDEBAR_ITEMS_SETTING_KEY, SIDEBAR_SECTION_ORDER_KEY, SIDEBAR_ITEM_ORDER_KEY, SIDEBAR_PRESET_KEY, SIDEBAR_SETTINGS_UPDATED_EVENT, SIDEBAR_SECTIONS, SIDEBAR_PRESETS, applySectionOrder, applyItemOrder, normalizeHiddenSidebarItems, type HideableSidebarItemId, type SidebarSectionId, type SidebarItemOrder, type SidebarPresetId, type SidebarSectionDefinition, type SidebarSectionChild, type SidebarItemDefinition, type SidebarItemGroup, } from "@/shared/constants/sidebarVisibility"; // ─── Sortable section row ────────────────────────────────────────────────────── interface SortableSectionProps { section: SidebarSectionDefinition & { title: string }; hiddenSet: Set; hiddenGroupLabelsSet: Set; itemOrder: string[]; onToggleItem: (id: HideableSidebarItemId) => void; onToggleGroupLabel: (id: HideableSidebarGroupId) => void; onItemReorder: (sectionId: SidebarSectionId, newOrder: string[]) => void; getLabel: (key: string, fallback: string) => string; } function SortableSection({ section, hiddenSet, hiddenGroupLabelsSet, itemOrder, onToggleItem, onToggleGroupLabel, onItemReorder, getLabel, }: SortableSectionProps) { const tSidebar = useTranslations("sidebar"); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: section.id, }); const style = { transform: CSS.Transform.toString(transform), transition }; const [expanded, setExpanded] = useState(true); const allChildren = section.children as SidebarSectionChild[]; const getChildId = (c: SidebarSectionChild) => "type" in c && c.type === "group" ? c.id : (c as SidebarItemDefinition).id; const orderedChildren = applyItemOrder(allChildren, itemOrder); const childIds = orderedChildren.map(getChildId); const sensors = useSensors(useSensor(PointerSensor)); const handleItemDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIdx = childIds.indexOf(active.id as string); const newIdx = childIds.indexOf(over.id as string); if (oldIdx === -1 || newIdx === -1) return; onItemReorder(section.id as SidebarSectionId, arrayMove(childIds, oldIdx, newIdx)); }; return (
{/* Section header */}
{/* Section children with inner DnD */} {expanded && (
{orderedChildren.map((child) => { if ("type" in child && child.type === "group") { const group = child as SidebarItemGroup; return ( ); } const item = child as SidebarItemDefinition; return ( ); })}
)}
); } // ─── Sortable child row wrapper ──────────────────────────────────────────────── function SortableChildRow({ id, children }: { id: string; children: React.ReactNode }) { const tSidebar = useTranslations("sidebar"); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, }); const style = { transform: CSS.Transform.toString(transform), transition }; return (
{children}
); } // ─── Item row ───────────────────────────────────────────────────────────────── interface ItemRowProps { item: SidebarItemDefinition; hiddenSet: Set; onToggleItem: (id: HideableSidebarItemId) => void; getLabel: (key: string, fallback: string) => string; } // Items that must always remain visible (safety guard) const PROTECTED_ITEM_IDS = new Set(["settings-sidebar"]); function ItemRow({ item, hiddenSet, onToggleItem, getLabel }: ItemRowProps) { const tSidebar = useTranslations("sidebar"); const isProtected = PROTECTED_ITEM_IDS.has(item.id); return (
{item.icon}

{getLabel(item.i18nKey, item.id)}

{isProtected ? ( lock ) : ( onToggleItem(item.id)} /> )}
); } // ─── Group row (items inside group, no sub-DnD) ─────────────────────────────── interface GroupRowProps { group: SidebarItemGroup; hiddenSet: Set; hiddenGroupLabelsSet: Set; onToggleItem: (id: HideableSidebarItemId) => void; onToggleGroupLabel: (id: HideableSidebarGroupId) => void; getLabel: (key: string, fallback: string) => string; } function GroupRow({ group, hiddenSet, hiddenGroupLabelsSet, onToggleItem, onToggleGroupLabel, getLabel, }: GroupRowProps) { const [open, setOpen] = useState(true); const groupId = group.id as HideableSidebarGroupId; const canToggleSeparator = HIDEABLE_SIDEBAR_GROUP_IDS.includes(groupId); const separatorVisible = !hiddenGroupLabelsSet.has(groupId); const separatorLabel = getLabel("groupSeparatorLabel", "Separator"); return (
{group.items.filter((i) => !hiddenSet.has(i.id)).length}/{group.items.length} {canToggleSeparator && (
{separatorLabel} onToggleGroupLabel(groupId)} />
)}
{open && (
{group.items.map((item) => (
{item.icon}

{getLabel(item.i18nKey, item.id)}

onToggleItem(item.id)} />
))}
)}
); } // ─── Main component ─────────────────────────────────────────────────────────── export default function SidebarTab() { const t = useTranslations("settings"); const tSidebar = useTranslations("sidebar"); const getLabel = useCallback( (key: string, fallback: string) => typeof tSidebar.has === "function" && tSidebar.has(key) ? tSidebar(key) : fallback, [tSidebar] ); const getSettingsLabel = useCallback( (key: string, fallback: string) => typeof t.has === "function" && t.has(key) ? t(key) : fallback, [t] ); const [loading, setLoading] = useState(true); const [hiddenSidebarItems, setHiddenSidebarItems] = useState([]); const [hiddenSidebarGroupLabels, setHiddenSidebarGroupLabels] = useState< HideableSidebarGroupId[] >([]); const [sectionOrder, setSectionOrder] = useState([]); const [itemOrder, setItemOrder] = useState({}); const [activePreset, setActivePreset] = useState(null); const [confirmPreset, setConfirmPreset] = useState(null); const [showDebug, setShowDebug] = useState(false); useEffect(() => { fetch("/api/settings") .then((r) => r.json()) .then((data) => { setHiddenSidebarItems( normalizeHiddenSidebarItems(data?.[HIDDEN_SIDEBAR_ITEMS_SETTING_KEY]) ); setHiddenSidebarGroupLabels( normalizeHiddenSidebarGroupLabels(data?.[HIDDEN_SIDEBAR_GROUP_LABELS_SETTING_KEY]) ); setSectionOrder( Array.isArray(data?.[SIDEBAR_SECTION_ORDER_KEY]) ? data[SIDEBAR_SECTION_ORDER_KEY] : [] ); setItemOrder( data?.[SIDEBAR_ITEM_ORDER_KEY] && typeof data[SIDEBAR_ITEM_ORDER_KEY] === "object" ? data[SIDEBAR_ITEM_ORDER_KEY] : {} ); setActivePreset(data?.[SIDEBAR_PRESET_KEY] ?? null); setShowDebug(data?.debugMode === true); setLoading(false); }) .catch(() => setLoading(false)); }, []); const patch = async (updates: Record) => { try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updates), }); if (res.ok) { window.dispatchEvent(new CustomEvent(SIDEBAR_SETTINGS_UPDATED_EVENT, { detail: updates })); } else { console.error("Failed to update sidebar settings:", res.statusText); } } catch (err) { console.error("Error updating sidebar settings:", err); } }; const toggleItem = (id: HideableSidebarItemId) => { // Protected items can never be hidden if (PROTECTED_ITEM_IDS.has(id)) return; const next = hiddenSidebarItems.includes(id) ? hiddenSidebarItems.filter((x) => x !== id) : [...hiddenSidebarItems, id]; setHiddenSidebarItems(next); // Any manual change → custom mode setActivePreset(null); patch({ [HIDDEN_SIDEBAR_ITEMS_SETTING_KEY]: next, [SIDEBAR_PRESET_KEY]: null }); }; const hiddenSet = new Set(hiddenSidebarItems); const hiddenGroupLabelsSet = new Set(hiddenSidebarGroupLabels); const toggleGroupLabel = (id: HideableSidebarGroupId) => { const next = hiddenSidebarGroupLabels.includes(id) ? hiddenSidebarGroupLabels.filter((x) => x !== id) : [...hiddenSidebarGroupLabels, id]; setHiddenSidebarGroupLabels(next); setActivePreset(null); patch({ [HIDDEN_SIDEBAR_GROUP_LABELS_SETTING_KEY]: next, [SIDEBAR_PRESET_KEY]: null }); }; const visibleSections = SIDEBAR_SECTIONS.filter((s) => s.visibility !== "debug" || showDebug).map( (s) => ({ ...s, title: getLabel(s.titleKey, s.titleFallback) }) ); const orderedSections = applySectionOrder(visibleSections, sectionOrder); const sectionIds = orderedSections.map((s) => s.id); const sensors = useSensors(useSensor(PointerSensor)); const handleSectionDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIdx = sectionIds.indexOf(active.id as SidebarSectionId); const newIdx = sectionIds.indexOf(over.id as SidebarSectionId); if (oldIdx === -1 || newIdx === -1) return; const newOrder = arrayMove(sectionIds, oldIdx, newIdx) as SidebarSectionId[]; setSectionOrder(newOrder); setActivePreset(null); patch({ [SIDEBAR_SECTION_ORDER_KEY]: newOrder, [SIDEBAR_PRESET_KEY]: null }); }; const handleItemReorder = (sectionId: SidebarSectionId, newOrder: string[]) => { const next = { ...itemOrder, [sectionId]: newOrder }; setItemOrder(next); setActivePreset(null); patch({ [SIDEBAR_ITEM_ORDER_KEY]: next, [SIDEBAR_PRESET_KEY]: null }); }; const applyPreset = (presetId: SidebarPresetId) => { const preset = SIDEBAR_PRESETS.find((p) => p.id === presetId); if (!preset) return; // Ensure protected items are never hidden, even if a preset includes them const safeHidden = preset.hiddenItems.filter((id) => !PROTECTED_ITEM_IDS.has(id)); setHiddenSidebarItems(safeHidden); setHiddenSidebarGroupLabels([]); setSectionOrder([]); setItemOrder({}); setActivePreset(presetId); setConfirmPreset(null); patch({ [HIDDEN_SIDEBAR_ITEMS_SETTING_KEY]: safeHidden, [HIDDEN_SIDEBAR_GROUP_LABELS_SETTING_KEY]: [], [SIDEBAR_SECTION_ORDER_KEY]: [], [SIDEBAR_ITEM_ORDER_KEY]: {}, [SIDEBAR_PRESET_KEY]: presetId, }); }; const resetToDefault = () => applyPreset("all"); const presetLabels: Record = { all: getSettingsLabel("presetAll", "All"), minimal: getSettingsLabel("presetMinimal", "Minimal"), developer: getSettingsLabel("presetDeveloper", "Developer"), admin: getSettingsLabel("presetAdmin", "Admin"), }; const presetDescriptions: Record = { all: getSettingsLabel("presetAllDesc", "Show everything"), minimal: getSettingsLabel("presetMinimalDesc", "Core pages only"), developer: getSettingsLabel("presetDeveloperDesc", "Dev & proxy tools"), admin: getSettingsLabel("presetAdminDesc", "Monitoring & audit"), }; return (

{getSettingsLabel("settingsSidebarTitle", "Sidebar Customization")}

{getSettingsLabel( "settingsSidebarDesc", "Control which items appear in the sidebar and their order" )}

{/* Presets */}

{getSettingsLabel("sidebarPresets", "Presets")}

{getSettingsLabel( "sidebarPresetsDesc", "Start from a role-based layout. Any change after applying a preset switches to Custom." )}

{/* Active preset badge */}
{getSettingsLabel("activePresetLabel", "Active:")} {activePreset ? presetLabels[activePreset] : getSettingsLabel("presetCustom", "Custom")}
{SIDEBAR_PRESETS.map((preset) => { const isActive = activePreset === preset.id; return ( ); })}
{/* Confirm preset dialog */} {confirmPreset && (
warning

{getSettingsLabel( "presetConfirmWarning", `Applying "${presetLabels[confirmPreset]}" will replace your current visibility and order settings.` )}

)}
{/* Visibility & order */}

{getSettingsLabel("sidebarOrder", "Visibility & Order")}

{getSettingsLabel( "sidebarOrderDesc", "Toggle items on/off and drag to reorder sections and their entries." )}

{orderedSections.map((section) => ( ))}

{getSettingsLabel( "sidebarVisibilityHint", "A sidebar section hides automatically when all of its entries are hidden" )}

); }