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

+36 -26
View File
@@ -1,17 +1,19 @@
/**
* Badge search — powered by official Habbo external texts.
*
* Uses habbo.it/gamedata/external_flash_texts as the single source.
* Hotel locale comes from CMS setting `habbo_gamedata_hotel`.
* Badge images from images.habbo.com/c_images/album1584/.
* No third-party APIs (habboassets.com) needed.
*/
export type { HabboAssetBadge } from "@/types/furni";
import type { HabboAssetBadge } from "@/types/furni";
import {
getHabboGamedataHotel,
habboExternalTextsUrl,
type HabboGamedataHotel,
} from "@/lib/habbo-gamedata-hotel";
const EXTERNAL_TEXTS_URL =
"https://www.habbo.it/gamedata/external_flash_texts/1";
const BADGE_IMAGE_BASE = "https://images.habbo.com/c_images/album1584";
const CACHE_TTL = 30 * 60 * 1000; // 30 minutes
@@ -22,26 +24,36 @@ interface BadgeEntry {
}
let badgeCache: BadgeEntry[] | null = null;
let cacheHotel: HabboGamedataHotel | null = null;
let cacheTimestamp = 0;
let loadingPromise: Promise<BadgeEntry[]> | null = null;
/**
* Fetch and parse all badges from official Habbo external texts.
* Cached in memory for 30 minutes.
* Cached in memory for 30 minutes (per configured hotel).
*/
async function loadBadges(): Promise<BadgeEntry[]> {
async function loadBadges(): Promise<{
badges: BadgeEntry[];
hotel: HabboGamedataHotel;
}> {
const hotel = await getHabboGamedataHotel();
const now = Date.now();
if (badgeCache && now - cacheTimestamp < CACHE_TTL) return badgeCache;
if (loadingPromise) return loadingPromise;
if (
badgeCache &&
cacheHotel === hotel &&
now - cacheTimestamp < CACHE_TTL
) {
return { badges: badgeCache, hotel };
}
if (loadingPromise) {
const badges = await loadingPromise;
return { badges, hotel: cacheHotel ?? hotel };
}
loadingPromise = (async () => {
const url = habboExternalTextsUrl(hotel);
try {
if (!EXTERNAL_TEXTS_URL) {
console.warn("[badges] HABBO_EXTERNAL_TEXTS_URL not configured");
return badgeCache || [];
}
const res = await fetch(EXTERNAL_TEXTS_URL, {
const res = await fetch(url, {
signal: AbortSignal.timeout(20000),
});
@@ -53,7 +65,6 @@ async function loadBadges(): Promise<BadgeEntry[]> {
const text = await res.text();
const lines = text.split("\n");
// Parse key=value format, extract badge_name_CODE and badge_desc_CODE
const nameMap = new Map<string, string>();
const descMap = new Map<string, string>();
@@ -63,7 +74,6 @@ async function loadBadges(): Promise<BadgeEntry[]> {
const key = line.substring(0, eqIdx).trim();
const value = line.substring(eqIdx + 1).trim();
// Format: badge_name_CODE or CODE_badge_name
let match = key.match(/^badge_name_(.+)$/);
if (match) {
nameMap.set(match[1], value);
@@ -86,7 +96,6 @@ async function loadBadges(): Promise<BadgeEntry[]> {
}
}
// Merge into badge entries
const allCodes = new Set([...nameMap.keys(), ...descMap.keys()]);
const badges: BadgeEntry[] = [];
@@ -98,14 +107,14 @@ async function loadBadges(): Promise<BadgeEntry[]> {
});
}
// Sort alphabetically by code
badges.sort((a, b) => a.code.localeCompare(b.code));
console.debug(
`[badges] Loaded ${badges.length} badges from external texts`,
`[badges] Loaded ${badges.length} badges from habbo.${hotel}`,
);
badgeCache = badges;
cacheTimestamp = now;
cacheHotel = hotel;
cacheTimestamp = Date.now();
return badges;
} catch (err) {
console.warn("[badges] Load error:", (err as Error).message);
@@ -115,7 +124,8 @@ async function loadBadges(): Promise<BadgeEntry[]> {
}
})();
return loadingPromise;
const badges = await loadingPromise;
return { badges, hotel: cacheHotel ?? hotel };
}
/**
@@ -126,9 +136,9 @@ export async function searchBadges(
term: string,
limit = 100,
offset = 0,
_hotel: string | null = null, // kept for API compat, ignored (single source)
_hotel: string | null = null, // API compat; locale comes from CMS settings
): Promise<HabboAssetBadge[]> {
const badges = await loadBadges();
const { badges, hotel } = await loadBadges();
const search = term.toLowerCase().trim();
let filtered = badges;
@@ -140,7 +150,6 @@ export async function searchBadges(
b.description.toLowerCase().includes(search),
);
} else {
// No search term: return newest (reverse sorted, last entries are newest)
filtered = [...badges].reverse();
}
@@ -151,14 +160,15 @@ export async function searchBadges(
code: b.code,
name: b.name,
description: b.description,
hotel: "it",
hotel,
url_habbo: `${BADGE_IMAGE_BASE}/${b.code}.gif`,
url_habboassets: `${BADGE_IMAGE_BASE}/${b.code}.gif`, // same source, no fallback needed
url_habboassets: `${BADGE_IMAGE_BASE}/${b.code}.gif`,
}));
}
/** Clear the badge cache */
export function clearBadgeCache(): void {
badgeCache = null;
cacheHotel = null;
cacheTimestamp = 0;
}