1 Commits
Author SHA1 Message Date
remco 2c26e4bebb chore(deps): update All dependencies
CI / check (pull_request) Failing after 11s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped
2026-08-15 11:00:27 +00:00
10 changed files with 113 additions and 1406 deletions

No files matched your search

+1 -1
View File
@@ -30,7 +30,7 @@
"@dnd-kit/core": "6.3.1",
"@dnd-kit/sortable": "10.0.0",
"@dnd-kit/utilities": "3.2.2",
"@hookform/resolvers": "5.9.1",
"@hookform/resolvers": "5.9.0",
"@next/bundle-analyzer": "16.3.1",
"@tanstack/react-virtual": "3.14.9",
"class-variance-authority": "0.7.1",
+5 -5
View File
@@ -35,8 +35,8 @@ importers:
specifier: 3.2.2
version: 3.2.2([email protected])
'@hookform/resolvers':
specifier: 5.9.1
version: 5.9.1(@standard-schema/[email protected])([email protected]([email protected]))([email protected])
specifier: 5.9.0
version: 5.9.0(@standard-schema/[email protected])([email protected]([email protected]))([email protected])
'@next/bundle-analyzer':
specifier: 16.3.1
version: 16.3.1
@@ -559,8 +559,8 @@ packages:
'@formatjs/[email protected]':
resolution: {integrity: sha512-kHEAFOkeJSPNi7c5PaKaRjxcBrJwzzt81ifUu+8uve1EDW/VJl83KsxmqgqNZLzcFEhSliZGvx3+pk/RH0IOmg==}
'@hookform/[email protected].1':
resolution: {integrity: sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w==}
'@hookform/[email protected].0':
resolution: {integrity: sha512-8sNo1IklGaONrsKzXjwZJNsPbccAXLXGX//G+VFEcQOviMtbvR8/fM6HWyA0qrjgpOYauwoodda53CQIWOZ7Ag==}
peerDependencies:
'@sinclair/typebox': '>=0.25.24'
'@standard-schema/spec': ^1.0.0
@@ -3372,7 +3372,7 @@ snapshots:
dependencies:
'@formatjs/fast-memoize': 3.1.7
'@hookform/[email protected].1(@standard-schema/[email protected])([email protected]([email protected]))([email protected])':
'@hookform/[email protected].0(@standard-schema/[email protected])([email protected]([email protected]))([email protected])':
dependencies:
'@standard-schema/utils': 0.3.0
react-hook-form: 7.85.0([email protected])
-27
View File
@@ -1,27 +0,0 @@
import { redirect } from "next/navigation";
import { StudioClient } from "@/components/admin/studio/studio-client";
import { buildFurniImportSource } from "@/lib/habbo-gamedata-hotel";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { getTreeFlat } from "@/lib/services/catalog-tree";
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
export default async function StudioPage(_props: {
params: Promise<{ locale: string }>;
}) {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.ASSETS_IMPORT, session.user.rank)) {
redirect("/admin");
}
const [hotel, catalogTree] = await Promise.all([
getHabboGamedataHotel(),
getTreeFlat(),
]);
return (
<StudioClient
source={buildFurniImportSource(hotel)}
initialTree={catalogTree}
/>
);
}
-59
View File
@@ -6,7 +6,6 @@ import { apiError, apiOk } from "@/lib/api-response";
import { CatalogPages, db, ItemsBase } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
import { logAudit } from "@/lib/services/audit";
import { getCloneList } from "@/lib/services/clone-import";
import { getSource } from "@/lib/services/clone-sources";
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
import {
@@ -214,64 +213,6 @@ export const GET = withAdmin(
const search = request.nextUrl.searchParams.get("search") || "";
const page = parseInt(request.nextUrl.searchParams.get("page") || "1", 10);
// ── Browse a custom clone source (retro hotel furnidata) ────────
const sourceId = request.nextUrl.searchParams.get("source");
if (sourceId) {
const source = await getSource(sourceId);
if (!source) return apiError("Source not found", 404);
const { items, meta } = await getCloneList({
source,
search,
page,
perPage: 50,
filter: "all",
});
const sourceIconBase = source.iconBaseUrl?.trim().replace(/\/+$/, "");
const enriched = items.map((item) => {
const imported = item.present;
let nitroExists = false;
if (imported) {
const starIdx = item.classname.indexOf("*");
const base =
starIdx !== -1
? item.classname.substring(0, starIdx)
: item.classname;
const p = path.join(
/*turbopackIgnore: true*/ nitroDir,
`${base}.nitro`,
);
nitroExists = existsSync(/*turbopackIgnore: true*/ p);
}
return {
id: item.id,
classname: item.classname,
type: item.itemType === "i" ? "wallitem" : "flooritem",
name: item.name,
description: item.description,
revision: 0,
category: String(item.category ?? ""),
alreadyImported: imported,
nitroExists,
iconUrl: sourceIconBase
? `${sourceIconBase}/${encodeURIComponent(item.classname)}_icon.png`
: undefined,
};
});
return apiOk({
items: enriched,
source: { id: source.id, name: source.name },
meta: {
currentPage: meta.page,
lastPage: Math.max(Math.ceil(meta.total / meta.perPage), 1),
total: meta.total,
perPage: meta.perPage,
},
});
}
// Pull every imported classname once so we can both (a) sort non-imported
// items to the top *before* pagination and (b) skip the per-page lookup
// below. Single-column indexed query — fast even at 10k+ rows.
File diff suppressed because it is too large. Load diff
-8
View File
@@ -23,7 +23,6 @@ import {
Monitor,
Newspaper,
Package,
Palette,
Radio,
Server,
Settings,
@@ -623,13 +622,6 @@ export const ADMIN_NAV_GROUPS: AdminNavGroup[] = [
permission: PERMS.ROOMS_VIEW,
matchPrefixes: ["/admin/rooms"],
},
{
href: "/admin/studio",
labelKey: "studio",
icon: Palette,
permission: PERMS.ASSETS_IMPORT,
matchPrefixes: ["/admin/studio"],
},
{
href: "/admin/import",
labelKey: "import",
-147
View File
@@ -1,147 +0,0 @@
/**
* Client-safe auto-catalog helpers (pure, no Node/DB imports).
*
* Shared by the server-side import pipeline (furni-import.ts re-exports from
* here) and the admin Studio UI, so the on-screen "auto category + auto price"
* preview always matches what the emulator import actually applies.
*/
export interface AutoPrice {
credits: number;
points: number;
pointsType: number;
}
export interface AutoCatalogPreview extends AutoPrice {
categoryKey: string;
categoryLabel: string;
}
// Catalog sub-page definitions (key = internal id used for caption_save `imp_<key>`)
export const CATEGORY_PAGE: Record<
string,
{ label: string; icon: number; order: number }
> = {
credits: { label: "Credits & Exchange", icon: 263, order: 1 },
rares: { label: "Rares", icon: 11, order: 2 },
wired: { label: "Wired", icon: 1, order: 3 },
nft: { label: "NFT", icon: 12, order: 4 },
seasonal: { label: "Seasonal", icon: 10, order: 5 },
music: { label: "Music & Sound", icon: 13, order: 6 },
clothing: { label: "Clothing", icon: 3, order: 7 },
builders: { label: "Builders Club", icon: 7, order: 8 },
walls: { label: "Wall Items", icon: 5, order: 9 },
promo: { label: "Promo & Custom", icon: 2, order: 10 },
trophies: { label: "Trophies", icon: 6, order: 11 },
pets: { label: "Pets", icon: 4, order: 12 },
room: { label: "Room & Building", icon: 8, order: 13 },
other: { label: "Other", icon: 1, order: 99 },
};
// Classname prefix → category key (order matters: first match wins)
export const CLASSNAME_RULES: Array<{
test: (cn: string, type: string) => boolean;
cat: string;
}> = [
{
test: (cn) => /^CF_/i.test(cn) || /^exe_/i.test(cn) || cn === "credit",
cat: "credits",
},
{ test: (cn) => /^rare_/i.test(cn), cat: "rares" },
{ test: (cn) => /^wf_/i.test(cn), cat: "wired" },
{ test: (cn) => /^nft_/i.test(cn), cat: "nft" },
{
test: (cn) =>
/^(xmas|hween|easter|val_|valentine|hw_|newyear|summer|carnival)/i.test(
cn,
),
cat: "seasonal",
},
{ test: (cn) => /^(trax|jukebox|sound|music)/i.test(cn), cat: "music" },
{ test: (cn) => /^(clothing|clothes)/i.test(cn), cat: "clothing" },
{ test: (cn) => /^bc_/i.test(cn), cat: "builders" },
{ test: (cn) => /^(poster|wall_|window_|wallpaper)/i.test(cn), cat: "walls" },
{ test: (_cn, type) => type === "wallitem" || type === "i", cat: "walls" },
{ test: (cn) => /^(ads_|cstm_)/i.test(cn), cat: "promo" },
{
test: (cn) => /^(trophy|gold_|silver_|bronze_)/i.test(cn),
cat: "trophies",
},
{
test: (cn) =>
/^(pet|dog|cat_|bird|horse|pig|monkey|frog|dragon|bear_|spider|bunny)/i.test(
cn,
),
cat: "pets",
},
{
test: (cn) =>
/^(room_|floor_|tile_|carpet|rug|door_|gate_|stair)/i.test(cn),
cat: "room",
},
];
/** Determine the catalog category for a furniture item based on its classname and type. */
export function classifyFurni(classname: string, itemType: string): string {
for (const rule of CLASSNAME_RULES) {
if (rule.test(classname, itemType)) return rule.cat;
}
return "other";
}
/**
* Auto-detect catalog price from classname.
* - CF_750_xxx → cost_credits = 750 (credit furni = the credit value it represents)
* - exe_50, exe_100, exe_200 etc. → cost_credits based on exchange value
* - rare_xxx → cost_credits = 25 (rares are expensive)
* - wf_xxx → cost_credits = 5 (wired components)
* - everything else → cost_credits = 3 (default)
*/
export function autoPriceFurni(classname: string): AutoPrice {
// CF_<number>_xxx → credit furniture, price = the credit value
const cfMatch = classname.match(/^CF_(\d+)/i);
if (cfMatch) {
return { credits: parseInt(cfMatch[1], 10), points: 0, pointsType: 0 };
}
// exe_<number> → exchange items (diamonds/duckets)
const exeMatch = classname.match(/^exe_(\d+)/i);
if (exeMatch) {
return { credits: parseInt(exeMatch[1], 10), points: 0, pointsType: 0 };
}
// Rares
if (/^rare_/i.test(classname)) {
return { credits: 25, points: 0, pointsType: 0 };
}
// Wired
if (/^wf_/i.test(classname)) {
return { credits: 5, points: 0, pointsType: 0 };
}
// NFT items
if (/^nft_/i.test(classname)) {
return { credits: 10, points: 0, pointsType: 0 };
}
// Trophies
if (/^(trophy|gold_|silver_|bronze_)/i.test(classname)) {
return { credits: 10, points: 0, pointsType: 0 };
}
// Default
return { credits: 3, points: 0, pointsType: 0 };
}
/** Combine auto-category + auto-price into a single preview for the Studio UI. */
export function previewAutoCatalog(
classname: string,
itemType: string,
): AutoCatalogPreview {
const categoryKey = classifyFurni(classname, itemType);
const categoryLabel =
CATEGORY_PAGE[categoryKey]?.label ?? CATEGORY_PAGE.other.label;
const price = autoPriceFurni(classname);
return { ...price, categoryKey, categoryLabel };
}
+107 -9
View File
@@ -50,11 +50,69 @@ export const FURNI_NITRO_DIR = DEFAULT_FURNI_NITRO_DIR;
export const IMPORTED_PAGE_CAPTION_SAVE = "imported_furni";
export const IMPORTED_PAGE_CAPTION = "Imported Furniture";
// Shared with the admin Studio via the client-safe auto-catalog module so the
// on-screen preview always matches what the emulator import applies.
export { CATEGORY_PAGE, CLASSNAME_RULES } from "@/lib/furni/auto-catalog";
// Catalog sub-page definitions (key = internal id used for caption_save `imp_<key>`)
export const CATEGORY_PAGE: Record<
string,
{ label: string; icon: number; order: number }
> = {
credits: { label: "Credits & Exchange", icon: 263, order: 1 },
rares: { label: "Rares", icon: 11, order: 2 },
wired: { label: "Wired", icon: 1, order: 3 },
nft: { label: "NFT", icon: 12, order: 4 },
seasonal: { label: "Seasonal", icon: 10, order: 5 },
music: { label: "Music & Sound", icon: 13, order: 6 },
clothing: { label: "Clothing", icon: 3, order: 7 },
builders: { label: "Builders Club", icon: 7, order: 8 },
walls: { label: "Wall Items", icon: 5, order: 9 },
promo: { label: "Promo & Custom", icon: 2, order: 10 },
trophies: { label: "Trophies", icon: 6, order: 11 },
pets: { label: "Pets", icon: 4, order: 12 },
room: { label: "Room & Building", icon: 8, order: 13 },
other: { label: "Other", icon: 1, order: 99 },
};
import { CATEGORY_PAGE } from "@/lib/furni/auto-catalog";
// Classname prefix → category key (order matters: first match wins)
export const CLASSNAME_RULES: Array<{
test: (cn: string, type: string) => boolean;
cat: string;
}> = [
{
test: (cn) => /^CF_/i.test(cn) || /^exe_/i.test(cn) || cn === "credit",
cat: "credits",
},
{ test: (cn) => /^rare_/i.test(cn), cat: "rares" },
{ test: (cn) => /^wf_/i.test(cn), cat: "wired" },
{ test: (cn) => /^nft_/i.test(cn), cat: "nft" },
{
test: (cn) =>
/^(xmas|hween|easter|val_|valentine|hw_|newyear|summer|carnival)/i.test(
cn,
),
cat: "seasonal",
},
{ test: (cn) => /^(trax|jukebox|sound|music)/i.test(cn), cat: "music" },
{ test: (cn) => /^(clothing|clothes)/i.test(cn), cat: "clothing" },
{ test: (cn) => /^bc_/i.test(cn), cat: "builders" },
{ test: (cn) => /^(poster|wall_|window_|wallpaper)/i.test(cn), cat: "walls" },
{ test: (_cn, type) => type === "wallitem" || type === "i", cat: "walls" },
{ test: (cn) => /^(ads_|cstm_)/i.test(cn), cat: "promo" },
{
test: (cn) => /^(trophy|gold_|silver_|bronze_)/i.test(cn),
cat: "trophies",
},
{
test: (cn) =>
/^(pet|dog|cat_|bird|horse|pig|monkey|frog|dragon|bear_|spider|bunny)/i.test(
cn,
),
cat: "pets",
},
{
test: (cn) =>
/^(room_|floor_|tile_|carpet|rug|door_|gate_|stair)/i.test(cn),
cat: "room",
},
];
// ── Helpers ──────────────────────────────────────────────────────────
@@ -163,9 +221,12 @@ export async function getOrCreateImportedParentPage(): Promise<number> {
}
/** Determine the catalog category for a furniture item based on its classname and type. */
export { classifyFurni } from "@/lib/furni/auto-catalog";
import { classifyFurni } from "@/lib/furni/auto-catalog";
export function classifyFurni(classname: string, itemType: string): string {
for (const rule of CLASSNAME_RULES) {
if (rule.test(classname, itemType)) return rule.cat;
}
return "other";
}
/**
* Auto-detect catalog price from classname.
@@ -177,9 +238,46 @@ import { classifyFurni } from "@/lib/furni/auto-catalog";
*
* Returns { credits, points, pointsType }
*/
export { autoPriceFurni } from "@/lib/furni/auto-catalog";
export function autoPriceFurni(classname: string): {
credits: number;
points: number;
pointsType: number;
} {
// CF_<number>_xxx → credit furniture, price = the credit value
const cfMatch = classname.match(/^CF_(\d+)/i);
if (cfMatch) {
return { credits: parseInt(cfMatch[1], 10), points: 0, pointsType: 0 };
}
import { autoPriceFurni } from "@/lib/furni/auto-catalog";
// exe_<number> → exchange items (diamonds/duckets)
const exeMatch = classname.match(/^exe_(\d+)/i);
if (exeMatch) {
return { credits: parseInt(exeMatch[1], 10), points: 0, pointsType: 0 };
}
// Rares
if (/^rare_/i.test(classname)) {
return { credits: 25, points: 0, pointsType: 0 };
}
// Wired
if (/^wf_/i.test(classname)) {
return { credits: 5, points: 0, pointsType: 0 };
}
// NFT items
if (/^nft_/i.test(classname)) {
return { credits: 10, points: 0, pointsType: 0 };
}
// Trophies
if (/^(trophy|gold_|silver_|bronze_)/i.test(classname)) {
return { credits: 10, points: 0, pointsType: 0 };
}
// Default
return { credits: 3, points: 0, pointsType: 0 };
}
/** Get or create a category sub-page under the imported parent page.
* Caches category page IDs in-memory for CATEGORY_CACHE_TTL_MS to avoid
-1
View File
@@ -2597,7 +2597,6 @@
"commando": "Commando",
"rooms": "Rooms",
"import": "Import",
"studio": "Studio",
"importOverview": "Overview",
"importBadges": "Badges",
"importFurni": "Furni",
-1
View File
@@ -2854,7 +2854,6 @@
"commando": "Commando",
"rooms": "Kamers",
"import": "Import",
"studio": "Studio",
"importOverview": "Overzicht",
"importBadges": "Badges",
"importFurni": "Meubels",