"use client"; import { AlertCircle, ChevronDown, ChevronRight, ExternalLink, Languages, Loader2, Package, Save, Settings, } from "lucide-react"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { updateCatalogPage } from "@/actions/catalog"; import { updateBcPage } from "@/actions/catalog-bc"; import type { CatalogItemData } from "@/app/admin/catalog/[id]/catalog-items-table"; import { CatalogItemsTable } from "@/app/admin/catalog/[id]/catalog-items-table"; import { CatalogTranslateTab } from "@/app/admin/catalog/[id]/catalog-translate-tab"; import { BulkImportItems } from "@/components/admin/catalog/bulk-import-items"; import { CatalogImagePicker } from "@/components/admin/catalog/catalog-image-picker"; import { IconPicker } from "@/components/admin/catalog/icon-picker"; import { ItemsShopPreview } from "@/components/admin/catalog/items-shop-preview"; import { QuickAddFurni } from "@/components/admin/catalog/quick-add-furni"; import { CatalogIcon, LAYOUT_COLORS } from "@/components/admin/catalog-tree"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; 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 { CATALOG_LAYOUTS, type CatalogLayout, LAYOUT_DESCRIPTIONS, } from "@/lib/catalog-layouts"; import { cn } from "@/lib/utils"; import { ImagePreview } from "./image-preview"; import { catalogTreeQs, useCatalogManager } from "./use-catalog-store"; interface PageDetail { 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; itemCount?: number; } interface ItemsData { items: CatalogItemData[]; baseItems: { id: number; publicName: string; itemName: string; spriteId: number; type: string; }[]; catalogNameMap: Record; furniDataIdList: number[]; furniDescriptionMap: Record; furniRevisionMap: Record; interactionTypes: string[]; allPages: { id: number; caption: string }[]; gamedataHotel?: string; } export interface PageUpdateData { id: number; caption: string; enabled: string; visible: string; pageLayout: string; iconImage: number; } interface InlineEditorProps { pageId: number | null; onSaved: (updated: PageUpdateData) => void; } type FormState = Record; export function InlineEditor({ pageId, onSaved }: InlineEditorProps) { const { dispatch, catalogType, canEdit } = useCatalogManager(); const catQs = catalogTreeQs(catalogType); const pageHref = catalogType === "bc" ? `/admin/catalog/builder-club/${pageId}` : `/admin/catalog/${pageId}`; const [page, setPage] = useState(null); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [form, setForm] = useState({}); const [original, setOriginal] = useState({}); const [showTexts, setShowTexts] = useState(false); const [activeTab, setActiveTab] = useState("settings"); const [showPreview, setShowPreview] = useState(false); // Items/translate data (lazy loaded) const [itemsData, setItemsData] = useState(null); const [itemsLoading, setItemsLoading] = useState(false); const [itemsLoaded, setItemsLoaded] = useState(false); // ── Dirty tracking ────────────────────────────────────────── const isDirty = useMemo(() => { return Object.keys(form).some((k) => form[k] !== original[k]); }, [form, original]); useEffect(() => { dispatch({ type: "SET_DIRTY", dirty: isDirty }); }, [isDirty, dispatch]); // ── Ctrl+S handler ────────────────────────────────────────── useEffect(() => { function onKeyDown(e: KeyboardEvent) { if ((e.metaKey || e.ctrlKey) && e.key === "s") { e.preventDefault(); if (canEdit && isDirty && !saving && page) handleSave(); } } document.addEventListener("keydown", onKeyDown); return () => document.removeEventListener("keydown", onKeyDown); }); // eslint-disable-line react-hooks/exhaustive-deps const loadPage = useCallback( async (id: number) => { setLoading(true); try { const res = await fetch( `/api/admin/catalog/tree?pageId=${id}${catQs}`, ); if (!res.ok) throw new Error(); const data = await res.json(); const p = data.page as PageDetail; setPage(p); const formData: FormState = { caption: p.caption, pageLayout: p.pageLayout, enabled: String(p.enabled), visible: String(p.visible), minRank: p.minRank ?? 0, clubOnly: String(p.clubOnly ?? "0"), orderNum: p.orderNum, iconImage: p.iconImage, iconColor: p.iconColor, pageHeadline: p.pageHeadline || "", pageTeaser: p.pageTeaser || "", pageSpecial: p.pageSpecial || "", pageText1: p.pageText1 || "", pageText2: p.pageText2 || "", pageTextDetails: p.pageTextDetails || "", pageTextTeaser: p.pageTextTeaser || "", }; setForm(formData); setOriginal(formData); } catch { toast.error("Failed to load page"); setPage(null); } finally { setLoading(false); } }, [catQs], ); const loadItemsData = useCallback(async (id: number) => { setItemsLoading(true); try { const res = await fetch(`/api/admin/catalog/items?pageId=${id}`); if (!res.ok) throw new Error(); const data = await res.json(); setItemsData(data as ItemsData); setItemsLoaded(true); } catch { toast.error("Failed to load items"); } finally { setItemsLoading(false); } }, []); const reloadItems = useCallback(() => { if (pageId && pageId > 0) loadItemsData(pageId); }, [pageId, loadItemsData]); useEffect(() => { if (pageId && pageId > 0) { loadPage(pageId); setItemsData(null); setItemsLoaded(false); setActiveTab("settings"); setShowPreview(false); } else { setPage(null); setForm({}); setOriginal({}); setItemsData(null); setItemsLoaded(false); } }, [pageId, loadPage]); // Lazy-load items (shop catalog only — BC items live on the detail page) useEffect(() => { if (catalogType === "bc") return; if ( (activeTab === "items" || activeTab === "translate") && !itemsLoaded && !itemsLoading && pageId && pageId > 0 ) { loadItemsData(pageId); } }, [ activeTab, itemsLoaded, itemsLoading, pageId, loadItemsData, catalogType, ]); function update(key: string, value: string | number | null) { if (!canEdit) return; setForm((prev) => ({ ...prev, [key]: value })); } async function handleSave() { if (!page || !canEdit) return; setSaving(true); try { const result = catalogType === "bc" ? await updateBcPage({ id: page.id, ...form }) : await updateCatalogPage({ id: page.id, ...form }); if (!result.ok) throw new Error(result.error || "Failed to save"); toast.success("Page updated & catalog synced"); setOriginal({ ...form }); setPage((prev) => prev ? { ...prev, caption: String(form.caption || prev.caption), enabled: String(form.enabled ?? prev.enabled), visible: String(form.visible ?? prev.visible), pageLayout: String(form.pageLayout || prev.pageLayout), iconImage: (form.iconImage as number) ?? prev.iconImage, } : prev, ); onSaved({ id: page.id, caption: String(form.caption || page.caption), enabled: String(form.enabled ?? page.enabled), visible: String(form.visible ?? page.visible), pageLayout: String(form.pageLayout || page.pageLayout), iconImage: (form.iconImage as number) ?? page.iconImage, }); } catch (e) { toast.error(e instanceof Error ? e.message : "Failed to save"); } finally { setSaving(false); } } if (!pageId || pageId <= 0) { return (
Select a page from the tree to edit
); } if (loading) { return (
); } if (!page) return null; const layoutColor = LAYOUT_COLORS[String(form.pageLayout)] || "bg-muted text-muted-foreground"; return (
{/* Header */}

{(form.caption as string) || page.caption}

{form.pageLayout as string} {isDirty && ( unsaved )}

#{page.id} · Parent:{" "} {page.parentId === -1 ? "Root" : `#${page.parentId}`} {page.itemCount !== undefined && ` · ${page.itemCount} items`}

{/* Dirty banner */} {isDirty && (
Unsaved changes — press{" "} Ctrl+S {" "} to save
)} {/* Tabs */} Settings {isDirty && ( )} Items {itemsData && ( {itemsData.items.length} )} Translate {itemsData && (() => { const furniSet = new Set(itemsData.furniDataIdList); const missing = itemsData.baseItems.filter( (b) => !furniSet.has(b.id), ).length; return missing > 0 ? ( {missing} ) : null; })()} {/* ── Settings tab ──────────────────────────────── */}
{/* Caption + Layout */}
update("caption", e.target.value)} className="h-8 text-sm" />

{LAYOUT_DESCRIPTIONS[ String(form.pageLayout) as CatalogLayout ] ?? "Custom layout"}

{/* Order + Rank + Icon */}
update("orderNum", parseInt(e.target.value, 10) || 0) } className="h-8 text-sm" />
update("minRank", parseInt(e.target.value, 10) || 1) } className="h-8 text-sm" />
update("iconImage", v)} />
{/* Toggles */}
update("enabled", v ? "1" : "0")} /> update("visible", v ? "1" : "0")} /> update("clubOnly", v ? "1" : "0")} />
{/* Headline image picker */}
update("pageHeadline", v)} type="header" label="Headline image" />
{/* Teaser image picker */}
update("pageTeaser", v)} type="teaser" label="Teaser image" />
{/* Collapsible text fields */} {showTexts && (