diff --git a/src/actions/admin-settings.ts b/src/actions/admin-settings.ts index 8b98b962..1e8f2fbb 100644 --- a/src/actions/admin-settings.ts +++ b/src/actions/admin-settings.ts @@ -5,12 +5,32 @@ import { z } from "zod"; import { MANAGED_SETTING_KEYS } from "@/app/admin/settings/cms-settings-config"; import { requirePermissionRateLimited } from "@/lib/admin/guard"; 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 { 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"; 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({ settings: z.record(z.string(), z.string()), }); @@ -23,9 +43,9 @@ export const saveManagedSettings = adminAction( rateLimitMax: 30, }, async (ctx) => { - const entries = Object.entries(ctx.data.settings).filter(([key]) => - managedKeySet.has(key), - ); + const entries = Object.entries(ctx.data.settings) + .filter(([key]) => managedKeySet.has(key)) + .map(([key, value]) => [key, normalizeSettingValue(key, value)] as const); await Promise.all( entries.map(([key, value]) => prisma.websiteSetting.upsert({ @@ -36,7 +56,12 @@ export const saveManagedSettings = adminAction( ), ); siteSettings.reload(); + if (entries.some(([key]) => key === HABBO_GAMEDATA_HOTEL_SETTING_KEY)) { + clearHabboItCache(); + clearBadgeCache(); + } revalidatePath("/admin/settings"); + revalidatePath("/admin/catalog"); return actionOk({ saved: entries.length }); }, ); @@ -46,10 +71,14 @@ export async function updateSetting(formData: FormData): Promise { const key = String(formData.get("key") ?? "") .normalize("NFC") .trim(); - const value = String(formData.get("value") ?? "").normalize("NFC"); + const value = normalizeSettingValue( + key, + String(formData.get("value") ?? "").normalize("NFC"), + ); if (!key) return; await prisma.websiteSetting.update({ where: { key }, data: { value } }); siteSettings.reload(); + bustGamedataCachesIfNeeded(key); revalidatePath("/admin/settings"); } @@ -59,7 +88,10 @@ export async function createSetting(formData: FormData): Promise { .normalize("NFC") .trim() .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") ?? "") .normalize("NFC") .trim() @@ -71,6 +103,7 @@ export async function createSetting(formData: FormData): Promise { create: { key, value, comment: comment || null }, }); siteSettings.reload(); + bustGamedataCachesIfNeeded(key); revalidatePath("/admin/settings"); } @@ -82,5 +115,6 @@ export async function deleteSetting(formData: FormData): Promise { if (!key) return; await prisma.websiteSetting.delete({ where: { key } }); siteSettings.reload(); + bustGamedataCachesIfNeeded(key); revalidatePath("/admin/settings"); } diff --git a/src/app/admin/catalog/[id]/catalog-detail-tabs.tsx b/src/app/admin/catalog/[id]/catalog-detail-tabs.tsx index 7f3487f4..260b0594 100644 --- a/src/app/admin/catalog/[id]/catalog-detail-tabs.tsx +++ b/src/app/admin/catalog/[id]/catalog-detail-tabs.tsx @@ -49,6 +49,7 @@ interface CatalogDetailTabsProps { furniRevisionMap: Record; 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 ( @@ -119,6 +121,7 @@ export function CatalogDetailTabs({ furniDataIdList={furniDataIdList} furniDescriptionMap={furniDescriptionMap} furniRevisionMap={furniRevisionMap} + gamedataHotel={gamedataHotel} /> diff --git a/src/app/admin/catalog/[id]/catalog-translate-tab.tsx b/src/app/admin/catalog/[id]/catalog-translate-tab.tsx index 1e24c0f7..468ab6e4 100644 --- a/src/app/admin/catalog/[id]/catalog-translate-tab.tsx +++ b/src/app/admin/catalog/[id]/catalog-translate-tab.tsx @@ -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; furniRevisionMap?: Record; + /** 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("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 ? ( ) : ( )} - Suggest IT{selected.size > 0 ? ` (${selected.size})` : ""} + Suggest {hotelShort} + {selected.size > 0 ? ` (${selected.size})` : ""}