Restore missing catalog admin API routes so Visual Manager works.
Local Build and Deploy / deploy (push) Successful in 1m10s
Local Build and Deploy / deploy (push) Successful in 1m10s
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
ce8c3503e9
commit
11d3cf31cc
6 files changed
+484
-4
No files matched your search
@@ -0,0 +1,47 @@
|
|||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { apiOk } from "@/lib/api-response";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
|
||||||
|
const CATALOGUE_DIR = path.join(
|
||||||
|
/*turbopackIgnore: true*/ process.cwd(),
|
||||||
|
"public",
|
||||||
|
"swf",
|
||||||
|
"c_images",
|
||||||
|
"catalogue",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const GET = withAdmin(
|
||||||
|
{ permission: PERMS.CATALOG_VIEW },
|
||||||
|
async (request) => {
|
||||||
|
const sp = request.nextUrl.searchParams;
|
||||||
|
const limit = Math.min(
|
||||||
|
Math.max(Number(sp.get("limit") || 120) || 120, 1),
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
const offset = Math.max(Number(sp.get("offset") || 0) || 0, 0);
|
||||||
|
const search = (sp.get("search") || "").trim().toLowerCase();
|
||||||
|
|
||||||
|
let icons: number[] = [];
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(CATALOGUE_DIR);
|
||||||
|
icons = files
|
||||||
|
.map((f) => /^icon_(\d+)\.(?:png|gif)$/i.exec(f))
|
||||||
|
.filter((m): m is RegExpExecArray => Boolean(m))
|
||||||
|
.map((m) => Number(m[1]))
|
||||||
|
.filter((n) => Number.isFinite(n))
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
} catch {
|
||||||
|
icons = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
icons = icons.filter((id) => String(id).includes(search));
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = icons.length;
|
||||||
|
const slice = icons.slice(offset, offset + limit);
|
||||||
|
return apiOk({ icons: slice, total });
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { apiOk } from "@/lib/api-response";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
|
||||||
|
const CATALOGUE_DIR = path.join(
|
||||||
|
/*turbopackIgnore: true*/ process.cwd(),
|
||||||
|
"public",
|
||||||
|
"swf",
|
||||||
|
"c_images",
|
||||||
|
"catalogue",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const GET = withAdmin(
|
||||||
|
{ permission: PERMS.CATALOG_VIEW },
|
||||||
|
async (request) => {
|
||||||
|
const sp = request.nextUrl.searchParams;
|
||||||
|
const limit = Math.min(
|
||||||
|
Math.max(Number(sp.get("limit") || 80) || 80, 1),
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
const offset = Math.max(Number(sp.get("offset") || 0) || 0, 0);
|
||||||
|
const search = (sp.get("search") || "").trim().toLowerCase();
|
||||||
|
const type = sp.get("type") || "all";
|
||||||
|
|
||||||
|
let names: string[] = [];
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(CATALOGUE_DIR);
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const file of files) {
|
||||||
|
if (/^icon_\d+\./i.test(file)) continue;
|
||||||
|
const base = file.replace(/\.(png|gif|jpg|jpeg|webp)$/i, "");
|
||||||
|
if (!base || seen.has(base)) continue;
|
||||||
|
seen.add(base);
|
||||||
|
names.push(base);
|
||||||
|
}
|
||||||
|
names.sort((a, b) => a.localeCompare(b));
|
||||||
|
} catch {
|
||||||
|
names = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "header") {
|
||||||
|
names = names.filter((n) => /header/i.test(n));
|
||||||
|
} else if (type === "teaser") {
|
||||||
|
names = names.filter((n) => /teaser/i.test(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
names = names.filter((n) => n.toLowerCase().includes(search));
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = names.length;
|
||||||
|
const images = names.slice(offset, offset + limit);
|
||||||
|
return apiOk({ images, total });
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { loadCatalogItemsData } from "@/lib/services/catalog-items-loader";
|
||||||
|
|
||||||
|
export const GET = withAdmin(
|
||||||
|
{ permission: PERMS.CATALOG_VIEW },
|
||||||
|
async (request) => {
|
||||||
|
const pageId = Number(request.nextUrl.searchParams.get("pageId"));
|
||||||
|
if (!Number.isFinite(pageId) || pageId <= 0) {
|
||||||
|
return apiError("Invalid pageId");
|
||||||
|
}
|
||||||
|
const data = await loadCatalogItemsData(pageId);
|
||||||
|
return apiOk(data as unknown as Record<string, unknown>);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { apiOk } from "@/lib/api-response";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { lookupHabboIt } from "@/lib/services/habbo-furnidata-cache";
|
||||||
|
|
||||||
|
export const GET = withAdmin(
|
||||||
|
{ permission: PERMS.CATALOG_VIEW },
|
||||||
|
async (request) => {
|
||||||
|
const raw = request.nextUrl.searchParams.get("classnames") || "";
|
||||||
|
const classnames = raw
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 100);
|
||||||
|
|
||||||
|
const suggestions: Record<string, { name: string; description: string }> =
|
||||||
|
{};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
classnames.map(async (classname) => {
|
||||||
|
const entry = await lookupHabboIt(classname);
|
||||||
|
if (!entry) return;
|
||||||
|
suggestions[classname] = {
|
||||||
|
name: entry.name ?? "",
|
||||||
|
description: entry.description ?? "",
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return apiOk({ suggestions });
|
||||||
|
},
|
||||||
|
);
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -10,7 +10,6 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import type { RootTab } from "@/types/catalog";
|
import type { RootTab } from "@/types/catalog";
|
||||||
@@ -26,16 +25,21 @@ export function CatalogManagerDialog() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen} disablePointerDismissal>
|
<Dialog open={open} onOpenChange={setOpen} disablePointerDismissal>
|
||||||
<DialogTrigger asChild>
|
<Button
|
||||||
<Button variant="outline" className="gap-2">
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="gap-2"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
<LayoutGrid className="h-4 w-4" />
|
<LayoutGrid className="h-4 w-4" />
|
||||||
Visual Manager
|
Visual Manager
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-w-full w-[98vw] h-[95vh] max-h-[95vh] p-0 flex flex-col gap-0 sm:max-w-[98vw]">
|
<DialogContent className="max-w-full w-[98vw] h-[95vh] max-h-[95vh] p-0 flex flex-col gap-0 sm:max-w-[98vw]">
|
||||||
|
{open ? (
|
||||||
<CatalogManagerProvider>
|
<CatalogManagerProvider>
|
||||||
<CatalogManagerInner open={open} onOpenChange={setOpen} />
|
<CatalogManagerInner open={open} onOpenChange={setOpen} />
|
||||||
</CatalogManagerProvider>
|
</CatalogManagerProvider>
|
||||||
|
) : null}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user