Add multi-hotel Habbo gamedata locale in CMS settings.
Local Build and Deploy / deploy (push) Failing after 33s
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:
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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user