refactor(admin): remove legacy menu editor

This commit is contained in:
Simo committed 2026-09-05 20:08:19 +02:00
1 parent 98b0009206
commit 667637f8da
12 files changed
+13 -461

No files matched your search

-318
View File
@@ -1,318 +0,0 @@
"use client";
import {
ChevronDown,
ChevronUp,
Eye,
EyeOff,
RotateCcw,
Save,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import { saveAdminNavConfig } from "@/actions/admin-nav-menu";
import { Button } from "@/components/ui/button";
import {
ADMIN_NAV_PINNED_HREFS,
type AdminNavConfig,
} from "@/lib/admin-nav-config";
import { cn } from "@/lib/utils";
export type EditableNavGroup = {
labelKey: string;
items: { href: string; labelKey: string }[];
};
function moveIndex<T>(arr: T[], from: number, to: number): T[] {
if (to < 0 || to >= arr.length || from === to) return arr;
const next = [...arr];
const [item] = next.splice(from, 1);
if (item === undefined) return arr;
next.splice(to, 0, item);
return next;
}
function buildConfig(
groups: EditableNavGroup[],
hiddenGroups: Set<string>,
hiddenItems: Set<string>,
): Required<AdminNavConfig> {
return {
groupOrder: groups.map((g) => g.labelKey),
hiddenGroups: [...hiddenGroups],
hiddenItems: [...hiddenItems].filter((h) => !ADMIN_NAV_PINNED_HREFS.has(h)),
itemOrder: Object.fromEntries(
groups.map((g) => [g.labelKey, g.items.map((i) => i.href)]),
),
};
}
export function AdminMenuEditor({
catalog,
initialConfig,
canEdit,
}: {
catalog: EditableNavGroup[];
initialConfig: AdminNavConfig;
canEdit: boolean;
}) {
const tNav = useTranslations("pages.admin.nav");
const t = useTranslations("pages.admin.menu");
const [pending, startTransition] = useTransition();
const initialGroups = useMemo(() => {
const order = initialConfig.groupOrder ?? [];
const byKey = new Map(catalog.map((g) => [g.labelKey, g]));
const ordered: EditableNavGroup[] = [];
const seen = new Set<string>();
for (const key of order) {
const g = byKey.get(key);
if (!g || seen.has(key)) continue;
const itemOrder = initialConfig.itemOrder?.[key] ?? [];
const itemsByHref = new Map(g.items.map((i) => [i.href, i]));
const items = [
...itemOrder
.map((href) => itemsByHref.get(href))
.filter((i): i is NonNullable<typeof i> => Boolean(i)),
...g.items.filter((i) => !itemOrder.includes(i.href)),
];
ordered.push({ labelKey: g.labelKey, items });
seen.add(key);
}
for (const g of catalog) {
if (seen.has(g.labelKey)) continue;
ordered.push({ ...g, items: [...g.items] });
}
return ordered;
}, [catalog, initialConfig]);
const [groups, setGroups] = useState(initialGroups);
const [hiddenGroups, setHiddenGroups] = useState(
() => new Set(initialConfig.hiddenGroups ?? []),
);
const [hiddenItems, setHiddenItems] = useState(
() => new Set(initialConfig.hiddenItems ?? []),
);
function reset() {
setGroups(catalog.map((g) => ({ ...g, items: [...g.items] })));
setHiddenGroups(new Set());
setHiddenItems(new Set());
}
function save() {
if (!canEdit) return;
const config = buildConfig(groups, hiddenGroups, hiddenItems);
startTransition(async () => {
const res = await saveAdminNavConfig(config);
if (res.ok) {
toast.success(t("saved"));
} else {
toast.error(res.error ?? t("saveFailed"));
}
});
}
return (
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-2">
{canEdit ? (
<>
<Button type="button" onClick={save} disabled={pending}>
<Save className="h-4 w-4" />
{pending ? t("saving") : t("save")}
</Button>
<Button
type="button"
variant="outline"
onClick={reset}
disabled={pending}
>
<RotateCcw className="h-4 w-4" />
{t("reset")}
</Button>
</>
) : (
<p className="text-sm text-muted-foreground">{t("viewOnly")}</p>
)}
<p className="text-sm text-muted-foreground w-full sm:w-auto sm:ml-auto">
{t("hint")}
</p>
</div>
<div className="space-y-3">
{groups.map((group, gi) => {
const groupHidden = hiddenGroups.has(group.labelKey);
return (
<section
key={group.labelKey}
className={cn(
"rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-3",
groupHidden && "opacity-55",
)}
>
<div className="flex flex-wrap items-center gap-2 mb-2">
<span className="font-semibold text-sm uppercase tracking-wide flex-1 min-w-0">
{tNav(group.labelKey)}
</span>
{canEdit && (
<>
<Button
type="button"
size="icon"
variant="ghost"
disabled={gi === 0}
aria-label={t("moveUp")}
onClick={() =>
setGroups((prev) => moveIndex(prev, gi, gi - 1))
}
>
<ChevronUp className="h-4 w-4" />
</Button>
<Button
type="button"
size="icon"
variant="ghost"
disabled={gi === groups.length - 1}
aria-label={t("moveDown")}
onClick={() =>
setGroups((prev) => moveIndex(prev, gi, gi + 1))
}
>
<ChevronDown className="h-4 w-4" />
</Button>
<Button
type="button"
size="icon"
variant="ghost"
aria-label={groupHidden ? t("showGroup") : t("hideGroup")}
onClick={() =>
setHiddenGroups((prev) => {
const next = new Set(prev);
if (next.has(group.labelKey))
next.delete(group.labelKey);
else next.add(group.labelKey);
return next;
})
}
>
{groupHidden ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</>
)}
</div>
<ul className="space-y-1">
{group.items.map((item, ii) => {
const pinned = ADMIN_NAV_PINNED_HREFS.has(item.href);
const itemHidden = !pinned && hiddenItems.has(item.href);
return (
<li
key={item.href}
className={cn(
"flex flex-wrap items-center gap-2 rounded-md px-2 py-1.5 text-sm",
"bg-[var(--admin-canvas)]/60",
itemHidden && "opacity-50",
)}
>
<span className="flex-1 min-w-0 truncate">
{tNav(item.labelKey)}
<span className="ml-2 text-xs text-muted-foreground font-mono">
{item.href}
</span>
{pinned && (
<span className="ml-2 text-[0.65rem] uppercase tracking-wide text-muted-foreground">
{t("pinned")}
</span>
)}
</span>
{canEdit && (
<>
<Button
type="button"
size="icon"
variant="ghost"
disabled={ii === 0}
aria-label={t("moveUp")}
onClick={() =>
setGroups((prev) =>
prev.map((g, idx) =>
idx === gi
? {
...g,
items: moveIndex(g.items, ii, ii - 1),
}
: g,
),
)
}
>
<ChevronUp className="h-4 w-4" />
</Button>
<Button
type="button"
size="icon"
variant="ghost"
disabled={ii === group.items.length - 1}
aria-label={t("moveDown")}
onClick={() =>
setGroups((prev) =>
prev.map((g, idx) =>
idx === gi
? {
...g,
items: moveIndex(g.items, ii, ii + 1),
}
: g,
),
)
}
>
<ChevronDown className="h-4 w-4" />
</Button>
<Button
type="button"
size="icon"
variant="ghost"
disabled={pinned}
aria-label={
itemHidden ? t("showItem") : t("hideItem")
}
onClick={() => {
if (pinned) return;
setHiddenItems((prev) => {
const next = new Set(prev);
if (next.has(item.href)) next.delete(item.href);
else next.add(item.href);
return next;
});
}}
>
{itemHidden || pinned ? (
itemHidden ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4 opacity-40" />
)
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</>
)}
</li>
);
})}
</ul>
</section>
);
})}
</div>
</div>
);
}
-49
View File
@@ -1,49 +0,0 @@
import { ListOrdered } from "lucide-react";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { AdminPageShell } from "@/components/admin/admin-page-shell";
import { ADMIN_NAV_GROUPS } from "@/lib/admin-nav";
import {
ADMIN_NAV_CONFIG_KEY,
parseAdminNavConfig,
} from "@/lib/admin-nav-config";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { siteSettings } from "@/lib/services/site-settings";
import { AdminMenuEditor } from "./admin-menu-editor";
export const metadata = { title: "Admin menu" };
export default async function AdminMenuPage() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SETTINGS_VIEW, session.user.rank)) {
redirect("/admin");
}
const canEdit = canAccess(
permissions,
PERMS.SETTINGS_EDIT,
session.user.rank,
);
const t = await getTranslations("pages.admin.menu");
const catalog = ADMIN_NAV_GROUPS.map((g) => ({
labelKey: g.labelKey,
items: g.items.map((i) => ({ href: i.href, labelKey: i.labelKey })),
}));
const initialConfig = parseAdminNavConfig(
await siteSettings.get(ADMIN_NAV_CONFIG_KEY, ""),
);
return (
<AdminPageShell
icon={ListOrdered}
title={t("title")}
subtitle={t("subtitle")}
>
<AdminMenuEditor
catalog={catalog}
initialConfig={initialConfig}
canEdit={canEdit}
/>
</AdminPageShell>
);
}