Wrap next/link in a shared Link component that ships prefetch=false by default, so no route is ever prefetched (viewport or hover) anymore, and drop the DNS prefetch hint. Removes hidden background requests that were the source of intermittent issues.
739 lines
21 KiB
TypeScript
739 lines
21 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
Check,
|
|
ChevronsUpDown,
|
|
FileText,
|
|
FolderTree,
|
|
Loader2,
|
|
Package,
|
|
Palette,
|
|
Save,
|
|
Settings,
|
|
Trash2,
|
|
} from "lucide-react";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { deleteCatalogPage, updateCatalogPage } from "@/actions/catalog";
|
|
import { CatalogImagePicker } from "@/components/admin/catalog/catalog-image-picker";
|
|
import { IconPicker } from "@/components/admin/catalog/icon-picker";
|
|
import { ImagePreview } from "@/components/admin/catalog-manager/image-preview";
|
|
import { CatalogIcon, LAYOUT_COLORS } from "@/components/admin/catalog-tree";
|
|
import { useConfirmDialog } from "@/components/admin/confirm-dialog";
|
|
import Link from "@/components/link";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from "@/components/ui/command";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { useServerAction } from "@/hooks/use-server-action";
|
|
import {
|
|
CATALOG_LAYOUTS,
|
|
type CatalogLayout,
|
|
LAYOUT_DESCRIPTIONS,
|
|
} from "@/lib/catalog-layouts";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface CatalogPageData {
|
|
id: number;
|
|
caption: string;
|
|
parentId: number;
|
|
pageLayout: string;
|
|
enabled: string;
|
|
visible: string;
|
|
minRank: number;
|
|
clubOnly: string;
|
|
orderNum: number;
|
|
iconImage: number;
|
|
iconColor: number;
|
|
pageHeadline: string;
|
|
pageTeaser: string;
|
|
pageSpecial: string | null;
|
|
pageText1: string | null;
|
|
pageText2: string | null;
|
|
pageTextDetails: string | null;
|
|
pageTextTeaser: string | null;
|
|
}
|
|
|
|
interface CatalogPageFormProps {
|
|
catalogPage: CatalogPageData;
|
|
childPages: { id: number; caption: string; enabled: string }[];
|
|
canEdit: boolean;
|
|
allPages: { id: number; caption: string }[];
|
|
}
|
|
|
|
export function CatalogPageForm({
|
|
catalogPage,
|
|
childPages,
|
|
canEdit,
|
|
allPages,
|
|
}: CatalogPageFormProps) {
|
|
const { isPending, run } = useServerAction();
|
|
const { confirm, dialog: confirmDialog } = useConfirmDialog();
|
|
const formRef = useRef<HTMLFormElement>(null);
|
|
|
|
const [form, setForm] = useState({
|
|
caption: catalogPage.caption,
|
|
parentId: catalogPage.parentId,
|
|
pageLayout: catalogPage.pageLayout,
|
|
enabled: catalogPage.enabled as "0" | "1",
|
|
visible: catalogPage.visible as "0" | "1",
|
|
minRank: catalogPage.minRank,
|
|
clubOnly: catalogPage.clubOnly as "0" | "1",
|
|
orderNum: catalogPage.orderNum,
|
|
iconImage: catalogPage.iconImage,
|
|
iconColor: catalogPage.iconColor,
|
|
pageHeadline: catalogPage.pageHeadline,
|
|
pageTeaser: catalogPage.pageTeaser,
|
|
pageSpecial: catalogPage.pageSpecial || "",
|
|
pageText1: catalogPage.pageText1 || "",
|
|
pageText2: catalogPage.pageText2 || "",
|
|
pageTextDetails: catalogPage.pageTextDetails || "",
|
|
pageTextTeaser: catalogPage.pageTextTeaser || "",
|
|
});
|
|
|
|
// Ctrl+S / Cmd+S to save
|
|
useEffect(() => {
|
|
if (!canEdit) return;
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
e.preventDefault();
|
|
formRef.current?.requestSubmit();
|
|
}
|
|
}
|
|
document.addEventListener("keydown", onKeyDown);
|
|
return () => document.removeEventListener("keydown", onKeyDown);
|
|
}, [canEdit]);
|
|
|
|
function update<K extends keyof typeof form>(
|
|
key: K,
|
|
value: (typeof form)[K],
|
|
) {
|
|
setForm((prev) => ({ ...prev, [key]: value }));
|
|
}
|
|
|
|
function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
run(() => updateCatalogPage({ id: catalogPage.id, ...form }), {
|
|
successMessage: "Page updated and catalog synced.",
|
|
errorMessage: "Failed to update.",
|
|
});
|
|
}
|
|
|
|
async function handleDelete() {
|
|
const childCount = childPages.length;
|
|
const description =
|
|
childCount > 0
|
|
? `Delete this catalog page? Its ${childCount} child page(s) will be moved to the parent. All items on this page will be deleted.`
|
|
: "Delete this catalog page and all its items?";
|
|
const ok = await confirm({
|
|
title: "Delete catalog page",
|
|
description,
|
|
confirmLabel: "Delete",
|
|
});
|
|
if (!ok) return;
|
|
run(() => deleteCatalogPage({ id: catalogPage.id }), {
|
|
successMessage: "Page deleted.",
|
|
redirectTo: "/admin/catalog",
|
|
});
|
|
}
|
|
|
|
const layoutColorClass =
|
|
LAYOUT_COLORS[form.pageLayout] || "bg-muted text-muted-foreground";
|
|
|
|
return (
|
|
<>
|
|
{confirmDialog}
|
|
<form ref={formRef} onSubmit={handleSubmit}>
|
|
<div className="grid gap-4 lg:grid-cols-3">
|
|
{/* ── Left column (2/3): tabs ─────────────────────────── */}
|
|
<div className="lg:col-span-2 space-y-4">
|
|
<Card>
|
|
<CardContent className="pt-4">
|
|
<Tabs defaultValue="general">
|
|
<TabsList className="mb-4">
|
|
<TabsTrigger value="general" className="gap-2">
|
|
<Settings className="h-4 w-4" />
|
|
General
|
|
</TabsTrigger>
|
|
<TabsTrigger value="texts" className="gap-2">
|
|
<FileText className="h-4 w-4" />
|
|
Texts
|
|
</TabsTrigger>
|
|
<TabsTrigger value="appearance" className="gap-2">
|
|
<Palette className="h-4 w-4" />
|
|
Appearance
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* ── General ──────────────────────────────────── */}
|
|
<TabsContent value="general" className="space-y-4">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="caption">Caption</Label>
|
|
<Input
|
|
id="caption"
|
|
value={form.caption}
|
|
onChange={(e) => update("caption", e.target.value)}
|
|
disabled={!canEdit}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Parent Page</Label>
|
|
<ParentCombobox
|
|
value={form.parentId}
|
|
onChange={(id) => update("parentId", id)}
|
|
pages={allPages}
|
|
currentPageId={catalogPage.id}
|
|
disabled={!canEdit}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>Layout</Label>
|
|
<LayoutSelect
|
|
value={form.pageLayout}
|
|
onChange={(v) => update("pageLayout", v)}
|
|
disabled={!canEdit}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
{LAYOUT_DESCRIPTIONS[
|
|
form.pageLayout as CatalogLayout
|
|
] ?? "Custom layout"}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="orderNum">Order</Label>
|
|
<Input
|
|
id="orderNum"
|
|
type="number"
|
|
value={form.orderNum}
|
|
onChange={(e) =>
|
|
update(
|
|
"orderNum",
|
|
parseInt(e.target.value, 10) || 0,
|
|
)
|
|
}
|
|
disabled={!canEdit}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="minRank">Min Rank</Label>
|
|
<Input
|
|
id="minRank"
|
|
type="number"
|
|
min={1}
|
|
max={100}
|
|
value={form.minRank}
|
|
onChange={(e) =>
|
|
update("minRank", parseInt(e.target.value, 10) || 1)
|
|
}
|
|
disabled={!canEdit}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* ── Texts ────────────────────────────────────── */}
|
|
<TabsContent value="texts" className="space-y-4">
|
|
<div className="space-y-1.5">
|
|
<Label>Headline</Label>
|
|
<CatalogImagePicker
|
|
value={form.pageHeadline}
|
|
onChange={(v) => update("pageHeadline", v)}
|
|
type="header"
|
|
label="Headline image"
|
|
disabled={!canEdit}
|
|
/>
|
|
<ImagePreview value={form.pageHeadline} height={50} />
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>Teaser</Label>
|
|
<CatalogImagePicker
|
|
value={form.pageTeaser}
|
|
onChange={(v) => update("pageTeaser", v)}
|
|
type="teaser"
|
|
label="Teaser image"
|
|
disabled={!canEdit}
|
|
/>
|
|
<ImagePreview value={form.pageTeaser} height={50} />
|
|
</div>
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<TextareaField
|
|
id="pageText1"
|
|
label="Text 1"
|
|
value={form.pageText1}
|
|
onChange={(v) => update("pageText1", v)}
|
|
disabled={!canEdit}
|
|
rows={4}
|
|
max={5000}
|
|
/>
|
|
<TextareaField
|
|
id="pageText2"
|
|
label="Text 2"
|
|
value={form.pageText2}
|
|
onChange={(v) => update("pageText2", v)}
|
|
disabled={!canEdit}
|
|
rows={4}
|
|
max={5000}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<TextareaField
|
|
id="pageTextDetails"
|
|
label="Details"
|
|
value={form.pageTextDetails}
|
|
onChange={(v) => update("pageTextDetails", v)}
|
|
disabled={!canEdit}
|
|
rows={3}
|
|
max={5000}
|
|
/>
|
|
<TextareaField
|
|
id="pageTextTeaser"
|
|
label="Text Teaser"
|
|
value={form.pageTextTeaser}
|
|
onChange={(v) => update("pageTextTeaser", v)}
|
|
disabled={!canEdit}
|
|
rows={3}
|
|
max={5000}
|
|
/>
|
|
</div>
|
|
<TextareaField
|
|
id="pageSpecial"
|
|
label="Special"
|
|
value={form.pageSpecial}
|
|
onChange={(v) => update("pageSpecial", v)}
|
|
disabled={!canEdit}
|
|
rows={2}
|
|
max={2048}
|
|
/>
|
|
</TabsContent>
|
|
|
|
{/* ── Appearance ───────────────────────────────── */}
|
|
<TabsContent value="appearance" className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>Icon</Label>
|
|
<div className="flex items-center gap-3">
|
|
<IconPicker
|
|
value={form.iconImage}
|
|
onChange={(id) => update("iconImage", id)}
|
|
disabled={!canEdit}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Icon shown in the catalog tree navigation.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="iconColor">Icon Color</Label>
|
|
<div className="flex items-center gap-2">
|
|
<Input
|
|
id="iconColor"
|
|
type="number"
|
|
min={0}
|
|
className="w-32"
|
|
value={form.iconColor}
|
|
onChange={(e) =>
|
|
update(
|
|
"iconColor",
|
|
parseInt(e.target.value, 10) || 0,
|
|
)
|
|
}
|
|
disabled={!canEdit}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Legacy tint value. Leave at default unless you need to
|
|
match a specific theme.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Child pages list */}
|
|
{childPages.length > 0 && (
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<FolderTree className="h-4 w-4" />
|
|
Child Pages ({childPages.length})
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-1">
|
|
{childPages.map((child) => (
|
|
<Link
|
|
key={child.id}
|
|
href={`/admin/catalog/${child.id}`}
|
|
className="flex items-center justify-between rounded-md px-3 py-2 hover:bg-accent text-sm"
|
|
>
|
|
<span>
|
|
{child.caption}
|
|
<span className="ml-2 text-muted-foreground font-mono text-xs">
|
|
#{child.id}
|
|
</span>
|
|
</span>
|
|
<Badge
|
|
variant={
|
|
child.enabled === "1" ? "default" : "outline"
|
|
}
|
|
>
|
|
{child.enabled === "1" ? "Enabled" : "Disabled"}
|
|
</Badge>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
|
|
{/* ── Right column (1/3): status sticky ────────────────── */}
|
|
<div className="lg:col-span-1">
|
|
<Card className="lg:sticky lg:top-4">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-base">Status & Preview</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Preview */}
|
|
<div className="flex items-center gap-3 rounded-md border bg-muted/30 p-3">
|
|
<div className="flex h-12 w-12 items-center justify-center rounded bg-background shrink-0">
|
|
<CatalogIcon iconImage={form.iconImage} size={36} />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="truncate text-sm font-medium">
|
|
{form.caption || "Untitled"}
|
|
</p>
|
|
<Badge
|
|
className={cn(
|
|
"mt-1 h-5 px-1.5 text-[10px] font-mono border-0",
|
|
layoutColorClass,
|
|
)}
|
|
>
|
|
{form.pageLayout}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Toggles */}
|
|
<div className="space-y-3">
|
|
<ToggleRow
|
|
id="tg-enabled"
|
|
label="Enabled"
|
|
description="Active in the catalog"
|
|
checked={form.enabled === "1"}
|
|
onChange={(v) => update("enabled", v ? "1" : "0")}
|
|
disabled={!canEdit}
|
|
/>
|
|
<ToggleRow
|
|
id="tg-visible"
|
|
label="Visible"
|
|
description="Shown in navigation"
|
|
checked={form.visible === "1"}
|
|
onChange={(v) => update("visible", v ? "1" : "0")}
|
|
disabled={!canEdit}
|
|
/>
|
|
<ToggleRow
|
|
id="tg-club"
|
|
label="Club Only"
|
|
description="Requires HC subscription"
|
|
checked={form.clubOnly === "1"}
|
|
onChange={(v) => update("clubOnly", v ? "1" : "0")}
|
|
disabled={!canEdit}
|
|
/>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="flex items-center gap-4 border-t pt-3 text-xs text-muted-foreground">
|
|
<span className="flex items-center gap-1">
|
|
<FolderTree className="h-3.5 w-3.5" />
|
|
{childPages.length} sub
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Package className="h-3.5 w-3.5" />#{catalogPage.id}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
{canEdit && (
|
|
<div className="space-y-2 border-t pt-3">
|
|
<Button
|
|
type="submit"
|
|
disabled={isPending}
|
|
className="w-full"
|
|
>
|
|
{isPending ? (
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Save className="mr-2 h-4 w-4" />
|
|
)}
|
|
Save & Sync
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
onClick={handleDelete}
|
|
disabled={isPending}
|
|
className="w-full"
|
|
>
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
Delete Page
|
|
</Button>
|
|
<p className="text-center text-[10px] text-muted-foreground">
|
|
Press <kbd className="rounded border px-1">Ctrl+S</kbd> to
|
|
save
|
|
</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ── Toggle row ─────────────────────────────────────────────────
|
|
|
|
function ToggleRow({
|
|
id,
|
|
label,
|
|
description,
|
|
checked,
|
|
onChange,
|
|
disabled,
|
|
}: {
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
checked: boolean;
|
|
onChange: (v: boolean) => void;
|
|
disabled?: boolean;
|
|
}) {
|
|
return (
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0 flex-1">
|
|
<Label htmlFor={id} className="text-sm font-medium cursor-pointer">
|
|
{label}
|
|
</Label>
|
|
<p className="text-[11px] text-muted-foreground">{description}</p>
|
|
</div>
|
|
<Switch
|
|
id={id}
|
|
checked={checked}
|
|
onCheckedChange={onChange}
|
|
disabled={disabled}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TextareaField({
|
|
id,
|
|
label,
|
|
value,
|
|
onChange,
|
|
disabled,
|
|
rows = 3,
|
|
max,
|
|
}: {
|
|
id: string;
|
|
label: string;
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
disabled?: boolean;
|
|
rows?: number;
|
|
max?: number;
|
|
}) {
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor={id}>{label}</Label>
|
|
{max && (
|
|
<span className="text-[10px] tabular-nums text-muted-foreground">
|
|
{value.length}/{max}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<Textarea
|
|
id={id}
|
|
rows={rows}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
disabled={disabled}
|
|
maxLength={max}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Layout select with color swatch ────────────────────────────
|
|
|
|
function LayoutSelect({
|
|
value,
|
|
onChange,
|
|
disabled,
|
|
}: {
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
disabled?: boolean;
|
|
}) {
|
|
return (
|
|
<Select value={value} onValueChange={onChange} disabled={disabled}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{CATALOG_LAYOUTS.map((layout) => {
|
|
const colorClass =
|
|
LAYOUT_COLORS[layout] || "bg-muted text-muted-foreground";
|
|
return (
|
|
<SelectItem key={layout} value={layout}>
|
|
<span className="flex items-center gap-2">
|
|
<span
|
|
className={cn(
|
|
"inline-block h-2 w-2 rounded-full",
|
|
colorClass,
|
|
)}
|
|
/>
|
|
{layout}
|
|
</span>
|
|
</SelectItem>
|
|
);
|
|
})}
|
|
</SelectContent>
|
|
</Select>
|
|
);
|
|
}
|
|
|
|
// ── Parent page combobox with search ────────────────────────────
|
|
|
|
function ParentCombobox({
|
|
value,
|
|
onChange,
|
|
pages,
|
|
currentPageId,
|
|
disabled,
|
|
}: {
|
|
value: number;
|
|
onChange: (id: number) => void;
|
|
pages: { id: number; caption: string }[];
|
|
currentPageId: number;
|
|
disabled?: boolean;
|
|
}) {
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const options = pages.filter((p) => p.id !== currentPageId);
|
|
const selected =
|
|
value === -1
|
|
? { id: -1, caption: "Root" }
|
|
: pages.find((p) => p.id === value);
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={open}
|
|
disabled={disabled}
|
|
className="w-full justify-between font-normal"
|
|
>
|
|
{selected ? (
|
|
<span className="truncate">
|
|
<span className="text-muted-foreground font-mono text-xs mr-1.5">
|
|
{selected.id === -1 ? "—" : `#${selected.id}`}
|
|
</span>
|
|
{selected.caption}
|
|
</span>
|
|
) : (
|
|
<span className="text-muted-foreground">Select parent...</span>
|
|
)}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="w-[--radix-popover-trigger-width] p-0"
|
|
align="start"
|
|
>
|
|
<Command>
|
|
<CommandInput placeholder="Search pages..." />
|
|
<CommandList>
|
|
<CommandEmpty>No page found.</CommandEmpty>
|
|
<CommandGroup>
|
|
<CommandItem
|
|
value="root"
|
|
onSelect={() => {
|
|
onChange(-1);
|
|
setOpen(false);
|
|
}}
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
value === -1 ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<span className="text-muted-foreground font-mono text-xs mr-1.5">
|
|
—
|
|
</span>
|
|
Root
|
|
</CommandItem>
|
|
{options.map((page) => (
|
|
<CommandItem
|
|
key={page.id}
|
|
value={`${page.id} ${page.caption}`}
|
|
onSelect={() => {
|
|
onChange(page.id);
|
|
setOpen(false);
|
|
}}
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
value === page.id ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
<span className="text-muted-foreground font-mono text-xs mr-1.5">
|
|
#{page.id}
|
|
</span>
|
|
<span className="truncate">{page.caption}</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|