style: clean up code with prettier and eslint
This commit is contained in:
1 parent
60278b9e71
commit
fb8e77bb68
378 files changed
+22407
-20689
No files matched your search
@@ -1,4 +1,4 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
Trash2,
|
||||
Wrench,
|
||||
XCircle,
|
||||
} from 'lucide-react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { deleteImportedFurni } from '@/actions/import-furni'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
} from "lucide-react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { deleteImportedFurni } from "@/actions/import-furni";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -24,245 +24,238 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
type MissingKind = 'nitro' | 'icon' | 'both'
|
||||
type MissingKind = "nitro" | "icon" | "both";
|
||||
|
||||
interface MissingItem {
|
||||
id: number
|
||||
classname: string
|
||||
baseClassname: string
|
||||
revision: number
|
||||
type: 's' | 'i'
|
||||
publicName: string
|
||||
hasSwf: boolean
|
||||
hasIcon: boolean
|
||||
hasNitro: boolean
|
||||
missing: MissingKind
|
||||
unrepairable: boolean
|
||||
id: number;
|
||||
classname: string;
|
||||
baseClassname: string;
|
||||
revision: number;
|
||||
type: "s" | "i";
|
||||
publicName: string;
|
||||
hasSwf: boolean;
|
||||
hasIcon: boolean;
|
||||
hasNitro: boolean;
|
||||
missing: MissingKind;
|
||||
unrepairable: boolean;
|
||||
}
|
||||
|
||||
interface AuditSummary {
|
||||
total: number
|
||||
missingBoth: number
|
||||
missingNitro: number
|
||||
missingIcon: number
|
||||
unrepairable: number
|
||||
items: MissingItem[]
|
||||
total: number;
|
||||
missingBoth: number;
|
||||
missingNitro: number;
|
||||
missingIcon: number;
|
||||
unrepairable: number;
|
||||
items: MissingItem[];
|
||||
}
|
||||
|
||||
type ItemStatus = 'idle' | 'working' | 'done' | 'partial' | 'failed'
|
||||
type ItemStatus = "idle" | "working" | "done" | "partial" | "failed";
|
||||
|
||||
interface ItemProgress {
|
||||
status: ItemStatus
|
||||
message?: string
|
||||
nitroRepaired?: boolean
|
||||
iconRepaired?: boolean
|
||||
unrepairable?: boolean
|
||||
status: ItemStatus;
|
||||
message?: string;
|
||||
nitroRepaired?: boolean;
|
||||
iconRepaired?: boolean;
|
||||
unrepairable?: boolean;
|
||||
}
|
||||
|
||||
interface RepairSummary {
|
||||
succeeded: number
|
||||
partial: number
|
||||
failed: number
|
||||
unrepairable: number
|
||||
duration: number
|
||||
succeeded: number;
|
||||
partial: number;
|
||||
failed: number;
|
||||
unrepairable: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
const TAB_FILTERS: Record<string, (m: MissingItem) => boolean> = {
|
||||
all: () => true,
|
||||
both: (m) => m.missing === 'both',
|
||||
nitro: (m) => m.missing === 'nitro',
|
||||
icon: (m) => m.missing === 'icon',
|
||||
both: (m) => m.missing === "both",
|
||||
nitro: (m) => m.missing === "nitro",
|
||||
icon: (m) => m.missing === "icon",
|
||||
unrepairable: (m) => m.unrepairable,
|
||||
}
|
||||
};
|
||||
|
||||
export function RepairClient() {
|
||||
const [summary, setSummary] = useState<AuditSummary | null>(null)
|
||||
const [auditing, setAuditing] = useState(false)
|
||||
const [repairing, setRepairing] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
const [tab, setTab] = useState<keyof typeof TAB_FILTERS>('all')
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||
const [progress, setProgress] = useState<Map<string, ItemProgress>>(new Map())
|
||||
const [lastSummary, setLastSummary] = useState<RepairSummary | null>(null)
|
||||
const [confirmDeleteAll, setConfirmDeleteAll] = useState(false)
|
||||
const [summary, setSummary] = useState<AuditSummary | null>(null);
|
||||
const [auditing, setAuditing] = useState(false);
|
||||
const [repairing, setRepairing] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
const [tab, setTab] = useState<keyof typeof TAB_FILTERS>("all");
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [progress, setProgress] = useState<Map<string, ItemProgress>>(new Map());
|
||||
const [lastSummary, setLastSummary] = useState<RepairSummary | null>(null);
|
||||
const [confirmDeleteAll, setConfirmDeleteAll] = useState(false);
|
||||
|
||||
// ── Audit ──────────────────────────────────────────────────────────
|
||||
const runAudit = useCallback(async () => {
|
||||
setAuditing(true)
|
||||
setLastSummary(null)
|
||||
setAuditing(true);
|
||||
setLastSummary(null);
|
||||
try {
|
||||
const res = await fetch('/api/admin/import/repair/audit', { method: 'POST' })
|
||||
const res = await fetch("/api/admin/import/repair/audit", { method: "POST" });
|
||||
if (!res.ok) {
|
||||
toast.error('Audit failed')
|
||||
return
|
||||
toast.error("Audit failed");
|
||||
return;
|
||||
}
|
||||
const data = await res.json()
|
||||
setSummary(data)
|
||||
setSelected(new Set())
|
||||
setProgress(new Map())
|
||||
const data = await res.json();
|
||||
setSummary(data);
|
||||
setSelected(new Set());
|
||||
setProgress(new Map());
|
||||
toast.success(
|
||||
`Audit: ${data.items.length} item${data.items.length === 1 ? '' : 's'} need repair (${data.total} scanned)`,
|
||||
)
|
||||
`Audit: ${data.items.length} item${data.items.length === 1 ? "" : "s"} need repair (${data.total} scanned)`,
|
||||
);
|
||||
} catch {
|
||||
toast.error('Audit network error')
|
||||
toast.error("Audit network error");
|
||||
} finally {
|
||||
setAuditing(false)
|
||||
setAuditing(false);
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
// ── Filters ────────────────────────────────────────────────────────
|
||||
const filtered = useMemo(() => {
|
||||
if (!summary) return []
|
||||
const s = search.trim().toLowerCase()
|
||||
if (!summary) return [];
|
||||
const s = search.trim().toLowerCase();
|
||||
return summary.items.filter((it) => {
|
||||
if (!TAB_FILTERS[tab](it)) return false
|
||||
if (!s) return true
|
||||
if (!TAB_FILTERS[tab](it)) return false;
|
||||
if (!s) return true;
|
||||
return (
|
||||
it.classname.toLowerCase().includes(s) ||
|
||||
it.publicName.toLowerCase().includes(s) ||
|
||||
String(it.id).includes(s)
|
||||
)
|
||||
})
|
||||
}, [summary, search, tab])
|
||||
);
|
||||
});
|
||||
}, [summary, search, tab]);
|
||||
|
||||
// ── Selection ──────────────────────────────────────────────────────
|
||||
const allSelected = filtered.length > 0 && filtered.every((i) => selected.has(i.classname))
|
||||
const allSelected = filtered.length > 0 && filtered.every((i) => selected.has(i.classname));
|
||||
const toggleAll = () => {
|
||||
setSelected((prev) => {
|
||||
if (allSelected) {
|
||||
const next = new Set(prev)
|
||||
const next = new Set(prev);
|
||||
for (const i of filtered) {
|
||||
next.delete(i.classname)
|
||||
next.delete(i.classname);
|
||||
}
|
||||
return next
|
||||
return next;
|
||||
}
|
||||
const next = new Set(prev)
|
||||
const next = new Set(prev);
|
||||
for (const i of filtered) {
|
||||
next.add(i.classname)
|
||||
next.add(i.classname);
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const toggleOne = (cn: string) => {
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(cn)) next.delete(cn)
|
||||
else next.add(cn)
|
||||
return next
|
||||
})
|
||||
}
|
||||
const next = new Set(prev);
|
||||
if (next.has(cn)) next.delete(cn);
|
||||
else next.add(cn);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// ── Repair ─────────────────────────────────────────────────────────
|
||||
const runRepair = useCallback(async (classnames: string[]) => {
|
||||
if (classnames.length === 0) return
|
||||
setRepairing(true)
|
||||
setLastSummary(null)
|
||||
if (classnames.length === 0) return;
|
||||
setRepairing(true);
|
||||
setLastSummary(null);
|
||||
|
||||
// Init progress for these items
|
||||
setProgress((prev) => {
|
||||
const next = new Map(prev)
|
||||
const next = new Map(prev);
|
||||
for (const cn of classnames) {
|
||||
next.set(cn, { status: 'working' })
|
||||
next.set(cn, { status: "working" });
|
||||
}
|
||||
return next
|
||||
})
|
||||
return next;
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/admin/import/repair', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
const res = await fetch("/api/admin/import/repair", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ classnames, concurrency: 6 }),
|
||||
})
|
||||
});
|
||||
if (!res.ok || !res.body) {
|
||||
toast.error('Failed to start repair')
|
||||
setRepairing(false)
|
||||
return
|
||||
toast.error("Failed to start repair");
|
||||
setRepairing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = res.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue
|
||||
if (!line.startsWith("data: ")) continue;
|
||||
try {
|
||||
const event = JSON.parse(line.slice(6))
|
||||
if (event.type === 'repair_progress') {
|
||||
const event = JSON.parse(line.slice(6));
|
||||
if (event.type === "repair_progress") {
|
||||
setProgress((prev) => {
|
||||
const next = new Map(prev)
|
||||
const next = new Map(prev);
|
||||
next.set(event.classname, {
|
||||
status: event.status as ItemStatus,
|
||||
message: event.error || (event.warnings?.[0] ?? undefined),
|
||||
nitroRepaired: event.nitroRepaired,
|
||||
iconRepaired: event.iconRepaired,
|
||||
unrepairable: event.unrepairable,
|
||||
})
|
||||
return next
|
||||
})
|
||||
});
|
||||
return next;
|
||||
});
|
||||
// Promote newly-discovered unrepairable flags onto the audit list
|
||||
if (event.unrepairable) {
|
||||
setSummary((prev) => {
|
||||
if (!prev) return prev
|
||||
const item = prev.items.find((i) => i.classname === event.classname)
|
||||
if (!item || item.unrepairable) return prev
|
||||
if (!prev) return prev;
|
||||
const item = prev.items.find((i) => i.classname === event.classname);
|
||||
if (!item || item.unrepairable) return prev;
|
||||
return {
|
||||
...prev,
|
||||
unrepairable: prev.unrepairable + 1,
|
||||
items: prev.items.map((i) =>
|
||||
i.classname === event.classname ? { ...i, unrepairable: true } : i,
|
||||
),
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
// Remove repaired items from the audit list on "done"
|
||||
if (event.status === 'done') {
|
||||
if (event.status === "done") {
|
||||
setSummary((prev) => {
|
||||
if (!prev) return prev
|
||||
const items = prev.items.filter((i) => i.classname !== event.classname)
|
||||
const diff = prev.items.length - items.length
|
||||
if (diff === 0) return prev
|
||||
const removed = prev.items.find((i) => i.classname === event.classname)
|
||||
if (!prev) return prev;
|
||||
const items = prev.items.filter((i) => i.classname !== event.classname);
|
||||
const diff = prev.items.length - items.length;
|
||||
if (diff === 0) return prev;
|
||||
const removed = prev.items.find((i) => i.classname === event.classname);
|
||||
return {
|
||||
...prev,
|
||||
items,
|
||||
missingBoth: prev.missingBoth - (removed?.missing === 'both' ? 1 : 0),
|
||||
missingNitro: prev.missingNitro - (removed?.missing === 'nitro' ? 1 : 0),
|
||||
missingIcon: prev.missingIcon - (removed?.missing === 'icon' ? 1 : 0),
|
||||
}
|
||||
})
|
||||
missingBoth: prev.missingBoth - (removed?.missing === "both" ? 1 : 0),
|
||||
missingNitro: prev.missingNitro - (removed?.missing === "nitro" ? 1 : 0),
|
||||
missingIcon: prev.missingIcon - (removed?.missing === "icon" ? 1 : 0),
|
||||
};
|
||||
});
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.delete(event.classname)
|
||||
return next
|
||||
})
|
||||
const next = new Set(prev);
|
||||
next.delete(event.classname);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
} else if (event.type === 'repair_complete') {
|
||||
} else if (event.type === "repair_complete") {
|
||||
setLastSummary({
|
||||
succeeded: event.succeeded,
|
||||
partial: event.partial,
|
||||
failed: event.failed,
|
||||
unrepairable: event.unrepairable ?? 0,
|
||||
duration: event.duration,
|
||||
})
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
/* skip malformed */
|
||||
@@ -270,72 +263,72 @@ export function RepairClient() {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
toast.error('Connection lost during repair')
|
||||
toast.error("Connection lost during repair");
|
||||
} finally {
|
||||
setRepairing(false)
|
||||
setRepairing(false);
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
// ── Delete unrepairable items ──────────────────────────────────────
|
||||
const deleteSelected = useCallback(async () => {
|
||||
const toDelete = Array.from(selected)
|
||||
if (toDelete.length === 0) return
|
||||
setConfirmDeleteAll(false)
|
||||
const toDelete = Array.from(selected);
|
||||
if (toDelete.length === 0) return;
|
||||
setConfirmDeleteAll(false);
|
||||
|
||||
let ok = 0
|
||||
let fail = 0
|
||||
let ok = 0;
|
||||
let fail = 0;
|
||||
for (const cn of toDelete) {
|
||||
setProgress((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(cn, { status: 'working', message: 'Deleting…' })
|
||||
return next
|
||||
})
|
||||
const next = new Map(prev);
|
||||
next.set(cn, { status: "working", message: "Deleting…" });
|
||||
return next;
|
||||
});
|
||||
try {
|
||||
const result = await deleteImportedFurni({ classname: cn })
|
||||
const result = await deleteImportedFurni({ classname: cn });
|
||||
if (result.ok) {
|
||||
ok++
|
||||
ok++;
|
||||
setSummary((prev) => {
|
||||
if (!prev) return prev
|
||||
const removed = prev.items.find((i) => i.classname === cn)
|
||||
if (!removed) return prev
|
||||
if (!prev) return prev;
|
||||
const removed = prev.items.find((i) => i.classname === cn);
|
||||
if (!removed) return prev;
|
||||
return {
|
||||
...prev,
|
||||
total: prev.total - 1,
|
||||
items: prev.items.filter((i) => i.classname !== cn),
|
||||
missingBoth: prev.missingBoth - (removed.missing === 'both' ? 1 : 0),
|
||||
missingNitro: prev.missingNitro - (removed.missing === 'nitro' ? 1 : 0),
|
||||
missingIcon: prev.missingIcon - (removed.missing === 'icon' ? 1 : 0),
|
||||
}
|
||||
})
|
||||
missingBoth: prev.missingBoth - (removed.missing === "both" ? 1 : 0),
|
||||
missingNitro: prev.missingNitro - (removed.missing === "nitro" ? 1 : 0),
|
||||
missingIcon: prev.missingIcon - (removed.missing === "icon" ? 1 : 0),
|
||||
};
|
||||
});
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.delete(cn)
|
||||
return next
|
||||
})
|
||||
const next = new Set(prev);
|
||||
next.delete(cn);
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
fail++
|
||||
fail++;
|
||||
setProgress((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(cn, { status: 'failed', message: 'Delete failed' })
|
||||
return next
|
||||
})
|
||||
const next = new Map(prev);
|
||||
next.set(cn, { status: "failed", message: "Delete failed" });
|
||||
return next;
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
fail++
|
||||
fail++;
|
||||
setProgress((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(cn, { status: 'failed', message: 'Delete error' })
|
||||
return next
|
||||
})
|
||||
const next = new Map(prev);
|
||||
next.set(cn, { status: "failed", message: "Delete error" });
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
toast.success(`Deleted ${ok}${fail > 0 ? `, ${fail} failed` : ''}`)
|
||||
}, [selected])
|
||||
toast.success(`Deleted ${ok}${fail > 0 ? `, ${fail} failed` : ""}`);
|
||||
}, [selected]);
|
||||
|
||||
// ── Render ─────────────────────────────────────────────────────────
|
||||
|
||||
const selectedArray = Array.from(selected)
|
||||
const selectedCount = selectedArray.length
|
||||
const selectedArray = Array.from(selected);
|
||||
const selectedCount = selectedArray.length;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -344,9 +337,8 @@ export function RepairClient() {
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Repair Imported Assets</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Re-download missing <code className="rounded bg-muted px-1">.nitro</code> bundles and
|
||||
icons for items already imported. Items with no public asset on habbo.com can be deleted
|
||||
from here.
|
||||
Re-download missing <code className="rounded bg-muted px-1">.nitro</code> bundles and icons for
|
||||
items already imported. Items with no public asset on habbo.com can be deleted from here.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -357,7 +349,7 @@ export function RepairClient() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4" /> {summary ? 'Re-scan' : 'Scan Assets'}
|
||||
<RefreshCw className="mr-2 h-4 w-4" /> {summary ? "Re-scan" : "Scan Assets"}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -368,12 +360,10 @@ export function RepairClient() {
|
||||
{!summary && !auditing && (
|
||||
<div className="rounded-md border bg-muted/30 p-10 text-center">
|
||||
<Wrench className="mx-auto mb-3 h-10 w-10 text-muted-foreground" />
|
||||
<p className="font-medium">
|
||||
Click “Scan Assets” to audit imported furniture.
|
||||
</p>
|
||||
<p className="font-medium">Click “Scan Assets” to audit imported furniture.</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This checks every row in <code>items_base</code> against files on disk. Scanning ~33k
|
||||
items takes a few seconds.
|
||||
This checks every row in <code>items_base</code> against files on disk. Scanning ~33k items takes
|
||||
a few seconds.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -388,12 +378,7 @@ export function RepairClient() {
|
||||
tone="danger"
|
||||
sub="no .nitro · no icon"
|
||||
/>
|
||||
<StatCard
|
||||
label="Missing .nitro"
|
||||
value={summary.missingNitro}
|
||||
tone="warn"
|
||||
sub="shows black cube"
|
||||
/>
|
||||
<StatCard label="Missing .nitro" value={summary.missingNitro} tone="warn" sub="shows black cube" />
|
||||
<StatCard
|
||||
label="Missing icon"
|
||||
value={summary.missingIcon}
|
||||
@@ -437,9 +422,7 @@ export function RepairClient() {
|
||||
<div className="rounded-md border bg-muted/30 p-10 text-center">
|
||||
<CheckCircle2 className="mx-auto mb-3 h-10 w-10 text-[var(--admin-success)]" />
|
||||
<p className="font-medium">All imported items have their assets on disk.</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{summary.total} items scanned, nothing to repair.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">{summary.total} items scanned, nothing to repair.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -461,16 +444,15 @@ export function RepairClient() {
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
// Skip unrepairable items — no point paying the network cost for them
|
||||
const pool = selectedCount > 0 ? selectedArray : filtered.map((i) => i.classname)
|
||||
const unrep =
|
||||
summary?.items.filter((i) => i.unrepairable).map((i) => i.classname) ?? []
|
||||
const unrepSet = new Set(unrep)
|
||||
const targets = pool.filter((cn) => !unrepSet.has(cn))
|
||||
const pool = selectedCount > 0 ? selectedArray : filtered.map((i) => i.classname);
|
||||
const unrep = summary?.items.filter((i) => i.unrepairable).map((i) => i.classname) ?? [];
|
||||
const unrepSet = new Set(unrep);
|
||||
const targets = pool.filter((cn) => !unrepSet.has(cn));
|
||||
if (targets.length === 0) {
|
||||
toast.info('Nothing to repair — selected items are all flagged unrepairable.')
|
||||
return
|
||||
toast.info("Nothing to repair — selected items are all flagged unrepairable.");
|
||||
return;
|
||||
}
|
||||
runRepair(targets)
|
||||
runRepair(targets);
|
||||
}}
|
||||
disabled={repairing || filtered.length === 0}
|
||||
>
|
||||
@@ -502,9 +484,9 @@ export function RepairClient() {
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
// Auto-select every unrepairable item, then open the same confirm dialog
|
||||
const unrep = summary.items.filter((i) => i.unrepairable).map((i) => i.classname)
|
||||
setSelected(new Set(unrep))
|
||||
setConfirmDeleteAll(true)
|
||||
const unrep = summary.items.filter((i) => i.unrepairable).map((i) => i.classname);
|
||||
setSelected(new Set(unrep));
|
||||
setConfirmDeleteAll(true);
|
||||
}}
|
||||
disabled={repairing}
|
||||
>
|
||||
@@ -529,11 +511,7 @@ export function RepairClient() {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[40px]">
|
||||
<Checkbox
|
||||
checked={allSelected}
|
||||
onCheckedChange={toggleAll}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
<Checkbox checked={allSelected} onCheckedChange={toggleAll} aria-label="Select all" />
|
||||
</TableHead>
|
||||
<TableHead className="w-[80px]">ID</TableHead>
|
||||
<TableHead>Classname</TableHead>
|
||||
@@ -576,13 +554,12 @@ export function RepairClient() {
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Delete {selectedCount} imported item{selectedCount === 1 ? '' : 's'}?
|
||||
Delete {selectedCount} imported item{selectedCount === 1 ? "" : "s"}?
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
This removes the rows from <code>items_base</code> / <code>catalog_items</code>, the
|
||||
entry in <code>FurnitureData.json</code>, and any remaining asset files on disk. Use
|
||||
this for items that have no public asset on habbo.com and therefore cannot be
|
||||
repaired. This cannot be undone.
|
||||
This removes the rows from <code>items_base</code> / <code>catalog_items</code>, the entry in{" "}
|
||||
<code>FurnitureData.json</code>, and any remaining asset files on disk. Use this for items that
|
||||
have no public asset on habbo.com and therefore cannot be repaired. This cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
@@ -596,7 +573,7 @@ export function RepairClient() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// ── Small cell helpers ────────────────────────────────────────────────
|
||||
@@ -607,24 +584,24 @@ function StatCard({
|
||||
tone,
|
||||
sub,
|
||||
}: {
|
||||
label: string
|
||||
value: number
|
||||
tone: 'default' | 'danger' | 'warn' | 'info'
|
||||
sub?: string
|
||||
label: string;
|
||||
value: number;
|
||||
tone: "default" | "danger" | "warn" | "info";
|
||||
sub?: string;
|
||||
}) {
|
||||
const tones = {
|
||||
default: 'border-border',
|
||||
danger: 'border-destructive/40 bg-destructive/5',
|
||||
warn: 'border-[var(--admin-warning-border)] bg-[var(--admin-warning-subtle)]',
|
||||
info: 'border-[var(--admin-info)]/40 bg-[var(--admin-info)]/5',
|
||||
}
|
||||
default: "border-border",
|
||||
danger: "border-destructive/40 bg-destructive/5",
|
||||
warn: "border-[var(--admin-warning-border)] bg-[var(--admin-warning-subtle)]",
|
||||
info: "border-[var(--admin-info)]/40 bg-[var(--admin-info)]/5",
|
||||
};
|
||||
return (
|
||||
<div className={`rounded-md border p-4 ${tones[tone]}`}>
|
||||
<div className="text-xs uppercase tracking-wide text-muted-foreground">{label}</div>
|
||||
<div className="mt-1 text-2xl font-bold">{value.toLocaleString()}</div>
|
||||
{sub && <div className="mt-0.5 text-xs text-muted-foreground">{sub}</div>}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function FileBadge({ present }: { present: boolean }) {
|
||||
@@ -639,49 +616,49 @@ function FileBadge({ present }: { present: boolean }) {
|
||||
<Badge variant="secondary" className="border-destructive/40 bg-destructive/10 text-destructive">
|
||||
<XCircle className="mr-1 h-3 w-3" /> Missing
|
||||
</Badge>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function StatusCell({ progress }: { progress?: ItemProgress }) {
|
||||
if (!progress || progress.status === 'idle')
|
||||
return <span className="text-muted-foreground text-xs">—</span>
|
||||
if (progress.status === 'working') {
|
||||
if (!progress || progress.status === "idle")
|
||||
return <span className="text-muted-foreground text-xs">—</span>;
|
||||
if (progress.status === "working") {
|
||||
return (
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
{progress.message || 'Working…'}
|
||||
{progress.message || "Working…"}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
||||
if (progress.status === 'done') {
|
||||
if (progress.status === "done") {
|
||||
const parts = [
|
||||
progress.nitroRepaired ? '.nitro ✓' : null,
|
||||
progress.iconRepaired ? 'icon ✓' : null,
|
||||
].filter(Boolean)
|
||||
progress.nitroRepaired ? ".nitro ✓" : null,
|
||||
progress.iconRepaired ? "icon ✓" : null,
|
||||
].filter(Boolean);
|
||||
return (
|
||||
<span className="flex items-center gap-1 text-xs text-[var(--admin-success)] dark:text-[var(--admin-success)]">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
{parts.length > 0 ? parts.join(' · ') : 'Repaired'}
|
||||
{parts.length > 0 ? parts.join(" · ") : "Repaired"}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
||||
if (progress.status === 'partial') {
|
||||
if (progress.status === "partial") {
|
||||
return (
|
||||
<span
|
||||
className="flex items-center gap-1 text-xs text-[var(--admin-warning)] dark:text-[var(--admin-warning)]"
|
||||
title={progress.message}
|
||||
>
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
Partial · {progress.message || 'check logs'}
|
||||
Partial · {progress.message || "check logs"}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="flex items-center gap-1 text-xs text-destructive" title={progress.message}>
|
||||
<XCircle className="h-3 w-3" />
|
||||
{progress.message || 'Failed'}
|
||||
{progress.message || "Failed"}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ItemRow({
|
||||
@@ -692,39 +669,32 @@ function ItemRow({
|
||||
onRepair,
|
||||
disabled,
|
||||
}: {
|
||||
item: MissingItem
|
||||
selected: boolean
|
||||
onToggle: () => void
|
||||
progress?: ItemProgress
|
||||
onRepair: () => void
|
||||
disabled: boolean
|
||||
item: MissingItem;
|
||||
selected: boolean;
|
||||
onToggle: () => void;
|
||||
progress?: ItemProgress;
|
||||
onRepair: () => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const isUnrepairable = item.unrepairable || progress?.unrepairable
|
||||
const isUnrepairable = item.unrepairable || progress?.unrepairable;
|
||||
return (
|
||||
<TableRow className={isUnrepairable ? 'bg-destructive/5' : undefined}>
|
||||
<TableRow className={isUnrepairable ? "bg-destructive/5" : undefined}>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
onCheckedChange={onToggle}
|
||||
aria-label={`Select ${item.classname}`}
|
||||
/>
|
||||
<Checkbox checked={selected} onCheckedChange={onToggle} aria-label={`Select ${item.classname}`} />
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">{item.id}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm">{item.classname}</span>
|
||||
{isUnrepairable && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="border-destructive/40 bg-destructive/10 text-destructive"
|
||||
>
|
||||
<Badge variant="secondary" className="border-destructive/40 bg-destructive/10 text-destructive">
|
||||
<AlertCircle className="mr-1 h-3 w-3" /> Unrepairable
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">{item.publicName}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">{item.revision || '—'}</TableCell>
|
||||
<TableCell className="text-xs">{item.revision || "—"}</TableCell>
|
||||
<TableCell>
|
||||
<FileBadge present={item.hasNitro} />
|
||||
</TableCell>
|
||||
@@ -742,10 +712,10 @@ function ItemRow({
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onRepair}
|
||||
disabled={disabled || progress?.status === 'working' || isUnrepairable}
|
||||
title={isUnrepairable ? 'habbo.com does not have this asset' : 'Retry download + convert'}
|
||||
disabled={disabled || progress?.status === "working" || isUnrepairable}
|
||||
title={isUnrepairable ? "habbo.com does not have this asset" : "Retry download + convert"}
|
||||
>
|
||||
{progress?.status === 'working' ? (
|
||||
{progress?.status === "working" ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Wrench className="h-3 w-3" />
|
||||
@@ -753,5 +723,5 @@ function ItemRow({
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user