Compare commits
3
Commits
2c26e4bebb
...
e8b5295ff7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8b5295ff7 | ||
|
|
54f2bc5e0c | ||
|
|
5b09179404 |
No files matched your search
+4
-4
@@ -30,7 +30,7 @@
|
|||||||
"@dnd-kit/core": "6.3.1",
|
"@dnd-kit/core": "6.3.1",
|
||||||
"@dnd-kit/sortable": "10.0.0",
|
"@dnd-kit/sortable": "10.0.0",
|
||||||
"@dnd-kit/utilities": "3.2.2",
|
"@dnd-kit/utilities": "3.2.2",
|
||||||
"@hookform/resolvers": "5.8.0",
|
"@hookform/resolvers": "5.9.1",
|
||||||
"@next/bundle-analyzer": "16.3.1",
|
"@next/bundle-analyzer": "16.3.1",
|
||||||
"@tanstack/react-virtual": "3.14.9",
|
"@tanstack/react-virtual": "3.14.9",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
"@tailwindcss/forms": "^0.5.11",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/postcss": "^4.3.3",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@tailwindcss/typography": "^0.5.20",
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"@types/node": "^26.1.2",
|
"@types/node": "^26.2.0",
|
||||||
"@types/react": "19.2.18",
|
"@types/react": "19.2.18",
|
||||||
"@types/react-dom": "19.2.4",
|
"@types/react-dom": "19.2.4",
|
||||||
"@vitest/coverage-v8": "4.1.10",
|
"@vitest/coverage-v8": "4.1.10",
|
||||||
@@ -79,9 +79,9 @@
|
|||||||
"knip": "6.32.2",
|
"knip": "6.32.2",
|
||||||
"lint-staged": "^17.3.0",
|
"lint-staged": "^17.3.0",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"postcss": "^8.5.25",
|
"postcss": "^8.5.26",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
"tsx": "^4.23.5",
|
"tsx": "^4.23.12",
|
||||||
"typescript": "^7.0.2",
|
"typescript": "^7.0.2",
|
||||||
"vite": "8.2.1",
|
"vite": "8.2.1",
|
||||||
"vitest": "4.1.10"
|
"vitest": "4.1.10"
|
||||||
|
|||||||
Generated
+8
-8
@@ -35,8 +35,8 @@ importers:
|
|||||||
specifier: 3.2.2
|
specifier: 3.2.2
|
||||||
version: 3.2.2([email protected])
|
version: 3.2.2([email protected])
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: 5.8.0
|
specifier: 5.9.1
|
||||||
version: 5.8.0(@standard-schema/[email protected])([email protected]([email protected]))([email protected])
|
version: 5.9.1(@standard-schema/[email protected])([email protected]([email protected]))([email protected])
|
||||||
'@next/bundle-analyzer':
|
'@next/bundle-analyzer':
|
||||||
specifier: 16.3.1
|
specifier: 16.3.1
|
||||||
version: 16.3.1
|
version: 16.3.1
|
||||||
@@ -150,7 +150,7 @@ importers:
|
|||||||
specifier: ^0.5.20
|
specifier: ^0.5.20
|
||||||
version: 0.5.20([email protected])
|
version: 0.5.20([email protected])
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^26.1.2
|
specifier: ^26.2.0
|
||||||
version: 26.2.0
|
version: 26.2.0
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 19.2.18
|
specifier: 19.2.18
|
||||||
@@ -183,7 +183,7 @@ importers:
|
|||||||
specifier: ^4.3.3
|
specifier: ^4.3.3
|
||||||
version: 4.3.3
|
version: 4.3.3
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.23.5
|
specifier: ^4.23.12
|
||||||
version: 4.23.12
|
version: 4.23.12
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^7.0.2
|
specifier: ^7.0.2
|
||||||
@@ -559,8 +559,8 @@ packages:
|
|||||||
'@formatjs/[email protected]':
|
'@formatjs/[email protected]':
|
||||||
resolution: {integrity: sha512-kHEAFOkeJSPNi7c5PaKaRjxcBrJwzzt81ifUu+8uve1EDW/VJl83KsxmqgqNZLzcFEhSliZGvx3+pk/RH0IOmg==}
|
resolution: {integrity: sha512-kHEAFOkeJSPNi7c5PaKaRjxcBrJwzzt81ifUu+8uve1EDW/VJl83KsxmqgqNZLzcFEhSliZGvx3+pk/RH0IOmg==}
|
||||||
|
|
||||||
'@hookform/resolvers@5.8.0':
|
'@hookform/resolvers@5.9.1':
|
||||||
resolution: {integrity: sha512-2m6GvRLmYYK1Fwt093lGMf7db9l/+8pNuAtwoNkpBntJT4xcA5lNthYGWKViOc3z2SuaPD0HjE81pyXmqc1JyA==}
|
resolution: {integrity: sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@sinclair/typebox': '>=0.25.24'
|
'@sinclair/typebox': '>=0.25.24'
|
||||||
'@standard-schema/spec': ^1.0.0
|
'@standard-schema/spec': ^1.0.0
|
||||||
@@ -578,7 +578,7 @@ packages:
|
|||||||
fluentvalidation-ts: ^3.0.0
|
fluentvalidation-ts: ^3.0.0
|
||||||
fp-ts: ^2.7.0
|
fp-ts: ^2.7.0
|
||||||
io-ts: ^2.0.0
|
io-ts: ^2.0.0
|
||||||
joi: ^17.0.0
|
joi: ^17.0.0 || ^18.0.0
|
||||||
nope-validator: '>=0.12.0'
|
nope-validator: '>=0.12.0'
|
||||||
react-hook-form: ^7.55.0
|
react-hook-form: ^7.55.0
|
||||||
superstruct: '>=0.12.0'
|
superstruct: '>=0.12.0'
|
||||||
@@ -3372,7 +3372,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/fast-memoize': 3.1.7
|
'@formatjs/fast-memoize': 3.1.7
|
||||||
|
|
||||||
'@hookform/resolvers@5.8.0(@standard-schema/[email protected])([email protected]([email protected]))([email protected])':
|
'@hookform/resolvers@5.9.1(@standard-schema/[email protected])([email protected]([email protected]))([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/utils': 0.3.0
|
'@standard-schema/utils': 0.3.0
|
||||||
react-hook-form: 7.85.0([email protected])
|
react-hook-form: 7.85.0([email protected])
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { apiError, apiOk } from "@/lib/api-response";
|
|||||||
import { CatalogPages, db, ItemsBase } from "@/lib/db";
|
import { CatalogPages, db, ItemsBase } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { logAudit } from "@/lib/services/audit";
|
import { logAudit } from "@/lib/services/audit";
|
||||||
|
import { getCloneList } from "@/lib/services/clone-import";
|
||||||
import { getSource } from "@/lib/services/clone-sources";
|
import { getSource } from "@/lib/services/clone-sources";
|
||||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||||
import {
|
import {
|
||||||
@@ -213,6 +214,64 @@ export const GET = withAdmin(
|
|||||||
const search = request.nextUrl.searchParams.get("search") || "";
|
const search = request.nextUrl.searchParams.get("search") || "";
|
||||||
const page = parseInt(request.nextUrl.searchParams.get("page") || "1", 10);
|
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
|
// 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
|
// items to the top *before* pagination and (b) skip the per-page lookup
|
||||||
// below. Single-column indexed query — fast even at 10k+ rows.
|
// below. Single-column indexed query — fast even at 10k+ rows.
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -23,6 +23,7 @@ import {
|
|||||||
Monitor,
|
Monitor,
|
||||||
Newspaper,
|
Newspaper,
|
||||||
Package,
|
Package,
|
||||||
|
Palette,
|
||||||
Radio,
|
Radio,
|
||||||
Server,
|
Server,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -622,6 +623,13 @@ export const ADMIN_NAV_GROUPS: AdminNavGroup[] = [
|
|||||||
permission: PERMS.ROOMS_VIEW,
|
permission: PERMS.ROOMS_VIEW,
|
||||||
matchPrefixes: ["/admin/rooms"],
|
matchPrefixes: ["/admin/rooms"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: "/admin/studio",
|
||||||
|
labelKey: "studio",
|
||||||
|
icon: Palette,
|
||||||
|
permission: PERMS.ASSETS_IMPORT,
|
||||||
|
matchPrefixes: ["/admin/studio"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: "/admin/import",
|
href: "/admin/import",
|
||||||
labelKey: "import",
|
labelKey: "import",
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* 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 };
|
||||||
|
}
|
||||||
@@ -50,69 +50,11 @@ export const FURNI_NITRO_DIR = DEFAULT_FURNI_NITRO_DIR;
|
|||||||
export const IMPORTED_PAGE_CAPTION_SAVE = "imported_furni";
|
export const IMPORTED_PAGE_CAPTION_SAVE = "imported_furni";
|
||||||
export const IMPORTED_PAGE_CAPTION = "Imported Furniture";
|
export const IMPORTED_PAGE_CAPTION = "Imported Furniture";
|
||||||
|
|
||||||
// Catalog sub-page definitions (key = internal id used for caption_save `imp_<key>`)
|
// Shared with the admin Studio via the client-safe auto-catalog module so the
|
||||||
export const CATEGORY_PAGE: Record<
|
// on-screen preview always matches what the emulator import applies.
|
||||||
string,
|
export { CATEGORY_PAGE, CLASSNAME_RULES } from "@/lib/furni/auto-catalog";
|
||||||
{ 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)
|
import { CATEGORY_PAGE } from "@/lib/furni/auto-catalog";
|
||||||
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 ──────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -221,12 +163,9 @@ export async function getOrCreateImportedParentPage(): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Determine the catalog category for a furniture item based on its classname and type. */
|
/** Determine the catalog category for a furniture item based on its classname and type. */
|
||||||
export function classifyFurni(classname: string, itemType: string): string {
|
export { classifyFurni } from "@/lib/furni/auto-catalog";
|
||||||
for (const rule of CLASSNAME_RULES) {
|
|
||||||
if (rule.test(classname, itemType)) return rule.cat;
|
import { classifyFurni } from "@/lib/furni/auto-catalog";
|
||||||
}
|
|
||||||
return "other";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-detect catalog price from classname.
|
* Auto-detect catalog price from classname.
|
||||||
@@ -238,46 +177,9 @@ export function classifyFurni(classname: string, itemType: string): string {
|
|||||||
*
|
*
|
||||||
* Returns { credits, points, pointsType }
|
* Returns { credits, points, pointsType }
|
||||||
*/
|
*/
|
||||||
export function autoPriceFurni(classname: string): {
|
export { autoPriceFurni } from "@/lib/furni/auto-catalog";
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// exe_<number> → exchange items (diamonds/duckets)
|
import { autoPriceFurni } from "@/lib/furni/auto-catalog";
|
||||||
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.
|
/** 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
|
* Caches category page IDs in-memory for CATEGORY_CACHE_TTL_MS to avoid
|
||||||
|
|||||||
@@ -2597,6 +2597,7 @@
|
|||||||
"commando": "Commando",
|
"commando": "Commando",
|
||||||
"rooms": "Rooms",
|
"rooms": "Rooms",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
|
"studio": "Studio",
|
||||||
"importOverview": "Overview",
|
"importOverview": "Overview",
|
||||||
"importBadges": "Badges",
|
"importBadges": "Badges",
|
||||||
"importFurni": "Furni",
|
"importFurni": "Furni",
|
||||||
|
|||||||
@@ -2854,6 +2854,7 @@
|
|||||||
"commando": "Commando",
|
"commando": "Commando",
|
||||||
"rooms": "Kamers",
|
"rooms": "Kamers",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
|
"studio": "Studio",
|
||||||
"importOverview": "Overzicht",
|
"importOverview": "Overzicht",
|
||||||
"importBadges": "Badges",
|
"importBadges": "Badges",
|
||||||
"importFurni": "Meubels",
|
"importFurni": "Meubels",
|
||||||
|
|||||||
Reference in new issue
Block a user