Revert "Merge pull request 'Complete Housekeeping migration and /ase cutover' (#52) from codex/housekeeping-complete into main"

This reverts commit 488b6e57c4, reversing
changes made to b506b4499a.
This commit is contained in:
Simo committed 2026-08-30 21:31:34 +02:00
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
+44
View File
@@ -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>
);
}
+44
View File
@@ -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