feat(admin): server-safe StatusCard and Import hub polish
Split OnlineUsersWidget from StatusCard, decouple ad delete button, sync badge import to ExternalTexts+WebsiteBadges, add Import section hub with cancelable SSE jobs and upload SQL option. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
db957d7fb1
commit
9c4949186c
21 files changed
+576
-249
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Check, ImageIcon, Loader2, Wrench } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { adminFetch } from "@/lib/admin-fetch";
|
||||
@@ -31,6 +31,7 @@ export function RepairIconsClient() {
|
||||
failed: number;
|
||||
skipped: number;
|
||||
} | null>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const startRepair = useCallback(async () => {
|
||||
setRunning(true);
|
||||
@@ -38,9 +39,13 @@ export function RepairIconsClient() {
|
||||
setLog([]);
|
||||
setResult(null);
|
||||
|
||||
const abort = new AbortController();
|
||||
abortRef.current = abort;
|
||||
|
||||
try {
|
||||
const res = await adminFetch("/api/admin/import/furni/repair-icons", {
|
||||
method: "POST",
|
||||
signal: abort.signal,
|
||||
});
|
||||
|
||||
if (!res.body) {
|
||||
@@ -131,26 +136,42 @@ export function RepairIconsClient() {
|
||||
setResult({ succeeded, failed, skipped });
|
||||
if (succeeded > 0) toast.success(`${succeeded} icons downloaded`);
|
||||
if (failed > 0) toast.error(`${failed} icons could not be obtained`);
|
||||
} catch {
|
||||
toast.error("Repair request failed");
|
||||
} catch (err) {
|
||||
if ((err as Error).name === "AbortError") {
|
||||
toast.info("Repair cancelled");
|
||||
} else {
|
||||
toast.error("Repair request failed");
|
||||
}
|
||||
} finally {
|
||||
setRunning(false);
|
||||
abortRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{!result && (
|
||||
<Button onClick={startRepair} disabled={running} size="lg">
|
||||
{running ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<Wrench className="w-4 h-4 mr-2" />
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Button onClick={startRepair} disabled={running} size="lg">
|
||||
{running ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<Wrench className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{running
|
||||
? `Repairing… ${progress ? `${progress.done}/${progress.total}` : ""}`
|
||||
: "Start Icon Repair"}
|
||||
</Button>
|
||||
{running && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={() => abortRef.current?.abort()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
{running
|
||||
? `Repairing… ${progress ? `${progress.done}/${progress.total}` : ""}`
|
||||
: "Start Icon Repair"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{progress && (
|
||||
|
||||
Reference in new issue
Block a user