Revert "Merge pull request 'Complete Housekeeping migration and /ase cutover' (#52) from codex/housekeeping-complete into main"
This reverts commit488b6e57c4, reversing changes made tob506b4499a.
This commit is contained in:
1 parent
488b6e57c4
commit
b1ddda66ff
802 files changed
+61370
-76659
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Re-export barrel for backward compatibility.
|
||||
* Implementation moved to ./catalog-items-table/ directory.
|
||||
*/
|
||||
export { CatalogItemsTable } from "./catalog-items-table/catalog-items-table";
|
||||
export { FurniIcon } from "./catalog-items-table/furni-icon";
|
||||
export type {
|
||||
BaseItemData,
|
||||
CatalogItemData,
|
||||
} from "./catalog-items-table/types";
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { Image as ImageIcon } from "lucide-react";
|
||||
import { getHabboCdnIconUrl, getLocalIconUrl } from "@/lib/furni/classname";
|
||||
|
||||
export function FurniIcon({
|
||||
classname,
|
||||
revision,
|
||||
}: {
|
||||
classname: string;
|
||||
revision: number;
|
||||
}) {
|
||||
if (!classname) {
|
||||
return (
|
||||
<div className="w-10 h-10 rounded bg-muted flex items-center justify-center">
|
||||
<ImageIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const localUrl = getLocalIconUrl(classname);
|
||||
|
||||
function handleError(e: React.SyntheticEvent<HTMLImageElement>) {
|
||||
const img = e.target as HTMLImageElement;
|
||||
const stage = img.dataset.fallback;
|
||||
if (!stage && revision > 0) {
|
||||
img.dataset.fallback = "cdn";
|
||||
img.src = getHabboCdnIconUrl(classname, revision);
|
||||
} else {
|
||||
img.dataset.fallback = "none";
|
||||
img.style.opacity = "0";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-10 h-10 rounded bg-muted/50 flex items-center justify-center overflow-hidden">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={localUrl}
|
||||
alt={classname}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
onError={handleError}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2, Music } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
getCachedSongPickerOptions,
|
||||
invalidateSongPickerCache,
|
||||
loadSongPickerOptions,
|
||||
type SoundtrackOption,
|
||||
} from "@/lib/client-cache/song-picker-cache";
|
||||
|
||||
// Re-export so existing `import { invalidateSongPickerCache } from '.../song-picker'`
|
||||
// callsites keep working. New code should import from the lib module directly.
|
||||
export { invalidateSongPickerCache };
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
onChange: (id: number) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function formatLength(seconds: number): string {
|
||||
if (!seconds || seconds <= 0) return "";
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = seconds % 60;
|
||||
return `${m}:${String(s).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function SongPicker({ value, onChange, disabled }: Props) {
|
||||
const initial = getCachedSongPickerOptions();
|
||||
const [options, setOptions] = useState<SoundtrackOption[]>(initial ?? []);
|
||||
const [loading, setLoading] = useState(!initial);
|
||||
|
||||
useEffect(() => {
|
||||
if (getCachedSongPickerOptions()) return;
|
||||
let cancelled = false;
|
||||
loadSongPickerOptions()
|
||||
.then((items) => {
|
||||
if (!cancelled) setOptions(items);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setOptions([]);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={String(value)}
|
||||
onValueChange={(v) => onChange(parseInt(v, 10) || 0)}
|
||||
disabled={disabled || loading}
|
||||
>
|
||||
<SelectTrigger>
|
||||
{loading ? (
|
||||
<span className="flex items-center gap-2 text-muted-foreground">
|
||||
<Loader2 className="h-3 w-3 animate-spin" /> Loading…
|
||||
</span>
|
||||
) : (
|
||||
<SelectValue placeholder="No song" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">
|
||||
<span className="text-muted-foreground">— No song —</span>
|
||||
</SelectItem>
|
||||
{options.map((opt) => (
|
||||
<SelectItem key={opt.id} value={String(opt.id)}>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Music className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="font-medium">{opt.name}</span>
|
||||
{opt.author && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
— {opt.author}
|
||||
</span>
|
||||
)}
|
||||
{opt.length > 0 && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{formatLength(opt.length)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
export interface BaseItemData {
|
||||
id: number;
|
||||
spriteId: number;
|
||||
publicName: string;
|
||||
itemName: string;
|
||||
type: string;
|
||||
width: number;
|
||||
length: number;
|
||||
stackHeight: number;
|
||||
allowStack: number;
|
||||
allowSit: number;
|
||||
allowLay: number;
|
||||
allowWalk: number;
|
||||
allowGift: number;
|
||||
allowTrade: number;
|
||||
allowRecycle: number;
|
||||
allowMarketplaceSell: number;
|
||||
allowInventoryStack: number;
|
||||
interactionType: string;
|
||||
interactionModesCount: number;
|
||||
vendingIds: string;
|
||||
customparams: string;
|
||||
effectIdMale: number;
|
||||
effectIdFemale: number;
|
||||
clothingOnWalk: string;
|
||||
}
|
||||
|
||||
export interface CatalogItemData {
|
||||
id: number;
|
||||
catalogName: string;
|
||||
itemIds: string;
|
||||
costCredits: number;
|
||||
costPoints: number;
|
||||
pointsType: number;
|
||||
amount: number;
|
||||
limitedSells: number;
|
||||
limitedStack: number;
|
||||
orderNumber: number;
|
||||
offerId: number;
|
||||
songId: number;
|
||||
haveOffer: string;
|
||||
clubOnly: string;
|
||||
extradata: string;
|
||||
baseName: string;
|
||||
baseItemName: string;
|
||||
spriteId: number;
|
||||
baseItem: BaseItemData | null;
|
||||
}
|
||||
|
||||
export interface InlineEdit {
|
||||
costCredits: number;
|
||||
costPoints: number;
|
||||
pointsType: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export const POINTS_TYPES: Record<number, { label: string; color: string }> = {
|
||||
0: { label: "Duckets", color: "text-[var(--admin-info)]" },
|
||||
5: { label: "Diamonds", color: "text-[var(--admin-accent-text)]" },
|
||||
101: { label: "Snowflakes", color: "text-[var(--admin-info)]" },
|
||||
103: { label: "Hearts", color: "text-[var(--admin-error)]" },
|
||||
104: { label: "Stars", color: "text-[var(--admin-warning)]" },
|
||||
105: { label: "Shells", color: "text-[var(--admin-warning)]" },
|
||||
};
|
||||
|
||||
export const DEFAULT_NEW_ITEM = {
|
||||
catalogName: "",
|
||||
itemIds: "0",
|
||||
costCredits: 0,
|
||||
costPoints: 0,
|
||||
pointsType: 0,
|
||||
amount: 1,
|
||||
limitedSells: 0,
|
||||
limitedStack: 0,
|
||||
orderNumber: 1,
|
||||
offerId: -1,
|
||||
songId: 0,
|
||||
haveOffer: "1",
|
||||
clubOnly: "0",
|
||||
extradata: "",
|
||||
};
|
||||
|
||||
export function getPointsLabel(type: number) {
|
||||
return (
|
||||
POINTS_TYPES[type] || { label: `Type ${type}`, color: "theme-text-muted" }
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,44 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export default function CatalogDetailLoading() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-4 w-4" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-4 w-4" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div>
|
||||
<Skeleton className="h-9 w-64" />
|
||||
<Skeleton className="h-5 w-40 mt-2" />
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2 border-b pb-2">
|
||||
{Array.from({ length: 4 }, (_, i) => i).map((n) => (
|
||||
<Skeleton
|
||||
key={`skeleton-tab-${n}`}
|
||||
className="h-9 w-24 rounded-md"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="rounded-lg border bg-card p-6 space-y-4">
|
||||
{Array.from({ length: 6 }, (_, i) => i).map((n) => (
|
||||
<div key={`skeleton-field-${n}`} className="space-y-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,67 @@
|
||||
import { asc, eq } from "drizzle-orm";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { CatalogItemsBc, CatalogPagesBc, db } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { BcPageDetail } from "../bc-manager";
|
||||
|
||||
export default async function BcPageDetailPage({
|
||||
params: paramsPromise,
|
||||
}: {
|
||||
params: Promise<{ locale: string; id: string }>;
|
||||
}) {
|
||||
const { id: idStr } = await paramsPromise;
|
||||
const id = parseInt(idStr, 10);
|
||||
if (Number.isNaN(id)) notFound();
|
||||
|
||||
const { session, permissions } = await getAdminContext();
|
||||
if (!canAccess(permissions, PERMS.CATALOG_VIEW, session.user.rank)) {
|
||||
redirect("/admin");
|
||||
}
|
||||
|
||||
const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank);
|
||||
|
||||
const [bcPage] = await db
|
||||
.select()
|
||||
.from(CatalogPagesBc)
|
||||
.where(eq(CatalogPagesBc.id, id))
|
||||
.limit(1);
|
||||
if (!bcPage) notFound();
|
||||
|
||||
const bcItems = await db
|
||||
.select()
|
||||
.from(CatalogItemsBc)
|
||||
.where(eq(CatalogItemsBc.pageId, id))
|
||||
.orderBy(asc(CatalogItemsBc.orderNumber));
|
||||
|
||||
return (
|
||||
<BcPageDetail
|
||||
page={{
|
||||
id: bcPage.id,
|
||||
parentId: bcPage.parentId,
|
||||
caption: bcPage.caption,
|
||||
pageLayout: bcPage.pageLayout,
|
||||
iconColor: bcPage.iconColor,
|
||||
iconImage: bcPage.iconImage,
|
||||
orderNum: bcPage.orderNum,
|
||||
visible: bcPage.visible,
|
||||
enabled: bcPage.enabled,
|
||||
pageHeadline: bcPage.pageHeadline,
|
||||
pageTeaser: bcPage.pageTeaser,
|
||||
pageSpecial: bcPage.pageSpecial ?? "",
|
||||
pageText1: bcPage.pageText1 ?? "",
|
||||
pageText2: bcPage.pageText2 ?? "",
|
||||
pageTextDetails: bcPage.pageTextDetails ?? "",
|
||||
pageTextTeaser: bcPage.pageTextTeaser ?? "",
|
||||
}}
|
||||
items={bcItems.map((i) => ({
|
||||
id: i.id,
|
||||
pageId: i.pageId,
|
||||
itemIds: i.itemIds,
|
||||
catalogName: i.catalogName,
|
||||
orderNumber: i.orderNumber,
|
||||
extradata: i.extradata,
|
||||
}))}
|
||||
canEdit={canEdit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function BuilderClubPage() {
|
||||
redirect("/admin/catalog?catalog=bc");
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
export const CatalogManagerDialog = dynamic(
|
||||
() =>
|
||||
import("@/components/admin/catalog-manager/catalog-manager-dialog").then(
|
||||
(m) => ({
|
||||
default: m.CatalogManagerDialog,
|
||||
}),
|
||||
),
|
||||
{ ssr: false },
|
||||
);
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { FolderTree, HardHat, List, Package } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CatalogViewToggleProps {
|
||||
currentView: string;
|
||||
catalogType?: string;
|
||||
}
|
||||
|
||||
export function CatalogViewToggle({
|
||||
currentView,
|
||||
catalogType = "normal",
|
||||
}: CatalogViewToggleProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
function setView(view: string) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (view === "tree") {
|
||||
params.delete("view");
|
||||
} else {
|
||||
params.set("view", view);
|
||||
}
|
||||
router.push(`?${params.toString()}`);
|
||||
}
|
||||
|
||||
function setCatalog(type: string) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (type === "normal") {
|
||||
params.delete("catalog");
|
||||
} else {
|
||||
params.set("catalog", type);
|
||||
}
|
||||
params.delete("view"); // reset to tree when switching
|
||||
router.push(`?${params.toString()}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Catalog type selector */}
|
||||
<div className="flex items-center rounded-md border bg-card">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-8 rounded-r-none",
|
||||
catalogType === "normal" && "bg-accent",
|
||||
)}
|
||||
onClick={() => setCatalog("normal")}
|
||||
>
|
||||
<Package className="mr-1.5 h-4 w-4" />
|
||||
Catalog
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-8 rounded-l-none",
|
||||
catalogType === "bc" && "bg-accent",
|
||||
)}
|
||||
onClick={() => setCatalog("bc")}
|
||||
>
|
||||
<HardHat className="mr-1.5 h-4 w-4" />
|
||||
Builder Club
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* View mode selector */}
|
||||
<div className="flex items-center rounded-md border bg-card">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-8 rounded-r-none",
|
||||
currentView === "tree" && "bg-accent",
|
||||
)}
|
||||
onClick={() => setView("tree")}
|
||||
>
|
||||
<FolderTree className="mr-1.5 h-4 w-4" />
|
||||
Tree
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-8 rounded-l-none",
|
||||
currentView === "table" && "bg-accent",
|
||||
)}
|
||||
onClick={() => setView("table")}
|
||||
>
|
||||
<List className="mr-1.5 h-4 w-4" />
|
||||
Table
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export default function CatalogLoading() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Skeleton className="h-9 w-48" />
|
||||
<Skeleton className="h-5 w-64 mt-2" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-10 w-24" />
|
||||
<Skeleton className="h-10 w-28" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tree skeleton */}
|
||||
<div className="rounded-lg border bg-card">
|
||||
<div className="border-b px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-4 w-4" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-5 w-16 rounded-full" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-56" />
|
||||
</div>
|
||||
<div className="p-2 space-y-1">
|
||||
{Array.from({ length: 12 }, (_, i) => i).map((n) => (
|
||||
<div
|
||||
key={`skeleton-tree-${n}`}
|
||||
className="flex items-center gap-2 px-2 py-1.5"
|
||||
style={{ paddingLeft: `${(n % 3) * 20 + 8}px` }}
|
||||
>
|
||||
<Skeleton className="h-5 w-5 shrink-0" />
|
||||
<Skeleton className="h-5 w-5 shrink-0" />
|
||||
<Skeleton className="h-4 flex-1 max-w-48" />
|
||||
<Skeleton className="h-5 w-8 rounded-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CatalogMaintenancePanel } from "@/components/admin/catalog/catalog-maintenance-panel";
|
||||
|
||||
export default function CatalogMaintenancePage() {
|
||||
return <CatalogMaintenancePanel />;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user