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
+276
-66
No files matched your search
@@ -5,12 +5,32 @@ import { z } from "zod";
|
|||||||
import { MANAGED_SETTING_KEYS } from "@/app/admin/settings/cms-settings-config";
|
import { MANAGED_SETTING_KEYS } from "@/app/admin/settings/cms-settings-config";
|
||||||
import { requirePermissionRateLimited } from "@/lib/admin/guard";
|
import { requirePermissionRateLimited } from "@/lib/admin/guard";
|
||||||
import { actionOk, adminAction } from "@/lib/foundation/action";
|
import { actionOk, adminAction } from "@/lib/foundation/action";
|
||||||
|
import {
|
||||||
|
HABBO_GAMEDATA_HOTEL_SETTING_KEY,
|
||||||
|
normalizeHabboGamedataHotel,
|
||||||
|
} from "@/lib/habbo-gamedata-hotel";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { clearBadgeCache } from "@/lib/services/habboassets";
|
||||||
|
import { clearHabboItCache } from "@/lib/services/habbo-furnidata-cache";
|
||||||
import { siteSettings } from "@/lib/services/site-settings";
|
import { siteSettings } from "@/lib/services/site-settings";
|
||||||
|
|
||||||
const managedKeySet = new Set(MANAGED_SETTING_KEYS);
|
const managedKeySet = new Set(MANAGED_SETTING_KEYS);
|
||||||
|
|
||||||
|
function normalizeSettingValue(key: string, value: string): string {
|
||||||
|
if (key === HABBO_GAMEDATA_HOTEL_SETTING_KEY) {
|
||||||
|
return normalizeHabboGamedataHotel(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bustGamedataCachesIfNeeded(key: string): void {
|
||||||
|
if (key === HABBO_GAMEDATA_HOTEL_SETTING_KEY) {
|
||||||
|
clearHabboItCache();
|
||||||
|
clearBadgeCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const saveManagedSchema = z.object({
|
const saveManagedSchema = z.object({
|
||||||
settings: z.record(z.string(), z.string()),
|
settings: z.record(z.string(), z.string()),
|
||||||
});
|
});
|
||||||
@@ -23,9 +43,9 @@ export const saveManagedSettings = adminAction(
|
|||||||
rateLimitMax: 30,
|
rateLimitMax: 30,
|
||||||
},
|
},
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const entries = Object.entries(ctx.data.settings).filter(([key]) =>
|
const entries = Object.entries(ctx.data.settings)
|
||||||
managedKeySet.has(key),
|
.filter(([key]) => managedKeySet.has(key))
|
||||||
);
|
.map(([key, value]) => [key, normalizeSettingValue(key, value)] as const);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
entries.map(([key, value]) =>
|
entries.map(([key, value]) =>
|
||||||
prisma.websiteSetting.upsert({
|
prisma.websiteSetting.upsert({
|
||||||
@@ -36,7 +56,12 @@ export const saveManagedSettings = adminAction(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
siteSettings.reload();
|
siteSettings.reload();
|
||||||
|
if (entries.some(([key]) => key === HABBO_GAMEDATA_HOTEL_SETTING_KEY)) {
|
||||||
|
clearHabboItCache();
|
||||||
|
clearBadgeCache();
|
||||||
|
}
|
||||||
revalidatePath("/admin/settings");
|
revalidatePath("/admin/settings");
|
||||||
|
revalidatePath("/admin/catalog");
|
||||||
return actionOk({ saved: entries.length });
|
return actionOk({ saved: entries.length });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -46,10 +71,14 @@ export async function updateSetting(formData: FormData): Promise<void> {
|
|||||||
const key = String(formData.get("key") ?? "")
|
const key = String(formData.get("key") ?? "")
|
||||||
.normalize("NFC")
|
.normalize("NFC")
|
||||||
.trim();
|
.trim();
|
||||||
const value = String(formData.get("value") ?? "").normalize("NFC");
|
const value = normalizeSettingValue(
|
||||||
|
key,
|
||||||
|
String(formData.get("value") ?? "").normalize("NFC"),
|
||||||
|
);
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
await prisma.websiteSetting.update({ where: { key }, data: { value } });
|
await prisma.websiteSetting.update({ where: { key }, data: { value } });
|
||||||
siteSettings.reload();
|
siteSettings.reload();
|
||||||
|
bustGamedataCachesIfNeeded(key);
|
||||||
revalidatePath("/admin/settings");
|
revalidatePath("/admin/settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +88,10 @@ export async function createSetting(formData: FormData): Promise<void> {
|
|||||||
.normalize("NFC")
|
.normalize("NFC")
|
||||||
.trim()
|
.trim()
|
||||||
.slice(0, 255);
|
.slice(0, 255);
|
||||||
const value = String(formData.get("value") ?? "").normalize("NFC");
|
const value = normalizeSettingValue(
|
||||||
|
key,
|
||||||
|
String(formData.get("value") ?? "").normalize("NFC"),
|
||||||
|
);
|
||||||
const comment = String(formData.get("comment") ?? "")
|
const comment = String(formData.get("comment") ?? "")
|
||||||
.normalize("NFC")
|
.normalize("NFC")
|
||||||
.trim()
|
.trim()
|
||||||
@@ -71,6 +103,7 @@ export async function createSetting(formData: FormData): Promise<void> {
|
|||||||
create: { key, value, comment: comment || null },
|
create: { key, value, comment: comment || null },
|
||||||
});
|
});
|
||||||
siteSettings.reload();
|
siteSettings.reload();
|
||||||
|
bustGamedataCachesIfNeeded(key);
|
||||||
revalidatePath("/admin/settings");
|
revalidatePath("/admin/settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,5 +115,6 @@ export async function deleteSetting(formData: FormData): Promise<void> {
|
|||||||
if (!key) return;
|
if (!key) return;
|
||||||
await prisma.websiteSetting.delete({ where: { key } });
|
await prisma.websiteSetting.delete({ where: { key } });
|
||||||
siteSettings.reload();
|
siteSettings.reload();
|
||||||
|
bustGamedataCachesIfNeeded(key);
|
||||||
revalidatePath("/admin/settings");
|
revalidatePath("/admin/settings");
|
||||||
}
|
}
|
||||||
@@ -49,6 +49,7 @@ interface CatalogDetailTabsProps {
|
|||||||
furniRevisionMap: Record<number, { classname: string; revision: number }>;
|
furniRevisionMap: Record<number, { classname: string; revision: number }>;
|
||||||
allPages: { id: number; caption: string }[];
|
allPages: { id: number; caption: string }[];
|
||||||
interactionTypes: string[];
|
interactionTypes: string[];
|
||||||
|
gamedataHotel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CatalogDetailTabs({
|
export function CatalogDetailTabs({
|
||||||
@@ -64,6 +65,7 @@ export function CatalogDetailTabs({
|
|||||||
furniRevisionMap,
|
furniRevisionMap,
|
||||||
allPages,
|
allPages,
|
||||||
interactionTypes,
|
interactionTypes,
|
||||||
|
gamedataHotel,
|
||||||
}: CatalogDetailTabsProps) {
|
}: CatalogDetailTabsProps) {
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="settings">
|
<Tabs defaultValue="settings">
|
||||||
@@ -119,6 +121,7 @@ export function CatalogDetailTabs({
|
|||||||
furniDataIdList={furniDataIdList}
|
furniDataIdList={furniDataIdList}
|
||||||
furniDescriptionMap={furniDescriptionMap}
|
furniDescriptionMap={furniDescriptionMap}
|
||||||
furniRevisionMap={furniRevisionMap}
|
furniRevisionMap={furniRevisionMap}
|
||||||
|
gamedataHotel={gamedataHotel}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -47,6 +47,7 @@ export default async function CatalogEditPage({
|
|||||||
furniRevisionMap,
|
furniRevisionMap,
|
||||||
interactionTypes,
|
interactionTypes,
|
||||||
allPages,
|
allPages,
|
||||||
|
gamedataHotel,
|
||||||
} = itemsData;
|
} = itemsData;
|
||||||
|
|
||||||
const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank);
|
const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank);
|
||||||
@@ -124,6 +125,7 @@ export default async function CatalogEditPage({
|
|||||||
furniRevisionMap={furniRevisionMap}
|
furniRevisionMap={furniRevisionMap}
|
||||||
allPages={allPages}
|
allPages={allPages}
|
||||||
interactionTypes={interactionTypes}
|
interactionTypes={interactionTypes}
|
||||||
|
gamedataHotel={gamedataHotel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import { HABBO_GAMEDATA_HOTELS } from "@/lib/habbo-gamedata-hotel";
|
||||||
|
|
||||||
export type FieldType =
|
export type FieldType =
|
||||||
| "text"
|
| "text"
|
||||||
| "password"
|
| "password"
|
||||||
| "url"
|
| "url"
|
||||||
| "number"
|
| "number"
|
||||||
| "boolean"
|
| "boolean"
|
||||||
| "textarea";
|
| "textarea"
|
||||||
|
| "select";
|
||||||
|
|
||||||
export type SettingsGroupIcon =
|
export type SettingsGroupIcon =
|
||||||
| "building"
|
| "building"
|
||||||
@@ -22,6 +25,7 @@ export interface ManagedField {
|
|||||||
type?: FieldType;
|
type?: FieldType;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
|
options?: Array<{ value: string; label: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsGroup {
|
export interface SettingsGroup {
|
||||||
@@ -150,6 +154,18 @@ export const SETTINGS_GROUPS: SettingsGroup[] = [
|
|||||||
icon: "music",
|
icon: "music",
|
||||||
description: "Public URLs and overrides for SWF / Nitro asset packages.",
|
description: "Public URLs and overrides for SWF / Nitro asset packages.",
|
||||||
fields: [
|
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",
|
key: "soundtrack_base_url",
|
||||||
label: "Song disks MP3 base URL",
|
label: "Song disks MP3 base URL",
|
||||||
|
|||||||
@@ -197,6 +197,20 @@ function FieldControl({
|
|||||||
onChange={(e) => onChange(field.key, e.target.value)}
|
onChange={(e) => onChange(field.key, e.target.value)}
|
||||||
rows={3}
|
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
|
<Input
|
||||||
id={field.key}
|
id={field.key}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiOk } from "@/lib/api-response";
|
import { apiOk } from "@/lib/api-response";
|
||||||
|
import { getHabboGamedataHotel } from "@/lib/habbo-gamedata-hotel";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { batchLookupByClassnames } from "@/lib/services/habbofurni";
|
import { batchLookupByClassnames } from "@/lib/services/habbofurni";
|
||||||
|
|
||||||
export const GET = withAdmin(
|
export const GET = withAdmin(
|
||||||
{ permission: PERMS.CATALOG_VIEW },
|
{ permission: PERMS.CATALOG_VIEW },
|
||||||
async (request) => {
|
async (request) => {
|
||||||
|
const hotel = await getHabboGamedataHotel();
|
||||||
const raw = request.nextUrl.searchParams.get("classnames") || "";
|
const raw = request.nextUrl.searchParams.get("classnames") || "";
|
||||||
const classnames = raw
|
const classnames = raw
|
||||||
.split(",")
|
.split(",")
|
||||||
@@ -13,6 +15,10 @@ export const GET = withAdmin(
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.slice(0, 100);
|
.slice(0, 100);
|
||||||
|
|
||||||
|
if (classnames.length === 0) {
|
||||||
|
return apiOk({ hotel, suggestions: {} });
|
||||||
|
}
|
||||||
|
|
||||||
const found = await batchLookupByClassnames(classnames);
|
const found = await batchLookupByClassnames(classnames);
|
||||||
const suggestions: Record<string, { name: string; description: string }> =
|
const suggestions: Record<string, { name: string; description: string }> =
|
||||||
{};
|
{};
|
||||||
@@ -24,6 +30,6 @@ export const GET = withAdmin(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiOk({ suggestions });
|
return apiOk({ hotel, suggestions });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -84,6 +84,7 @@ interface ItemsData {
|
|||||||
furniRevisionMap: Record<number, { classname: string; revision: number }>;
|
furniRevisionMap: Record<number, { classname: string; revision: number }>;
|
||||||
interactionTypes: string[];
|
interactionTypes: string[];
|
||||||
allPages: { id: number; caption: string }[];
|
allPages: { id: number; caption: string }[];
|
||||||
|
gamedataHotel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageUpdateData {
|
export interface PageUpdateData {
|
||||||
@@ -691,6 +692,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
|
|||||||
furniDataIdList={itemsData.furniDataIdList}
|
furniDataIdList={itemsData.furniDataIdList}
|
||||||
furniDescriptionMap={itemsData.furniDescriptionMap}
|
furniDescriptionMap={itemsData.furniDescriptionMap}
|
||||||
furniRevisionMap={itemsData.furniRevisionMap}
|
furniRevisionMap={itemsData.furniRevisionMap}
|
||||||
|
gamedataHotel={itemsData.gamedataHotel}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
habboExternalTextsUrl,
|
||||||
|
habboFurnidataUrl,
|
||||||
|
habboGamedataHotelShort,
|
||||||
|
normalizeHabboGamedataHotel,
|
||||||
|
} from "./habbo-gamedata-hotel";
|
||||||
|
|
||||||
|
describe("habbo-gamedata-hotel", () => {
|
||||||
|
it("normalizes known hotels and falls back to it", () => {
|
||||||
|
expect(normalizeHabboGamedataHotel("ES")).toBe("es");
|
||||||
|
expect(normalizeHabboGamedataHotel("com.br")).toBe("com.br");
|
||||||
|
expect(normalizeHabboGamedataHotel("nope")).toBe("it");
|
||||||
|
expect(normalizeHabboGamedataHotel(null)).toBe("it");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds official gamedata URLs", () => {
|
||||||
|
expect(habboFurnidataUrl("de")).toBe(
|
||||||
|
"https://www.habbo.de/gamedata/furnidata_json/1",
|
||||||
|
);
|
||||||
|
expect(habboExternalTextsUrl("com.br")).toBe(
|
||||||
|
"https://www.habbo.com.br/gamedata/external_flash_texts/1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats short hotel labels", () => {
|
||||||
|
expect(habboGamedataHotelShort("it")).toBe("IT");
|
||||||
|
expect(habboGamedataHotelShort("com.br")).toBe("COM.BR");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/** Official Habbo hotel TLDs used for gamedata (furnidata / external texts). */
|
||||||
|
export const HABBO_GAMEDATA_HOTELS = [
|
||||||
|
{ value: "it", label: "Italy (habbo.it)" },
|
||||||
|
{ value: "com", label: "International (habbo.com)" },
|
||||||
|
{ value: "es", label: "Spain (habbo.es)" },
|
||||||
|
{ value: "de", label: "Germany (habbo.de)" },
|
||||||
|
{ value: "fr", label: "France (habbo.fr)" },
|
||||||
|
{ value: "fi", label: "Finland (habbo.fi)" },
|
||||||
|
{ value: "nl", label: "Netherlands (habbo.nl)" },
|
||||||
|
{ value: "com.br", label: "Brazil (habbo.com.br)" },
|
||||||
|
{ value: "com.tr", label: "Turkey (habbo.com.tr)" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type HabboGamedataHotel =
|
||||||
|
(typeof HABBO_GAMEDATA_HOTELS)[number]["value"];
|
||||||
|
|
||||||
|
export const HABBO_GAMEDATA_HOTEL_SETTING_KEY = "habbo_gamedata_hotel";
|
||||||
|
export const DEFAULT_HABBO_GAMEDATA_HOTEL: HabboGamedataHotel = "it";
|
||||||
|
|
||||||
|
const ALLOWED = new Set<string>(HABBO_GAMEDATA_HOTELS.map((h) => h.value));
|
||||||
|
|
||||||
|
export function normalizeHabboGamedataHotel(
|
||||||
|
raw: string | null | undefined,
|
||||||
|
): HabboGamedataHotel {
|
||||||
|
const v = (raw ?? "").trim().toLowerCase();
|
||||||
|
if (ALLOWED.has(v)) return v as HabboGamedataHotel;
|
||||||
|
return DEFAULT_HABBO_GAMEDATA_HOTEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function habboGamedataHotelLabel(hotel: string): string {
|
||||||
|
const found = HABBO_GAMEDATA_HOTELS.find((h) => h.value === hotel);
|
||||||
|
return found?.label ?? `habbo.${hotel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Short badge for buttons, e.g. IT / COM / COM.BR */
|
||||||
|
export function habboGamedataHotelShort(hotel: string): string {
|
||||||
|
return normalizeHabboGamedataHotel(hotel).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function habboFurnidataUrl(hotel: string): string {
|
||||||
|
const h = normalizeHabboGamedataHotel(hotel);
|
||||||
|
return `https://www.habbo.${h}/gamedata/furnidata_json/1`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function habboExternalTextsUrl(hotel: string): string {
|
||||||
|
const h = normalizeHabboGamedataHotel(hotel);
|
||||||
|
return `https://www.habbo.${h}/gamedata/external_flash_texts/1`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getHabboGamedataHotel(): Promise<HabboGamedataHotel> {
|
||||||
|
const { siteSettings } = await import("@/lib/services/site-settings");
|
||||||
|
const raw = await siteSettings.get(
|
||||||
|
HABBO_GAMEDATA_HOTEL_SETTING_KEY,
|
||||||
|
DEFAULT_HABBO_GAMEDATA_HOTEL,
|
||||||
|
);
|
||||||
|
return normalizeHabboGamedataHotel(raw);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "node:fs";
|
||||||
import { Prisma } from "@/generated/prisma/client";
|
import { Prisma } from "@/generated/prisma/client";
|
||||||
|
import { getHabboGamedataHotel } from "@/lib/habbo-gamedata-hotel";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { getFurnitureDataPath } from "@/lib/services/furni-data";
|
import { getFurnitureDataPath } from "@/lib/services/furni-data";
|
||||||
|
|
||||||
@@ -128,6 +129,8 @@ export interface CatalogItemsData {
|
|||||||
furniRevisionMap: Record<number, { classname: string; revision: number }>;
|
furniRevisionMap: Record<number, { classname: string; revision: number }>;
|
||||||
interactionTypes: string[];
|
interactionTypes: string[];
|
||||||
allPages: { id: number; caption: string }[];
|
allPages: { id: number; caption: string }[];
|
||||||
|
/** CMS `habbo_gamedata_hotel` — locale used for Suggest names */
|
||||||
|
gamedataHotel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadCatalogItemsData(
|
export async function loadCatalogItemsData(
|
||||||
@@ -160,7 +163,7 @@ export async function loadCatalogItemsData(
|
|||||||
clubOnly: String(r.club_only ?? "0"),
|
clubOnly: String(r.club_only ?? "0"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const [allPages, interactionTypesRaw] = await Promise.all([
|
const [allPages, interactionTypesRaw, gamedataHotel] = await Promise.all([
|
||||||
prisma.catalogPages.findMany({
|
prisma.catalogPages.findMany({
|
||||||
orderBy: { caption: "asc" },
|
orderBy: { caption: "asc" },
|
||||||
select: { id: true, caption: true },
|
select: { id: true, caption: true },
|
||||||
@@ -168,6 +171,7 @@ export async function loadCatalogItemsData(
|
|||||||
prisma.$queryRaw<Array<{ interaction_type: string }>>`
|
prisma.$queryRaw<Array<{ interaction_type: string }>>`
|
||||||
SELECT DISTINCT interaction_type FROM items_base ORDER BY interaction_type ASC
|
SELECT DISTINCT interaction_type FROM items_base ORDER BY interaction_type ASC
|
||||||
`,
|
`,
|
||||||
|
getHabboGamedataHotel(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const interactionTypes = interactionTypesRaw.map((r) =>
|
const interactionTypes = interactionTypesRaw.map((r) =>
|
||||||
@@ -319,5 +323,6 @@ export async function loadCatalogItemsData(
|
|||||||
furniRevisionMap,
|
furniRevisionMap,
|
||||||
interactionTypes,
|
interactionTypes,
|
||||||
allPages,
|
allPages,
|
||||||
|
gamedataHotel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Furniture search & listing — powered by official Habbo furnidata.
|
* Furniture search & listing — powered by official Habbo furnidata.
|
||||||
*
|
*
|
||||||
* Uses habbo.it/gamedata/furnidata_json as the single source of truth.
|
* Hotel locale comes from CMS setting `habbo_gamedata_hotel`.
|
||||||
* No third-party APIs (habbofurni.com) needed.
|
* No third-party APIs (habbofurni.com) needed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export type { FurniListResult, HabboFurniItem };
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* List furniture from official Habbo furnidata with optional search and pagination.
|
* List furniture from official Habbo furnidata with optional search and pagination.
|
||||||
* Data comes from habbo.it/gamedata/furnidata_json/1 (cached 30 min).
|
* Data comes from the configured hotel furnidata_json (cached 30 min).
|
||||||
*/
|
*/
|
||||||
export async function listFurniture(
|
export async function listFurniture(
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const DEFAULTS: Record<string, string> = {
|
|||||||
habbo_imaging_url: "/imaging",
|
habbo_imaging_url: "/imaging",
|
||||||
logo_url: "",
|
logo_url: "",
|
||||||
nitro_client_url: "",
|
nitro_client_url: "",
|
||||||
|
habbo_gamedata_hotel: "it",
|
||||||
};
|
};
|
||||||
|
|
||||||
const CACHE_TTL_MS = 300_000;
|
const CACHE_TTL_MS = 300_000;
|
||||||
|
|||||||
Reference in new issue
Block a user