feat(catalog): add auto-category wizard with live preview and smart suggestions
CI / check (push) Successful in 3m1s
CI / deploy (push) Successful in 2m7s
CI / publish-container (push) Successful in 45s

- Add AutoCategoryDialog: pick furni (or empty page), suggest caption/icon/layout, live preview
- Add createAutoCategory server action (page + offers in one export) with CatalogKind
- Extend furni search API with interactionType
- Share ShopTile and refactor inline-editor/items-shop-preview to use it
- Add suggestion heuristics (suggestCategoryName/dIcon/layout) with tests
- Add autoCategory translations (en/nl)
This commit is contained in:
openhands committed 2026-09-11 18:47:31 +02:00
1 parent baeb54aeb5
commit f832479e52
14 files changed
+2577 -567

No files matched your search

+1 -1
View File
@@ -20,7 +20,7 @@ import { logStaffActivity } from "@/lib/services/staff-activity";
import { translateItemsSchema } from "@/lib/validators/catalog";
/** Raw INSERT — catalog_items.id has no AUTO_INCREMENT on real Habbo DBs; page_id is often VARCHAR. */
async function insertCatalogItemRow(data: {
export async function insertCatalogItemRow(data: {
pageId: number;
itemIds: string;
catalogName: string;
File diff suppressed because it is too large. Load diff
+2
View File
@@ -43,6 +43,7 @@ export const GET = withAdmin(
publicName: ItemsBase.publicName,
type: ItemsBase.type,
spriteId: ItemsBase.spriteId,
interactionType: ItemsBase.interactionType,
})
.from(ItemsBase)
.where(or(...conditions))
@@ -56,6 +57,7 @@ export const GET = withAdmin(
publicName: r.publicName ?? "",
type: r.type ?? "s",
spriteId: r.spriteId ?? 0,
interactionType: r.interactionType ?? "default",
})),
});
},
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,6 @@
"use client";
import { LayoutGrid, Search, X } from "lucide-react";
import { LayoutGrid, Search, Wand2, X } from "lucide-react";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
@@ -15,6 +15,7 @@ import { useLatestRequest } from "@/features/catalog/client/use-latest-request";
import { CatalogSearch } from "@/features/catalog/components/catalog-search";
import type { CatalogSearchResult } from "@/features/catalog/domain/search";
import type { RootTab } from "@/types/catalog";
import { AutoCategoryDialog } from "./auto-category-dialog";
import { BreadcrumbBar } from "./breadcrumb-bar";
import { InlineEditor, type PageUpdateData } from "./inline-editor";
import { RootTabs } from "./root-tabs";
@@ -165,6 +166,7 @@ function CatalogManagerInner({
const rootRequests = useLatestRequest();
const searchNavigation = useLatestRequest();
const [searchOpen, setSearchOpen] = useState(false);
const [autoOpen, setAutoOpen] = useState(false);
const [searchTarget, setSearchTarget] = useState<{
offerId: number | null;
key: number;
@@ -399,6 +401,23 @@ function CatalogManagerInner({
{heading}
</h2>
<div className="flex items-center gap-1">
{canEdit && catalogType === "normal" && (
<Button
variant="outline"
size="sm"
aria-label={t("autoCategory.title")}
title={t("autoCategory.title")}
onClick={() => {
if (!mayLeave()) return;
setAutoOpen(true);
}}
>
<Wand2 className="size-4" />
<span className="hidden sm:inline">
{t("autoCategory.title")}
</span>
</Button>
)}
<Button
variant="outline"
size="sm"
@@ -437,6 +456,16 @@ function CatalogManagerInner({
</div>
)}
<AutoCategoryDialog
catalogType={catalogType}
open={autoOpen}
onOpenChange={setAutoOpen}
onCreated={(pageId) => {
void loadRootPages();
handleSelectPage(pageId);
}}
/>
<BreadcrumbBar onSelectPage={handleSelectPage} />
<RootTabs
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+41
View File
@@ -2157,6 +2157,47 @@
"clubHint": "Restricts access to users with an HC subscription.",
"yes": "Yes",
"no": "No"
},
"preview": {
"label": "Live catalog preview",
"title": "Live preview",
"on": "On",
"off": "Off",
"untitled": "Untitled page",
"noItems": "No offers on this page yet",
"items": "offer(s)",
"disabledHint": "This page is disabled — players cannot see it.",
"suggestedLayout": "Better layout suggested",
"suggestedLayoutReason": "This page {reason}. Switch to “{layout}” to match players’ expectations.",
"applyLayout": "Apply layout"
},
"autoCategory": {
"title": "Auto-Category",
"itemsBadge": "{count} items",
"fromFurni": "From furni",
"emptyPage": "Empty page",
"searchPlaceholder": "Search furni by name or classname…",
"noResults": "No results",
"added": "Added",
"price": "Price",
"free": "Free",
"setPrice": "Set a price",
"credits": "Credits",
"points": "Points",
"pointsType": "Type",
"parent": "Parent",
"root": "Catalog root",
"caption": "Caption",
"captionPlaceholder": "e.g. Chairs, Wallpapers…",
"icon": "Icon",
"pickIcon": "Pick icon",
"layout": "Layout",
"suggestedLayout": "Suggested: {layout}",
"enabled": "Enabled",
"visible": "Visible",
"cancel": "Cancel",
"create": "Create",
"createWithItems": "Create ({count} items)"
}
},
"bulk": {
+41
View File
@@ -1925,6 +1925,47 @@
"clubHint": "Beperkt toegang tot gebruikers met een HC-abonnement.",
"yes": "Ja",
"no": "Nee"
},
"preview": {
"label": "Live catalogusvoorbeeld",
"title": "Live voorbeeld",
"on": "Aan",
"off": "Uit",
"untitled": "Naamloze pagina",
"noItems": "Nog geen aanbiedingen op deze pagina",
"items": "aanbieding(en)",
"disabledHint": "Deze pagina is uitgeschakeld — spelers zien haar niet.",
"suggestedLayout": "Betere indeling voorgesteld",
"suggestedLayoutReason": "Deze pagina {reason}. Schakel over naar “{layout}” zodat deze overeenkomt met wat spelers verwachten.",
"applyLayout": "Indeling toepassen"
},
"autoCategory": {
"title": "Auto-Categorie",
"itemsBadge": "{count} items",
"fromFurni": "Vanuit furni",
"emptyPage": "Lege pagina",
"searchPlaceholder": "Zoek furni op naam of classname…",
"noResults": "Geen resultaten",
"added": "Toegevoegd",
"price": "Prijs",
"free": "Gratis",
"setPrice": "Stel een prijs in",
"credits": "Credits",
"points": "Punten",
"pointsType": "Type",
"parent": "Bovenliggende categorie",
"root": "Catalogus-hoofdmap",
"caption": "Naam",
"captionPlaceholder": "bijv. Stoelen, Behang…",
"icon": "Icoon",
"pickIcon": "Kies icoon",
"layout": "Indeling",
"suggestedLayout": "Voorgesteld: {layout}",
"enabled": "Ingeschakeld",
"visible": "Zichtbaar",
"cancel": "Annuleren",
"create": "Aanmaken",
"createWithItems": "Aanmaken ({count} items)"
}
},
"bulk": {