import React, { useState, useEffect, useRef, useCallback } from "react"; import { Palette, Mic2, Users, FileText, BookOpen, MessageSquare, Shield, Image, Layout, Tag, Mail, Plus, Search, X, MoreHorizontal, Upload, Download, Trash2, Pencil, Copy, ChevronDown, ChevronLeft, ChevronRight, RefreshCw, Link2, List, Check, Share2, } from "lucide-react"; import { useBreakpoint } from "../../hooks/useBreakpoint"; import { useAuth } from "../../contexts/AuthContext"; import { useAppNav } from "../../contexts/AppNavContext"; import { tagChipStyle } from "../participants/participantTagUtils"; import { useParticipantSelection } from "../participants/useParticipantSelection"; import { useParticipantBulkActions } from "../participants/useParticipantBulkActions"; import { ParticipantTagFilters } from "../participants/ParticipantTagFilters"; import { ParticipantSelectAllMatching } from "../participants/ParticipantSelectAllMatching"; import { ParticipantBulkActionBar } from "../participants/ParticipantBulkActionBar"; import { ParticipantBulkDialogs } from "../participants/ParticipantBulkDialogs"; import { designTemplatesApi, speakersApi, participantsApi, formsApi, usersApi, eventsApi, participantListsApi, listMedia, contentCollectionsApi, portfolioListOrganizationId, eventMicrositesApi, badgeBatchesApi, } from "../../../services/api"; import type { DesignTemplate, DesignTemplateType, BadgeBatchRecord, Speaker, ParticipantWithEvent, FormWithSubmissionCount, User, ParticipantListRecord, ListMemberRow, Event, LibraryContent, ContentCollectionRecord, EventMicrosite, } from "../../../services/api"; import { LoadingState } from "../shared"; import { ConfirmModal } from "../shared"; import { AddSpeakerPanel } from "../panels/AddSpeakerPanel"; import { CreateCustomListPanel } from "../panels/CreateCustomListPanel"; import { ImportParticipantsPanel } from "../panels/ImportParticipantsPanel"; import { EditParticipantPanel } from "../panels/EditParticipantPanel"; import { CreateFormPanel } from "../panels/CreateFormPanel"; import { FormSubmissionsPanel } from "../panels/FormSubmissionsPanel"; import { ShareFormPanel } from "../forms/ShareFormPanel"; import { usePermission } from "../../hooks/usePermission"; import { UploadFilePanel } from "../panels/UploadFilePanel"; import { CreateCollectionPanel } from "../panels/CreateCollectionPanel"; import { MessagingEmailPage } from "../messaging/MessagingEmailPage"; import { MessagingWhatsappPage } from "../messaging/MessagingWhatsappPage"; import { InviteUserPanel } from "../panels/InviteUserPanel"; import { CreateBrochurePanel } from "../panels/CreateBrochurePanel"; import { fetchBrochurePdfContext, buildBrochurePreviewData, downloadBrochurePdfOffscreen, BrochurePreviewModal, type BrochurePdfContext, } from "../brochure"; import { CreateMicrositePanel } from "../panels/CreateMicrositePanel"; import { CreateBadgePanel } from "../panels/CreateBadgePanel"; import { CreateEmailerPanel } from "../panels/CreateEmailerPanel"; import { ImportDesignPanel } from "../panels/ImportDesignPanel"; import { IS, Label, Field, RadioGroup, focusIn, focusOut } from "../panels/PanelShell"; /* ────────────────────────────────────── Shared helpers ────────────────────────────────────── */ const labelStyle: React.CSSProperties = { fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", marginBottom: "4px", }; /** Horizontally scrollable card for tables on small screens */ function ScrollTable({ minWidth, children }: { minWidth?: number; children: React.ReactNode }) { return (
{children}
); } function EmptyState({ icon, title, description, cta, onCta }: { icon: React.ReactNode; title: string; description: string; cta?: string; onCta?: () => void; }) { return (
{icon}

{title}

{description}

{cta && onCta && ( )}
); } /* ────────────────────────────────────── Design pages (dynamic: brochure, microsite, badges, emailers) ────────────────────────────────────── */ const SUBPAGE_TO_TYPE: Record = { "design-brochure": "brochure", "design-microsite": "microsite", "design-badges": "badge", "design-emailers": "emailer", }; const templateIcons: Record = { Brochure: , "Microsite Templates": , Badges: , Emailers: , }; function formatLastEdited(updatedAt?: string): string { if (!updatedAt) return "—"; const d = new Date(updatedAt); const now = new Date(); const diffMs = now.getTime() - d.getTime(); const days = Math.floor(diffMs / (24 * 60 * 60 * 1000)); if (days === 0) return "Today"; if (days === 1) return "Yesterday"; if (days < 7) return `${days} days ago`; if (days < 30) return `${Math.floor(days / 7)} week(s) ago`; if (days < 365) return `${Math.floor(days / 30)} month(s) ago`; return d.toLocaleDateString(); } export function DesignPage({ subPage }: { subPage?: string }) { const { user } = useAuth(); const bp = useBreakpoint(); const isMobile = bp === "mobile"; const activeTab = subPage === "design-brochure" ? "Brochure" : subPage === "design-microsite" ? "Microsite Templates" : subPage === "design-badges" ? "Badges" : subPage === "design-emailers" ? "Emailers" : "Brochure"; const apiType = (subPage && SUBPAGE_TO_TYPE[subPage]) || "brochure"; const isSuperAdmin = user?.role === "super_admin"; const canManageMicrosite = apiType === "microsite" && user && ["super_admin", "admin", "moderator"].includes(user.role); const canManageBadgeBatch = Boolean(user && ["super_admin", "admin", "moderator"].includes(user.role)); const [templates, setTemplates] = useState([]); const [badgeBatches, setBadgeBatches] = useState([]); const [badgeBatchesRefreshing, setBadgeBatchesRefreshing] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [microEvents, setMicroEvents] = useState([]); const [orgMicrosites, setOrgMicrosites] = useState([]); const [microLoading, setMicroLoading] = useState(false); const [microError, setMicroError] = useState(null); const [hovered, setHovered] = useState(null); const [showBrochure, setShowBrochure] = useState(false); const [showMicrosite, setShowMicrosite] = useState(false); const [showBadge, setShowBadge] = useState(false); const [duplicateBadgeBatchId, setDuplicateBadgeBatchId] = useState(null); const [showEmailer, setShowEmailer] = useState(false); const [showImport, setShowImport] = useState(false); const [editTemplateId, setEditTemplateId] = useState(null); const [editMicrositeEventId, setEditMicrositeEventId] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState(null); const [brochurePdfBusyId, setBrochurePdfBusyId] = useState(null); const [brochurePdfError, setBrochurePdfError] = useState(null); const [brochurePreviewCtx, setBrochurePreviewCtx] = useState(null); const [emailerPreview, setEmailerPreview] = useState<{ html: string; title: string } | null>(null); const [emailerPreviewBusyId, setEmailerPreviewBusyId] = useState(null); const fetchDesignList = useCallback((options?: { badgeSoftRefresh?: boolean }) => { const badgeSoftRefresh = options?.badgeSoftRefresh === true; if (apiType === "microsite") { setLoading(false); setTemplates([]); setMicroLoading(true); setMicroError(null); setOrgMicrosites([]); const orgId = portfolioListOrganizationId(user); if (!orgId) { setMicroEvents([]); setMicroError( isSuperAdmin ? "Assign an organisation to your account to load events." : "Your profile has no organisation.", ); setMicroLoading(false); return; } Promise.all([ eventsApi.list({ limit: 100, organization: orgId }), eventMicrositesApi.listForOrganization(orgId), ]) .then(([evRes, msRes]) => { if (evRes.data?.events != null) { setMicroEvents(evRes.data.events); } else { setMicroEvents([]); setMicroError(evRes.error || "Failed to load events"); } if (msRes.error || !msRes.data) { setOrgMicrosites([]); } else { setOrgMicrosites(msRes.data); } }) .catch(() => { setMicroEvents([]); setOrgMicrosites([]); setMicroError("Failed to load events"); }) .finally(() => setMicroLoading(false)); return; } if (apiType === "badge") { setMicroLoading(false); if (badgeSoftRefresh) setBadgeBatchesRefreshing(true); else setLoading(true); setError(null); setTemplates([]); const orgId = portfolioListOrganizationId(user); if (!orgId) { setBadgeBatches([]); setError( isSuperAdmin ? "Assign an organisation to your account to load badge exports." : "Your profile has no organisation.", ); if (badgeSoftRefresh) setBadgeBatchesRefreshing(false); else setLoading(false); return; } badgeBatchesApi .list({ organization: orgId, limit: 100 }) .then((res) => { if (res.data?.batches != null) setBadgeBatches(res.data.batches); else setError(res.error || "Failed to load badge exports"); }) .catch(() => setError("Failed to load badge exports")) .finally(() => { if (badgeSoftRefresh) setBadgeBatchesRefreshing(false); else setLoading(false); }); return; } setMicroLoading(false); setLoading(true); setError(null); const orgId = portfolioListOrganizationId(user); if (!orgId) { setTemplates([]); setError( isSuperAdmin ? "Assign an organisation to your account (or use an org-scoped user) to load design templates." : "Your profile has no organisation.", ); setLoading(false); return; } designTemplatesApi .list({ type: apiType, limit: 100, organization: orgId }) .then((res) => { if (res.data?.templates != null) setTemplates(res.data.templates); else setError(res.error || "Failed to load templates"); }) .catch(() => setError("Failed to load templates")) .finally(() => setLoading(false)); }, [apiType, user, isSuperAdmin]); useEffect(() => { fetchDesignList(); }, [fetchDesignList]); const handleCreate = () => { setEditTemplateId(null); setEditMicrositeEventId(null); if (activeTab === "Brochure") setShowBrochure(true); else if (activeTab === "Microsite Templates") setShowMicrosite(true); else if (activeTab === "Badges") { setDuplicateBadgeBatchId(null); setShowBadge(true); } else if (activeTab === "Emailers") setShowEmailer(true); }; const handleEdit = (t: DesignTemplate) => { setEditTemplateId(t._id); if (apiType === "brochure") setShowBrochure(true); else if (apiType === "emailer") setShowEmailer(true); }; const handleOpenMicrositeForEvent = (ev: Event) => { setEditMicrositeEventId(ev._id); setShowMicrosite(true); }; const handleClosePanel = (panel: "brochure" | "microsite" | "badge" | "emailer") => { setEditTemplateId(null); setEditMicrositeEventId(null); if (panel === "brochure") setShowBrochure(false); else if (panel === "microsite") setShowMicrosite(false); else if (panel === "badge") { setShowBadge(false); setDuplicateBadgeBatchId(null); } else if (panel === "emailer") setShowEmailer(false); fetchDesignList(); }; const handleDeleteConfirm = async () => { if (!deleteConfirm) return; const id = deleteConfirm._id; setDeleteConfirm(null); const res = await designTemplatesApi.remove(id); if (!res.error) fetchDesignList(); }; const handleBrochurePdf = async (t: DesignTemplate, mode: "preview" | "download") => { if (apiType !== "brochure") return; setBrochurePdfBusyId(t._id); setBrochurePdfError(null); try { const fresh = await designTemplatesApi.get(t._id); if (fresh.error || !fresh.data) throw new Error(fresh.error || "Failed to load brochure"); const ctxRes = await fetchBrochurePdfContext(fresh.data); if (!ctxRes.ok) throw new Error(ctxRes.error); const safeName = (fresh.data.name || "brochure").replace(/[^a-z0-9-_]+/gi, "-").replace(/^-|-$/g, "") || "brochure"; if (mode === "preview") { setBrochurePreviewCtx(ctxRes.ctx); } else { const data = buildBrochurePreviewData(ctxRes.ctx); const templateId = ctxRes.ctx.content.layoutId || "tpl1"; await downloadBrochurePdfOffscreen(templateId, data, `${safeName}.pdf`); } } catch (e) { setBrochurePdfError(e instanceof Error ? e.message : "Could not generate PDF"); } finally { setBrochurePdfBusyId(null); } }; const handleEmailerPreview = async (t: DesignTemplate) => { setEmailerPreviewBusyId(t._id); try { const fresh = await designTemplatesApi.get(t._id); if (fresh.error || !fresh.data) return; const c = fresh.data.content; let html = ""; if (c != null && typeof c === "object" && "html" in c && typeof (c as { html?: unknown }).html === "string") { html = String((c as { html: string }).html || ""); } if (!html.trim()) { html = '

No HTML saved for this template yet. Use Edit to build the emailer.

'; } setEmailerPreview({ html, title: fresh.data.name || "Email preview" }); } finally { setEmailerPreviewBusyId(null); } }; const createLabel = activeTab === "Brochure" ? "Create Brochure" : activeTab === "Badges" ? "Create Badge" : activeTab === "Emailers" ? "Create Emailer" : activeTab === "Microsite Templates" ? "Configure microsite" : "Create Template"; const showDesignHeaderActions = apiType === "microsite" ? canManageMicrosite : apiType === "badge" ? canManageBadgeBatch : isSuperAdmin; function badgeBatchEventLabel(b: BadgeBatchRecord): string { const e = b.event; if (e && typeof e === "object" && "name" in e) return (e as { name?: string }).name || "—"; return "—"; } const badgeListOrgId = portfolioListOrganizationId(user); return ( <>

Portfolio · Design

{activeTab}

{!loading && !error && apiType !== "microsite" && apiType !== "badge" && (

{templates.length} template{templates.length !== 1 ? "s" : ""}

)} {!loading && !error && apiType === "badge" && (

{badgeBatches.length} export job{badgeBatches.length !== 1 ? "s" : ""}

)} {!microLoading && !microError && apiType === "microsite" && (

{microEvents.length} event{microEvents.length !== 1 ? "s" : ""} {" · "} {orgMicrosites.length} microsite template{orgMicrosites.length !== 1 ? "s" : ""}

)}
{apiType === "badge" && badgeListOrgId && ( )} {showDesignHeaderActions && ( <> {isSuperAdmin && activeTab !== "Emailers" && activeTab !== "Badges" && ( )} {(apiType === "microsite" ? canManageMicrosite : apiType === "badge" ? canManageBadgeBatch : isSuperAdmin) && ( )} )}
{loading && apiType !== "microsite" && ( )} {microLoading && apiType === "microsite" && } {error && apiType !== "microsite" &&

{error}

} {brochurePdfError && apiType === "brochure" && (

{brochurePdfError}{" "}

)} {microError && apiType === "microsite" &&

{microError}

} {!loading && !error && apiType !== "microsite" && apiType !== "badge" && (
{templates.map(t => (
setHovered(t._id)} onMouseLeave={() => setHovered(null)} style={{ background: "var(--card)", border: "1px solid var(--border)", borderRadius: "var(--radius-card)", overflow: "hidden", cursor: "pointer", boxShadow: hovered === t._id ? "var(--elevation-sm)" : "none", transition: "box-shadow 0.15s" }} >
{templateIcons[activeTab]} {hovered === t._id && (
{isSuperAdmin && ( )} {apiType === "brochure" ? ( <> ) : apiType === "emailer" ? ( ) : null} {isSuperAdmin && ( )}
)}

{t.name}

{t.category || "—"} {formatLastEdited(t.updatedAt)}
))} {isSuperAdmin && (
{ (e.currentTarget as HTMLElement).style.borderColor = "var(--primary)"; (e.currentTarget as HTMLElement).style.background = "var(--accent)"; }} onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = "var(--border)"; (e.currentTarget as HTMLElement).style.background = "transparent"; }} >

{createLabel}

)}
)} {!loading && !error && apiType === "badge" && (
{badgeBatches.map((b) => (

{b.name}

{badgeBatchEventLabel(b)} Status: {b.status} {b.itemCount != null ? ` · ${b.itemCount} badge${b.itemCount !== 1 ? "s" : ""}` : ""} {b.lastError && b.status === "failed" && ( {b.lastError} )}
{canManageBadgeBatch && ( )} {b.status === "ready" && b.zipUrl ? ( Download zip ) : ( )}
{b.createdAt && ( Requested {formatLastEdited(b.createdAt)} )}
))} {canManageBadgeBatch && (
{ (e.currentTarget as HTMLElement).style.borderColor = "var(--primary)"; (e.currentTarget as HTMLElement).style.background = "var(--accent)"; }} onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = "var(--border)"; (e.currentTarget as HTMLElement).style.background = "transparent"; }} >

{createLabel}

)}
)} {!microLoading && !microError && apiType === "microsite" && (
{microEvents.map((ev) => { const forEvent = orgMicrosites.filter((m) => String(m.event) === String(ev._id)); const n = forEvent.length; const folders = forEvent.map((m) => (m.publishFolder || "").trim()).filter(Boolean); const folderLabel = folders.length === 0 ? "—" : folders.length <= 2 ? folders.join(", ") : `${folders.slice(0, 2).join(", ")} +${folders.length - 2}`; const liveCount = forEvent.filter((m) => m.enabled).length; const statusLabel = n === 0 ? "" : liveCount === n ? " · all live" : liveCount === 0 ? " · all draft" : ` · ${liveCount} live, ${n - liveCount} draft`; return (

{ev.name}

{n === 0 ? "0 microsite templates" : `${n} microsite template${n !== 1 ? "s" : ""} · slug(s): ${folderLabel}${statusLabel}`} {canManageMicrosite && ( )}
); })} {canManageMicrosite && (
{ (e.currentTarget as HTMLElement).style.borderColor = "var(--primary)"; (e.currentTarget as HTMLElement).style.background = "var(--accent)"; }} onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = "var(--border)"; (e.currentTarget as HTMLElement).style.background = "transparent"; }} >

{createLabel}

)}
)}
{showBrochure && handleClosePanel("brochure")} onSuccess={fetchDesignList} />} {brochurePreviewCtx && ( setBrochurePreviewCtx(null)} /> )} {emailerPreview && (
setEmailerPreview(null)} >
e.stopPropagation()} >
{emailerPreview.title}