EventBuzz_Console/src/app/components/dashboard/EventBadgesPanel.tsx

376 lines
14 KiB
TypeScript

import React, { useCallback, useEffect, useState } from "react";
import { Copy, Download, Plus, Tag, X } from "lucide-react";
import { useAuth } from "../../contexts/AuthContext";
import { badgeBatchesApi, type BadgeBatchRecord } from "../../../services/api/badgeBatches";
function formatRequestedAt(createdAt?: string): string {
if (!createdAt) return "";
const d = new Date(createdAt);
if (Number.isNaN(d.getTime())) return "";
return d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
}
function statusLabel(status: BadgeBatchRecord["status"]): string {
if (status === "ready") return "Ready";
if (status === "processing") return "Processing";
if (status === "failed") return "Failed";
return "Pending";
}
export function EventBadgesPanel({
eventId,
eventName,
onClose,
onAddBadge,
onDuplicateBadge,
}: {
eventId: string;
eventName?: string;
onClose: () => void;
onAddBadge: () => void;
onDuplicateBadge: (batchId: string) => void;
}) {
const { user } = useAuth();
const canManage = Boolean(user && ["super_admin", "admin", "moderator"].includes(user.role));
const [badges, setBadges] = useState<BadgeBatchRecord[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [downloadingId, setDownloadingId] = useState<string | null>(null);
const loadBadges = useCallback(async () => {
setLoading(true);
setError("");
try {
const res = await badgeBatchesApi.list({ event: eventId, limit: 100 });
if (res.error) {
setError(typeof res.error === "string" ? res.error : "Failed to load badges.");
setBadges([]);
} else {
setBadges(res.data?.batches ?? []);
}
} catch {
setError("Failed to load badges.");
setBadges([]);
} finally {
setLoading(false);
}
}, [eventId]);
useEffect(() => {
void loadBadges();
}, [loadBadges]);
return (
<>
<div
onClick={onClose}
role="presentation"
style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", zIndex: 200 }}
/>
<div
role="dialog"
aria-modal="true"
aria-labelledby="event-badges-title"
style={{
position: "fixed",
top: 0,
right: 0,
height: "100%",
width: "min(720px, 100vw)",
background: "var(--card)",
zIndex: 201,
display: "flex",
flexDirection: "column",
boxShadow: "-4px 0 40px rgba(0,0,0,0.2)",
}}
>
<div
style={{
display: "flex",
alignItems: "flex-start",
justifyContent: "space-between",
gap: 12,
padding: "16px 18px",
borderBottom: "1px solid var(--border)",
flexShrink: 0,
}}
>
<div style={{ minWidth: 0 }}>
<p
style={{
fontSize: "var(--text-xs)",
fontFamily: "var(--font-noto-sans)",
color: "var(--muted)",
margin: "0 0 4px",
}}
>
Tag & Badge Configuration
</p>
<h2
id="event-badges-title"
style={{
fontFamily: "var(--font-inter)",
fontSize: "var(--text-lg)",
fontWeight: "var(--font-weight-medium)",
color: "var(--foreground)",
margin: 0,
lineHeight: 1.3,
}}
>
{eventName ? `Badges · ${eventName}` : "Badges"}
</h2>
</div>
<div style={{ display: "flex", gap: 8, flexShrink: 0 }}>
{canManage ? (
<button
type="button"
onClick={onAddBadge}
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
padding: "8px 12px",
borderRadius: "var(--radius-button)",
border: "none",
background: "var(--primary)",
color: "var(--primary-foreground)",
fontSize: "var(--text-xs)",
fontFamily: "var(--font-noto-sans)",
fontWeight: "var(--font-weight-medium)",
cursor: "pointer",
}}
>
<Plus size={14} /> Add Badge
</button>
) : null}
<button
type="button"
onClick={onClose}
aria-label="Close"
style={{
width: 36,
height: 36,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "var(--radius-button)",
border: "1px solid var(--border)",
background: "var(--card)",
color: "var(--muted)",
cursor: "pointer",
}}
>
<X size={18} />
</button>
</div>
</div>
<div style={{ flex: 1, overflow: "auto", padding: "16px 18px" }}>
{loading ? (
<p style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)" }}>
Loading badges…
</p>
) : null}
{error ? (
<p style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--destructive)" }}>
{error}
</p>
) : null}
{!loading && !error && badges.length === 0 ? (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
textAlign: "center",
padding: "48px 24px",
gap: 12,
}}
>
<Tag size={36} style={{ color: "var(--border)" }} />
<p
style={{
fontFamily: "var(--font-inter)",
fontSize: "var(--text-base)",
fontWeight: "var(--font-weight-medium)",
color: "var(--foreground)",
margin: 0,
}}
>
No badges yet
</p>
<p
style={{
fontSize: "var(--text-sm)",
fontFamily: "var(--font-noto-sans)",
color: "var(--muted)",
margin: 0,
maxWidth: 320,
lineHeight: 1.5,
}}
>
Create a badge export to design attendee and speaker badges for this event.
</p>
{canManage ? (
<button
type="button"
onClick={onAddBadge}
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
marginTop: 8,
padding: "8px 14px",
borderRadius: "var(--radius-button)",
border: "none",
background: "var(--primary)",
color: "var(--primary-foreground)",
fontSize: "var(--text-sm)",
fontFamily: "var(--font-noto-sans)",
fontWeight: "var(--font-weight-medium)",
cursor: "pointer",
}}
>
<Plus size={14} /> Add Badge
</button>
) : null}
</div>
) : null}
{!loading && !error && badges.length > 0 ? (
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{badges.map((b) => (
<div
key={b._id}
style={{
background: "var(--card)",
border: "1px solid var(--border)",
borderRadius: "var(--radius-card)",
padding: "14px 16px",
display: "flex",
flexDirection: "column",
gap: 8,
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Tag size={18} style={{ color: "var(--muted)", flexShrink: 0 }} />
<p
style={{
fontFamily: "var(--font-noto-sans)",
fontSize: "var(--text-sm)",
fontWeight: "var(--font-weight-medium)",
color: "var(--foreground)",
margin: 0,
}}
>
{b.name}
</p>
</div>
<span style={{ fontSize: "11px", fontFamily: "var(--font-noto-sans)", color: "var(--muted)" }}>
Status: <strong style={{ color: "var(--foreground)" }}>{statusLabel(b.status)}</strong>
{b.itemCount != null ? ` · ${b.itemCount} badge${b.itemCount !== 1 ? "s" : ""}` : ""}
</span>
{b.lastError && b.status === "failed" ? (
<span style={{ fontSize: "10px", color: "var(--destructive)" }}>{b.lastError}</span>
) : null}
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
{canManage ? (
<button
type="button"
title="Duplicate — copy settings and edit"
onClick={() => onDuplicateBadge(b._id)}
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
padding: "6px 12px",
borderRadius: "var(--radius-button)",
border: "1px solid var(--border)",
background: "var(--card)",
color: "var(--foreground)",
fontSize: "var(--text-xs)",
fontFamily: "var(--font-noto-sans)",
fontWeight: "var(--font-weight-medium)",
cursor: "pointer",
}}
>
<Copy size={13} /> Duplicate
</button>
) : null}
{b.status === "ready" && b.zipUrl ? (
<button
type="button"
disabled={downloadingId === b._id}
onClick={async () => {
setDownloadingId(b._id);
setError("");
try {
const org =
typeof b.organization === "string"
? b.organization
: undefined;
await badgeBatchesApi.downloadZip(b, org);
} catch (e) {
setError(
e instanceof Error ? e.message : "Download failed.",
);
} finally {
setDownloadingId(null);
}
}}
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
padding: "6px 12px",
borderRadius: "var(--radius-button)",
background: "var(--primary)",
color: "var(--primary-foreground)",
border: "none",
fontSize: "var(--text-xs)",
fontFamily: "var(--font-noto-sans)",
fontWeight: "var(--font-weight-medium)",
cursor: downloadingId === b._id ? "wait" : "pointer",
opacity: downloadingId === b._id ? 0.7 : 1,
}}
>
<Download size={13} />{" "}
{downloadingId === b._id ? "Downloading…" : "Download zip"}
</button>
) : (
<button
type="button"
disabled
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
padding: "6px 12px",
borderRadius: "var(--radius-button)",
background: "var(--secondary)",
color: "var(--muted)",
border: "1px solid var(--border)",
fontSize: "var(--text-xs)",
fontFamily: "var(--font-noto-sans)",
cursor: "not-allowed",
}}
>
<Download size={13} /> Download zip
</button>
)}
</div>
{b.createdAt ? (
<span style={{ fontSize: "10px", fontFamily: "var(--font-noto-sans)", color: "var(--muted)" }}>
Requested {formatRequestedAt(b.createdAt)}
</span>
) : null}
</div>
))}
</div>
) : null}
</div>
</div>
</>
);
}