feat: public events/polls, friends graph, captcha, SSE hardening, and admin UX
Ship product gaps: register/vote pages, friend add/accept/decline/remove, email verify TTL, captcha on login/forgot, soft-fail user actions, SSE abort/shared client, Commando Centrum error toasts, admin delete for events/polls, and IT/NL i18n fills. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
2ff08e5127
commit
ed7db6e048
76 files changed
+4834
-1376
No files matched your search
@@ -20,6 +20,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { adminFetch } from "@/lib/admin-fetch";
|
||||
import { runSseImport } from "@/lib/sse-client";
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -55,52 +56,6 @@ interface PageMeta {
|
||||
total: number;
|
||||
}
|
||||
|
||||
// ── Shared SSE reader (mirrors import-clothing-client.tsx) ────────────────────
|
||||
|
||||
async function runSseImport(
|
||||
url: string,
|
||||
body: unknown,
|
||||
onDone: (classname: string) => void,
|
||||
onComplete: (succeeded: number, failed: number) => void,
|
||||
): Promise<void> {
|
||||
const res = await adminFetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.body) {
|
||||
toast.error("No response stream");
|
||||
return;
|
||||
}
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buf = "";
|
||||
let succeeded = 0;
|
||||
let failed = 0;
|
||||
while (true) {
|
||||
const { value, done: streamDone } = await reader.read();
|
||||
if (streamDone) break;
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
const parts = buf.split("\n\n");
|
||||
buf = parts.pop() ?? "";
|
||||
for (const part of parts) {
|
||||
if (!part.startsWith("data: ")) continue;
|
||||
const evt = JSON.parse(part.slice(6));
|
||||
if (
|
||||
evt.type === "item_progress" &&
|
||||
(evt.status === "done" || evt.status === "failed")
|
||||
) {
|
||||
onDone(evt.classname);
|
||||
}
|
||||
if (evt.type === "batch_complete") {
|
||||
succeeded = evt.succeeded;
|
||||
failed = evt.failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
onComplete(succeeded, failed);
|
||||
}
|
||||
|
||||
// ── Source form ───────────────────────────────────────────────────────────────
|
||||
|
||||
interface SourceFormProps {
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getAvatarUrl } from "@/lib/imager";
|
||||
import { adminFetch } from "@/lib/admin-fetch";
|
||||
import { runSseImport } from "@/lib/sse-client";
|
||||
|
||||
// ── Shared types ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -43,52 +44,6 @@ type Mode = "libs" | "sets";
|
||||
|
||||
const PER_PAGE = 100;
|
||||
|
||||
// ── Shared SSE reader ─────────────────────────────────────────────────────────
|
||||
|
||||
async function runSseImport(
|
||||
url: string,
|
||||
body: unknown,
|
||||
onDone: (label: string) => void,
|
||||
onComplete: (succeeded: number, failed: number) => void,
|
||||
): Promise<void> {
|
||||
const res = await adminFetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.body) {
|
||||
toast.error("No response stream");
|
||||
return;
|
||||
}
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buf = "";
|
||||
let succeeded = 0;
|
||||
let failed = 0;
|
||||
while (true) {
|
||||
const { value, done: streamDone } = await reader.read();
|
||||
if (streamDone) break;
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
const parts = buf.split("\n\n");
|
||||
buf = parts.pop() ?? "";
|
||||
for (const part of parts) {
|
||||
if (!part.startsWith("data: ")) continue;
|
||||
const evt = JSON.parse(part.slice(6));
|
||||
if (
|
||||
evt.type === "item_progress" &&
|
||||
(evt.status === "done" || evt.status === "failed")
|
||||
) {
|
||||
onDone(evt.classname);
|
||||
}
|
||||
if (evt.type === "batch_complete") {
|
||||
succeeded = evt.succeeded;
|
||||
failed = evt.failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
onComplete(succeeded, failed);
|
||||
}
|
||||
|
||||
// ── Avatar preview (renders a mini avatar wearing the item) ───────────────────
|
||||
|
||||
// A default dressed avatar; the previewed part overrides/adds its slot so the
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { adminFetch } from "@/lib/admin-fetch";
|
||||
import { readSseStream } from "@/lib/sse-client";
|
||||
|
||||
interface PetItem {
|
||||
lib: string;
|
||||
@@ -109,34 +110,22 @@ export function ImportPetsClient() {
|
||||
setBatchProgress(null);
|
||||
return;
|
||||
}
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buf = "";
|
||||
let done = 0;
|
||||
let succeeded = 0;
|
||||
let failed = 0;
|
||||
while (true) {
|
||||
const { value, done: streamDone } = await reader.read();
|
||||
if (streamDone) break;
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
const parts = buf.split("\n\n");
|
||||
buf = parts.pop() ?? "";
|
||||
for (const part of parts) {
|
||||
if (!part.startsWith("data: ")) continue;
|
||||
const evt = JSON.parse(part.slice(6));
|
||||
if (evt.type === "item_progress" && evt.status === "done") {
|
||||
done++;
|
||||
markDone(evt.classname);
|
||||
setBatchProgress({ done, total: items.length });
|
||||
} else if (evt.type === "item_progress" && evt.status === "failed") {
|
||||
done++;
|
||||
setBatchProgress({ done, total: items.length });
|
||||
} else if (evt.type === "batch_complete") {
|
||||
succeeded = evt.succeeded;
|
||||
failed = evt.failed;
|
||||
}
|
||||
await readSseStream(res.body, (evt) => {
|
||||
if (evt.type === "item_progress" && evt.status === "done") {
|
||||
done++;
|
||||
markDone(String(evt.classname ?? ""));
|
||||
setBatchProgress({ done, total: items.length });
|
||||
} else if (evt.type === "item_progress" && evt.status === "failed") {
|
||||
done++;
|
||||
setBatchProgress({ done, total: items.length });
|
||||
} else if (evt.type === "batch_complete") {
|
||||
succeeded = Number(evt.succeeded ?? 0);
|
||||
failed = Number(evt.failed ?? 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
setBatchProgress(null);
|
||||
setSelected(new Set());
|
||||
if (succeeded > 0) toast.success(`${succeeded} pet(s) imported`);
|
||||
|
||||
Reference in new issue
Block a user