feat(catalog): add auto-category wizard with live preview and smart suggestions
- 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:
1 parent
baeb54aeb5
commit
f832479e52
14 files changed
+2577
-567
No files matched your search
@@ -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
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in new issue
Block a user