Add multi-hotel Habbo gamedata locale in CMS settings.
Local Build and Deploy / deploy (push) Failing after 33s

Furni name suggestions, import enrichment, and badge texts now follow habbo_gamedata_hotel instead of hardcoded habbo.it.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-17 22:28:18 +02:00
1 parent 7bc0845932
commit d1baaf798a
15 files changed
+275 -65

No files matched your search

@@ -49,6 +49,7 @@ interface CatalogDetailTabsProps {
furniRevisionMap: Record<number, { classname: string; revision: number }>;
allPages: { id: number; caption: string }[];
interactionTypes: string[];
gamedataHotel: string;
}
export function CatalogDetailTabs({
@@ -64,6 +65,7 @@ export function CatalogDetailTabs({
furniRevisionMap,
allPages,
interactionTypes,
gamedataHotel,
}: CatalogDetailTabsProps) {
return (
<Tabs defaultValue="settings">
@@ -119,6 +121,7 @@ export function CatalogDetailTabs({
furniDataIdList={furniDataIdList}
furniDescriptionMap={furniDescriptionMap}
furniRevisionMap={furniRevisionMap}
gamedataHotel={gamedataHotel}
/>
</TabsContent>
</Tabs>
@@ -13,7 +13,7 @@ import {
Sparkles,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { translateCatalogItems } from "@/actions/catalog-items";
import { Badge } from "@/components/ui/badge";
@@ -35,6 +35,11 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
habboGamedataHotelLabel,
habboGamedataHotelShort,
normalizeHabboGamedataHotel,
} from "@/lib/habbo-gamedata-hotel";
import { useServerAction } from "@/hooks/use-server-action";
import { FurniIcon } from "./catalog-items-table";
@@ -53,6 +58,8 @@ interface CatalogTranslateTabProps {
furniDataIdList: number[];
furniDescriptionMap: Record<number, string>;
furniRevisionMap?: Record<number, { classname: string; revision: number }>;
/** CMS setting `habbo_gamedata_hotel` — which official Habbo locale to suggest from */
gamedataHotel?: string;
}
type FilterMode = "all" | "missing" | "modified";
@@ -64,11 +71,20 @@ export function CatalogTranslateTab({
furniDataIdList,
furniDescriptionMap,
furniRevisionMap = {},
gamedataHotel = "it",
}: CatalogTranslateTabProps) {
const router = useRouter();
const { run, isPending } = useServerAction();
const [isSuggesting, setIsSuggesting] = useState(false);
const [filterMode, setFilterMode] = useState<FilterMode>("all");
const [activeHotel, setActiveHotel] = useState(() =>
normalizeHabboGamedataHotel(gamedataHotel),
);
useEffect(() => {
setActiveHotel(normalizeHabboGamedataHotel(gamedataHotel));
}, [gamedataHotel]);
const hotelShort = habboGamedataHotelShort(activeHotel);
const hotelLabel = habboGamedataHotelLabel(activeHotel);
const furniDataIdSet = useMemo(
() => new Set(furniDataIdList),
@@ -186,6 +202,7 @@ export function CatalogTranslateTab({
string,
{ name: string; description: string }
> = {};
let suggestHotel = activeHotel;
for (let i = 0; i < classnames.length; i += CHUNK_SIZE) {
const chunk = classnames.slice(i, i + CHUNK_SIZE);
@@ -194,9 +211,14 @@ export function CatalogTranslateTab({
);
if (res.ok) {
const data = await res.json();
if (typeof data.hotel === "string") {
suggestHotel = normalizeHabboGamedataHotel(data.hotel);
setActiveHotel(suggestHotel);
}
Object.assign(allSuggestions, data.suggestions || {});
}
}
const suggestLabel = habboGamedataHotelLabel(suggestHotel);
// Apply suggestions to items that have a match (names + descriptions)
let nameCount = 0;
@@ -229,15 +251,15 @@ export function CatalogTranslateTab({
if (nameCount > 0) parts.push(`${nameCount} name(s)`);
if (descCount > 0) parts.push(`${descCount} description(s)`);
toast.success(
`HabboFurni: ${parts.join(" + ")} suggested from habbo.it. Review and save.`,
`${parts.join(" + ")} suggested from ${suggestLabel}. Review and save.`,
);
} else {
toast.info(
`HabboFurni: No new suggestions found (${Object.keys(allSuggestions).length} looked up, all already match).`,
`No new suggestions from ${suggestLabel} (${Object.keys(allSuggestions).length} looked up, all already match).`,
);
}
} catch (err) {
toast.error(`HabboFurni error: ${(err as Error).message}`);
toast.error(`Suggest error: ${(err as Error).message}`);
} finally {
setIsSuggesting(false);
}
@@ -318,14 +340,15 @@ export function CatalogTranslateTab({
size="sm"
onClick={handleSuggestFromHabboFurni}
disabled={isSuggesting || isPending || targetItems.length === 0}
title="Suggest Italian names + descriptions from HabboFurni API (habbo.it)"
title={`Suggest names + descriptions from ${hotelLabel}. Change hotel in Admin → Settings → Game assets.`}
>
{isSuggesting ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Sparkles className="mr-2 h-4 w-4" />
)}
Suggest IT{selected.size > 0 ? ` (${selected.size})` : ""}
Suggest {hotelShort}
{selected.size > 0 ? ` (${selected.size})` : ""}
</Button>
<Button
variant="outline"
+2
View File
@@ -47,6 +47,7 @@ export default async function CatalogEditPage({
furniRevisionMap,
interactionTypes,
allPages,
gamedataHotel,
} = itemsData;
const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank);
@@ -124,6 +125,7 @@ export default async function CatalogEditPage({
furniRevisionMap={furniRevisionMap}
allPages={allPages}
interactionTypes={interactionTypes}
gamedataHotel={gamedataHotel}
/>
</div>
);
+17 -1
View File
@@ -1,10 +1,13 @@
import { HABBO_GAMEDATA_HOTELS } from "@/lib/habbo-gamedata-hotel";
export type FieldType =
| "text"
| "password"
| "url"
| "number"
| "boolean"
| "textarea";
| "textarea"
| "select";
export type SettingsGroupIcon =
| "building"
@@ -22,6 +25,7 @@ export interface ManagedField {
type?: FieldType;
placeholder?: string;
defaultValue?: string;
options?: Array<{ value: string; label: string }>;
}
export interface SettingsGroup {
@@ -150,6 +154,18 @@ export const SETTINGS_GROUPS: SettingsGroup[] = [
icon: "music",
description: "Public URLs and overrides for SWF / Nitro asset packages.",
fields: [
{
key: "habbo_gamedata_hotel",
label: "Habbo hotel for furni names",
description:
"Official Habbo locale used for catalog name suggestions, furni import enrichment, and badge text lookup (furnidata / external texts).",
type: "select",
defaultValue: "it",
options: HABBO_GAMEDATA_HOTELS.map((h) => ({
value: h.value,
label: h.label,
})),
},
{
key: "soundtrack_base_url",
label: "Song disks MP3 base URL",
@@ -197,6 +197,20 @@ function FieldControl({
onChange={(e) => onChange(field.key, e.target.value)}
rows={3}
/>
) : type === "select" ? (
<select
id={field.key}
value={value}
disabled={!canEdit}
onChange={(e) => onChange(field.key, e.target.value)}
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
{(field.options ?? []).map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
) : (
<Input
id={field.key}
+7 -1
View File
@@ -1,11 +1,13 @@
import { withAdmin } from "@/lib/api-handler";
import { apiOk } from "@/lib/api-response";
import { getHabboGamedataHotel } from "@/lib/habbo-gamedata-hotel";
import { PERMS } from "@/lib/permissions";
import { batchLookupByClassnames } from "@/lib/services/habbofurni";
export const GET = withAdmin(
{ permission: PERMS.CATALOG_VIEW },
async (request) => {
const hotel = await getHabboGamedataHotel();
const raw = request.nextUrl.searchParams.get("classnames") || "";
const classnames = raw
.split(",")
@@ -13,6 +15,10 @@ export const GET = withAdmin(
.filter(Boolean)
.slice(0, 100);
if (classnames.length === 0) {
return apiOk({ hotel, suggestions: {} });
}
const found = await batchLookupByClassnames(classnames);
const suggestions: Record<string, { name: string; description: string }> =
{};
@@ -24,6 +30,6 @@ export const GET = withAdmin(
};
}
return apiOk({ suggestions });
return apiOk({ hotel, suggestions });
},
);