diff --git a/src/app/components/dashboard/AiAvatarManagePanel.tsx b/src/app/components/dashboard/AiAvatarManagePanel.tsx new file mode 100644 index 00000000..dc316dd1 --- /dev/null +++ b/src/app/components/dashboard/AiAvatarManagePanel.tsx @@ -0,0 +1,661 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { Check, Copy, Link2, Trash2 } from "lucide-react"; +import { PanelShell, Label, Field, Grid, IS, SS, focusIn, focusOut, HelperText, RadioGroup } from "../panels/PanelShell"; +import { + aiAvatarApi, + uploadMediaFile, + type AiAvatarConfig, + type AiAvatarEventHeader, + type AiAvatarTemplate, +} from "../../../services/api"; +import { aiAvatarVisitorUrl } from "../../utils/aiAvatarUrls"; +import { + EVENT_HEADER_TOGGLE_LABELS, + eventHeaderFromApi, + eventHeaderToApi, + type EventHeaderToggleKey, +} from "../../utils/formEventHeaderDisplay"; + +type Tab = "settings" | "avatars" | "links"; + +const DEFAULT_HEADER = eventHeaderFromApi(null); + +export function AiAvatarManagePanel({ + eventId, + eventName, + canWrite, + onClose, + onChanged, +}: { + eventId: string; + eventName?: string; + canWrite: boolean; + onClose: () => void; + onChanged?: () => void; +}) { + const [tab, setTab] = useState("settings"); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [config, setConfig] = useState(null); + const [avatars, setAvatars] = useState([]); + const [copied, setCopied] = useState(false); + const [editingAvatarId, setEditingAvatarId] = useState(null); + const [uploadingImage, setUploadingImage] = useState(false); + const [avatarDraft, setAvatarDraft] = useState({ + name: "Avatar", + enabled: true, + imageUrl: "" as string, + }); + + const [draft, setDraft] = useState({ + accessMode: "open" as "open" | "login", + allowDownload: true, + eventHeader: DEFAULT_HEADER, + sharePlatforms: { + linkedin: false, + facebook: false, + instagram: false, + snapchat: false, + x: false, + whatsapp: false, + }, + }); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + try { + const res = await aiAvatarApi.get(eventId); + if (res.error || !res.data) { + setError(res.error || "Failed to load AI Avatar"); + setConfig(null); + setAvatars([]); + return; + } + const data = res.data; + setConfig(data); + setAvatars(data.avatars || []); + setDraft({ + accessMode: data.accessMode || "open", + allowDownload: data.allowDownload !== false, + eventHeader: eventHeaderFromApi(data.eventHeader as AiAvatarEventHeader | null), + sharePlatforms: { + linkedin: Boolean(data.sharePlatforms?.linkedin), + facebook: Boolean(data.sharePlatforms?.facebook), + instagram: Boolean(data.sharePlatforms?.instagram), + snapchat: Boolean(data.sharePlatforms?.snapchat), + x: Boolean(data.sharePlatforms?.x), + whatsapp: Boolean(data.sharePlatforms?.whatsapp), + }, + }); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to load"); + } finally { + setLoading(false); + } + }, [eventId]); + + useEffect(() => { + void load(); + }, [load]); + + const openAvatarEditor = (avatar: AiAvatarTemplate) => { + setEditingAvatarId(avatar._id); + setAvatarDraft({ + name: avatar.name || "Avatar", + enabled: avatar.enabled !== false, + imageUrl: avatar.imageUrl || "", + }); + }; + + const saveSettings = async () => { + if (!canWrite) return; + setSaving(true); + setError(null); + try { + const res = await aiAvatarApi.update(eventId, { + accessMode: draft.accessMode, + allowDownload: draft.allowDownload, + sharePlatforms: draft.sharePlatforms, + eventHeader: eventHeaderToApi(draft.eventHeader), + }); + if (res.error || !res.data) { + setError(res.error || "Failed to save"); + return; + } + setConfig(res.data); + setAvatars(res.data.avatars || []); + onChanged?.(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to save"); + } finally { + setSaving(false); + } + }; + + const saveAvatar = async () => { + if (!canWrite || !editingAvatarId) return; + setSaving(true); + setError(null); + try { + const res = await aiAvatarApi.updateAvatar(eventId, editingAvatarId, { + name: avatarDraft.name, + enabled: avatarDraft.enabled, + imageUrl: avatarDraft.imageUrl || null, + }); + if (res.error || !res.data) { + setError(res.error || "Failed to save avatar"); + return; + } + setAvatars((prev) => prev.map((a) => (a._id === res.data!._id ? res.data! : a))); + onChanged?.(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to save avatar"); + } finally { + setSaving(false); + } + }; + + const addAvatar = async () => { + if (!canWrite) return; + setSaving(true); + setError(null); + try { + const res = await aiAvatarApi.createAvatar(eventId, { + name: `Avatar ${avatars.length + 1}`, + }); + if (res.error || !res.data) { + setError(res.error || "Failed to create avatar"); + return; + } + setAvatars((prev) => [...prev, res.data!]); + openAvatarEditor(res.data); + onChanged?.(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to create avatar"); + } finally { + setSaving(false); + } + }; + + const deleteAvatar = async (avatarId: string) => { + if (!canWrite) return; + if (!window.confirm("Delete this avatar?")) return; + setError(null); + try { + await aiAvatarApi.deleteAvatar(eventId, avatarId); + setAvatars((prev) => prev.filter((a) => a._id !== avatarId)); + if (editingAvatarId === avatarId) setEditingAvatarId(null); + onChanged?.(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to delete"); + } + }; + + const uploadAvatarImage = async (file: File | null) => { + if (!file || !canWrite) return; + setUploadingImage(true); + setError(null); + try { + const r = await uploadMediaFile(file, { eventId, module: "ai-avatar/avatars" }); + const url = r.url || ""; + if (!url) throw new Error("Upload succeeded but no URL returned"); + setAvatarDraft((d) => ({ ...d, imageUrl: url })); + } catch (e) { + setError(e instanceof Error ? e.message : "Avatar upload failed"); + } finally { + setUploadingImage(false); + } + }; + + const copyLink = async () => { + if (!config?.uploadToken) return; + try { + await navigator.clipboard.writeText(aiAvatarVisitorUrl(config.uploadToken)); + setCopied(true); + window.setTimeout(() => setCopied(false), 1500); + } catch { + /* ignore */ + } + }; + + const tabBtn = (id: Tab, label: string) => ( + + ); + + const ctaLabel = + tab === "avatars" && editingAvatarId ? "Save avatar" : tab === "settings" ? "Save settings" : "Save"; + const showCta = tab === "settings" || (tab === "avatars" && Boolean(editingAvatarId)); + + return ( + { + if (tab === "avatars" && editingAvatarId) void saveAvatar(); + else void saveSettings(); + }} + panelWidth="min(720px,100vw)" + > +
+ {tabBtn("settings", "Settings")} + {tabBtn("avatars", "Avatars")} + {tabBtn("links", "Links")} +
+ +
+ {error ? ( +

{error}

+ ) : null} + {loading ? ( +

Loading…

+ ) : null} + + {!loading && tab === "links" && config ? ( +
+
+
+ + + Visitor upload URL + +
+

+ Guests pick an avatar, upload a selfie, then download or share +

+ + {config.uploadToken + ? aiAvatarVisitorUrl(config.uploadToken) + : "Enable AI Avatar to generate link"} + + +
+ {canWrite ? ( + + ) : null} +
+ ) : null} + + {!loading && tab === "settings" ? ( +
+ + + + + + + + + + + +
+ ) : null} + + {!loading && tab === "avatars" ? ( + editingAvatarId ? ( +
+ + + + + + + + + + +
+ ) : ( +
+
+

+ {avatars.length} avatar{avatars.length === 1 ? "" : "s"} +

+ {canWrite ? ( + + ) : null} +
+ + {!avatars.length ? ( +

+ No avatars yet. Add an image template for visitors to choose. +

+ ) : ( +
+ {avatars.map((avatar) => ( +
+
+ {avatar.imageUrl ? ( + {avatar.name} + ) : null} +
+
+

+ {avatar.name} +

+

+ {avatar.enabled !== false ? "Enabled" : "Disabled"} + {!avatar.imageUrl ? " · No image" : ""} +

+
+ + {canWrite ? ( + + ) : null} +
+ ))} +
+ )} +
+ ) + ) : null} +
+
+ ); +} diff --git a/src/app/components/dashboard/EventHomeModules.tsx b/src/app/components/dashboard/EventHomeModules.tsx index 09cb48f9..5da16faf 100644 --- a/src/app/components/dashboard/EventHomeModules.tsx +++ b/src/app/components/dashboard/EventHomeModules.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from "react"; -import { BarChart2, CalendarDays, Check, FileText, Globe, Image, Images, Link2, Mail, Plus, Share2, ShieldCheck, Store, Tag, Ticket, Trophy, Upload } from "lucide-react"; +import { BarChart2, CalendarDays, Check, FileText, Globe, Image, Images, Link2, Mail, Palette, Plus, Share2, ShieldCheck, Sparkles, Store, Tag, Ticket, Trophy, Upload } from "lucide-react"; import { useBreakpoint } from "../../hooks/useBreakpoint"; import { useAppNav } from "../../contexts/AppNavContext"; import { useAuth } from "../../contexts/AuthContext"; @@ -12,6 +12,7 @@ import { eventTicketsApi } from "../../../services/api/eventTickets"; import { campaignsApi } from "../../../services/api/campaigns"; import { countConfiguredRewardRules, RewardRulesSettingsPanel } from "../rewards/rewardRulesSettings"; import { ManageStaticMicrositePanel } from "../microsite-v2/ManageStaticMicrositePanel"; +import { VisitorThemeSettingsPanel } from "../panels/VisitorThemeSettingsPanel"; import { CreateFormPanel } from "../panels/CreateFormPanel"; import { CreateEmailCampaignPanel } from "../panels/CreateEmailCampaignPanel"; import { FormSubmissionsPanel } from "../panels/FormSubmissionsPanel"; @@ -20,9 +21,17 @@ import { CreateBadgePanel } from "../panels/CreateBadgePanel"; import { EventBadgesPanel } from "./EventBadgesPanel"; import { EventDocumentsPanel } from "./EventDocumentsPanel"; import { PhotoWallManagePanel } from "./PhotoWallManagePanel"; +import { SocialShareManagePanel } from "./SocialShareManagePanel"; +import { AiAvatarManagePanel } from "./AiAvatarManagePanel"; import { EventPhotoSearchPanel } from "../panels/EventPhotoSearchPanel"; import { photoWallApi } from "../../../services/api/photoWall"; +import { eventPhotoSearchApi } from "../../../services/api/eventPhotoSearch"; +import { socialShareApi } from "../../../services/api/socialShare"; +import { aiAvatarApi } from "../../../services/api/aiAvatar"; import { photoWallUploadUrl, photoWallPresentUrl } from "../../utils/photoWallUrls"; +import { photoSearchVisitorUrl } from "../../utils/photoSearchUrls"; +import { socialShareVisitorUrl } from "../../utils/socialShareUrls"; +import { aiAvatarVisitorUrl } from "../../utils/aiAvatarUrls"; import { EventEmailCampaignsPanel } from "./EventEmailCampaignsPanel"; import { EventExhibitorsPanel } from "./EventExhibitorsPanel"; import { AddExhibitorPanel } from "../panels/AddExhibitorPanel"; @@ -36,6 +45,7 @@ import { formsApi, staticMicrositeApi, staticMicrositePublicUrl, + visitorThemeApi, exhibitorsApi, type ApiExhibitor, } from "../../../services/api"; @@ -236,6 +246,10 @@ export function EventHomeModules() { const canReadDocuments = canModule("content_management", "read"); const canReadPhotoWall = canModule("photo_wall", "read"); const canWritePhotoWall = canModule("photo_wall", "write"); + const canReadSocialShare = canModule("social_share", "read"); + const canWriteSocialShare = canModule("social_share", "write"); + const canReadAiAvatar = canModule("ai_avatar", "read"); + const canWriteAiAvatar = canModule("ai_avatar", "write"); const canReadEmailCampaigns = canModule("email_outreach_campaigns", "read"); const canWriteEmailCampaigns = canModule("email_outreach_campaigns", "write"); const canReadExhibitors = canModule("exhibitors", "read"); @@ -275,15 +289,21 @@ export function EventHomeModules() { const [staticMicrositeOpen, setStaticMicrositeOpen] = useState(false); const [staticMicrositeMeta, setStaticMicrositeMeta] = useState(null); const [staticMicrositeLoading, setStaticMicrositeLoading] = useState(false); + const [visitorThemeOpen, setVisitorThemeOpen] = useState(false); + const [visitorThemeMeta, setVisitorThemeMeta] = useState(null); + const [visitorThemeLoading, setVisitorThemeLoading] = useState(false); const [agendaCounts, setAgendaCounts] = useState({ tracks: 0, sessions: 0 }); const [agendaLoading, setAgendaLoading] = useState(false); const [eventSettings, setEventSettings] = useState>({}); const [webCheckInEnabled, setWebCheckInEnabled] = useState(false); const [webCheckInLoading, setWebCheckInLoading] = useState(false); - const [pictureWallEnabled, setPictureWallEnabled] = useState(false); - const [pictureWallLoading, setPictureWallLoading] = useState(false); const [showDocumentsPanel, setShowDocumentsPanel] = useState(false); const [showEventPhotoSearchPanel, setShowEventPhotoSearchPanel] = useState(false); + const [photoSearchEnabled, setPhotoSearchEnabled] = useState(false); + const [photoSearchLoading, setPhotoSearchLoading] = useState(false); + const [photoSearchCount, setPhotoSearchCount] = useState(0); + const [photoSearchToken, setPhotoSearchToken] = useState(null); + const [copiedPhotoSearchLink, setCopiedPhotoSearchLink] = useState(false); const [showPhotoWallPanel, setShowPhotoWallPanel] = useState(false); const [photoWallEnabled, setPhotoWallEnabled] = useState(false); const [photoWallLoading, setPhotoWallLoading] = useState(false); @@ -291,6 +311,18 @@ export function EventHomeModules() { const [photoWallUploadToken, setPhotoWallUploadToken] = useState(null); const [photoWallPresentToken, setPhotoWallPresentToken] = useState(null); const [copiedPhotoWallLink, setCopiedPhotoWallLink] = useState<"upload" | "present" | null>(null); + const [showSocialSharePanel, setShowSocialSharePanel] = useState(false); + const [socialShareEnabled, setSocialShareEnabled] = useState(false); + const [socialShareLoading, setSocialShareLoading] = useState(false); + const [socialShareFrameCount, setSocialShareFrameCount] = useState(0); + const [socialShareToken, setSocialShareToken] = useState(null); + const [copiedSocialShareLink, setCopiedSocialShareLink] = useState(false); + const [showAiAvatarPanel, setShowAiAvatarPanel] = useState(false); + const [aiAvatarEnabled, setAiAvatarEnabled] = useState(false); + const [aiAvatarLoading, setAiAvatarLoading] = useState(false); + const [aiAvatarCount, setAiAvatarCount] = useState(0); + const [aiAvatarToken, setAiAvatarToken] = useState(null); + const [copiedAiAvatarLink, setCopiedAiAvatarLink] = useState(false); const [emailCampaignCount, setEmailCampaignCount] = useState(0); const [emailCampaignLoading, setEmailCampaignLoading] = useState(false); const [showCreateEmailCampaign, setShowCreateEmailCampaign] = useState(false); @@ -328,7 +360,6 @@ export function EventHomeModules() { setEventName(res.data?.name ?? null); setEventSettings((res.data?.settings as Record | undefined) ?? {}); setWebCheckInEnabled(Boolean((res.data?.settings as Record | undefined)?.webCheckInEnabled)); - setPictureWallEnabled(Boolean((res.data?.settings as Record | undefined)?.pictureWallEnabled)); }); return () => { cancelled = true; @@ -497,6 +528,30 @@ export function EventHomeModules() { void loadStaticMicrositeMeta(); }, [loadStaticMicrositeMeta]); + const loadVisitorThemeMeta = useCallback(async () => { + if (!selectedEventId) { + setVisitorThemeMeta(null); + setVisitorThemeLoading(false); + return; + } + setVisitorThemeLoading(true); + try { + const res = await visitorThemeApi.get(selectedEventId); + const mode = res.data?.theme; + if (mode === "gradient") setVisitorThemeMeta("Gradient"); + else if (mode === "background") setVisitorThemeMeta("Background"); + else setVisitorThemeMeta("Simple"); + } catch { + setVisitorThemeMeta("Simple"); + } finally { + setVisitorThemeLoading(false); + } + }, [selectedEventId]); + + useEffect(() => { + void loadVisitorThemeMeta(); + }, [loadVisitorThemeMeta]); + const loadAgendaMeta = useCallback(async () => { if (!selectedEventId) { setAgendaCounts({ tracks: 0, sessions: 0 }); @@ -597,6 +652,90 @@ export function EventHomeModules() { void loadPhotoWallMeta(); }, [loadPhotoWallMeta]); + const loadSocialShareMeta = useCallback(async () => { + if (!selectedEventId || !canReadSocialShare) { + setSocialShareEnabled(false); + setSocialShareFrameCount(0); + setSocialShareToken(null); + setSocialShareLoading(false); + return; + } + setSocialShareLoading(true); + try { + const res = await socialShareApi.get(selectedEventId); + const data = res.data; + setSocialShareEnabled(Boolean(data?.enabled)); + setSocialShareFrameCount(data?.frameCount ?? data?.frames?.length ?? 0); + setSocialShareToken(data?.uploadToken ?? null); + } catch { + setSocialShareEnabled(false); + setSocialShareFrameCount(0); + setSocialShareToken(null); + } finally { + setSocialShareLoading(false); + } + }, [selectedEventId, canReadSocialShare]); + + useEffect(() => { + void loadSocialShareMeta(); + }, [loadSocialShareMeta]); + + const loadAiAvatarMeta = useCallback(async () => { + if (!selectedEventId || !canReadAiAvatar) { + setAiAvatarEnabled(false); + setAiAvatarCount(0); + setAiAvatarToken(null); + setAiAvatarLoading(false); + return; + } + setAiAvatarLoading(true); + try { + const res = await aiAvatarApi.get(selectedEventId); + const data = res.data; + setAiAvatarEnabled(Boolean(data?.enabled)); + setAiAvatarCount(data?.avatarCount ?? data?.avatars?.length ?? 0); + setAiAvatarToken(data?.uploadToken ?? null); + } catch { + setAiAvatarEnabled(false); + setAiAvatarCount(0); + setAiAvatarToken(null); + } finally { + setAiAvatarLoading(false); + } + }, [selectedEventId, canReadAiAvatar]); + + useEffect(() => { + void loadAiAvatarMeta(); + }, [loadAiAvatarMeta]); + + const loadPhotoSearchMeta = useCallback(async () => { + if (!selectedEventId) { + setPhotoSearchEnabled(false); + setPhotoSearchCount(0); + setPhotoSearchToken(null); + setPhotoSearchLoading(false); + return; + } + setPhotoSearchLoading(true); + try { + const res = await eventPhotoSearchApi.get(selectedEventId); + const data = res.data; + setPhotoSearchEnabled(Boolean(data?.enabled)); + setPhotoSearchCount(data?.photoCount ?? 0); + setPhotoSearchToken(data?.searchToken ?? null); + } catch { + setPhotoSearchEnabled(false); + setPhotoSearchCount(0); + setPhotoSearchToken(null); + } finally { + setPhotoSearchLoading(false); + } + }, [selectedEventId]); + + useEffect(() => { + void loadPhotoSearchMeta(); + }, [loadPhotoSearchMeta]); + const handleBadgeSuccess = () => { setShowAddBadge(false); setDuplicateBadgeBatchId(null); @@ -664,6 +803,11 @@ export function EventHomeModules() { : rewardRulesCount === 0 ? "No rules configured" : `${rewardRulesCount} rule${rewardRulesCount === 1 ? "" : "s"} configured`; + const visitorThemeCardMeta = !selectedEventId + ? "Select an event to manage visitor theme" + : visitorThemeLoading + ? "Loading…" + : visitorThemeMeta || "Simple"; const micrositeMeta = !selectedEventId ? "Select an event to manage microsite" : staticMicrositeLoading @@ -691,13 +835,6 @@ export function EventHomeModules() { : webCheckInEnabled ? "Enabled" : "Disabled"; - const pictureWallMeta = !selectedEventId - ? "Select an event to configure picture wall" - : pictureWallLoading - ? "Saving…" - : pictureWallEnabled - ? "Enabled" - : "Disabled"; const photoWallMeta = !selectedEventId ? "Select an event to configure photo wall" : photoWallLoading @@ -705,6 +842,27 @@ export function EventHomeModules() { : photoWallEnabled ? `Enabled · ${photoWallCount} photo${photoWallCount === 1 ? "" : "s"}` : "Disabled"; + const socialShareMeta = !selectedEventId + ? "Select an event to configure social share" + : socialShareLoading + ? "Loading…" + : socialShareEnabled + ? `Enabled · ${socialShareFrameCount} frame${socialShareFrameCount === 1 ? "" : "s"}` + : "Disabled"; + const aiAvatarMeta = !selectedEventId + ? "Select an event to configure AI avatar" + : aiAvatarLoading + ? "Loading…" + : aiAvatarEnabled + ? `Enabled · ${aiAvatarCount} avatar${aiAvatarCount === 1 ? "" : "s"}` + : "Disabled"; + const photoSearchMeta = !selectedEventId + ? "Select an event to configure photo search" + : photoSearchLoading + ? "Loading…" + : photoSearchEnabled + ? `Enabled · ${photoSearchCount} photo${photoSearchCount === 1 ? "" : "s"}` + : "Disabled"; const handleWebCheckInToggle = async (enabled: boolean) => { if (!selectedEventId) return; @@ -721,21 +879,6 @@ export function EventHomeModules() { } }; - const handlePictureWallToggle = async (enabled: boolean) => { - if (!selectedEventId) return; - setPictureWallLoading(true); - try { - const nextSettings = { ...(eventSettings || {}), pictureWallEnabled: enabled }; - const res = await eventsApi.update(selectedEventId, { settings: nextSettings }); - if (!res.error) { - setEventSettings(nextSettings); - setPictureWallEnabled(enabled); - } - } finally { - setPictureWallLoading(false); - } - }; - const handlePhotoWallToggle = async (enabled: boolean) => { if (!selectedEventId || !canWritePhotoWall) return; setPhotoWallLoading(true); @@ -752,6 +895,85 @@ export function EventHomeModules() { } }; + const handleSocialShareToggle = async (enabled: boolean) => { + if (!selectedEventId || !canWriteSocialShare) return; + setSocialShareLoading(true); + try { + const res = await socialShareApi.update(selectedEventId, { enabled }); + if (!res.error && res.data) { + setSocialShareEnabled(Boolean(res.data.enabled)); + setSocialShareFrameCount(res.data.frameCount ?? res.data.frames?.length ?? 0); + setSocialShareToken(res.data.uploadToken ?? null); + } + } finally { + setSocialShareLoading(false); + } + }; + + const copySocialShareLink = async () => { + if (!socialShareToken) return; + try { + await navigator.clipboard.writeText(socialShareVisitorUrl(socialShareToken)); + setCopiedSocialShareLink(true); + window.setTimeout(() => setCopiedSocialShareLink(false), 1500); + } catch { + /* ignore */ + } + }; + + const handleAiAvatarToggle = async (enabled: boolean) => { + if (!selectedEventId || !canWriteAiAvatar) return; + setAiAvatarLoading(true); + try { + const res = await aiAvatarApi.update(selectedEventId, { enabled }); + if (!res.error && res.data) { + setAiAvatarEnabled(Boolean(res.data.enabled)); + setAiAvatarCount(res.data.avatarCount ?? res.data.avatars?.length ?? 0); + setAiAvatarToken(res.data.uploadToken ?? null); + } + } finally { + setAiAvatarLoading(false); + } + }; + + const copyAiAvatarLink = async () => { + if (!aiAvatarToken) return; + try { + await navigator.clipboard.writeText(aiAvatarVisitorUrl(aiAvatarToken)); + setCopiedAiAvatarLink(true); + window.setTimeout(() => setCopiedAiAvatarLink(false), 1500); + } catch { + /* ignore */ + } + }; + + const handlePhotoSearchToggle = async (enabled: boolean) => { + if (!selectedEventId) return; + setPhotoSearchLoading(true); + try { + const res = await eventPhotoSearchApi.update(selectedEventId, { enabled }); + if (!res.error && res.data) { + setPhotoSearchEnabled(Boolean(res.data.enabled)); + setPhotoSearchCount(res.data.photoCount ?? 0); + setPhotoSearchToken(res.data.searchToken ?? null); + } + } finally { + setPhotoSearchLoading(false); + } + }; + + const copyPhotoSearchLink = async () => { + if (!photoSearchToken) return; + const url = photoSearchVisitorUrl(photoSearchToken); + try { + await navigator.clipboard.writeText(url); + setCopiedPhotoSearchLink(true); + window.setTimeout(() => setCopiedPhotoSearchLink(false), 1500); + } catch { + /* ignore */ + } + }; + const copyPhotoWallLink = async (kind: "upload" | "present") => { const url = kind === "upload" && photoWallUploadToken @@ -1159,6 +1381,26 @@ export function EventHomeModules() { } /> ) : null} + } + title="Visitor Theme" + description="Colors and background for visitor event, session, quiz, and photo pages" + meta={visitorThemeCardMeta} + actions={ + + } + /> {canReadMicrosite ? ( } @@ -1267,117 +1509,205 @@ export function EventHomeModules() { ) : null} } - title="Picture Wall" - meta={pictureWallMeta} - description="Enable a public gallery where guests can add and view event photos" + title="Event Photo Search" + meta={photoSearchMeta} + description="Upload event photos and share visitor Find Photos" headerAction={ - pictureWallEnabled ? ( -
- - -
+ photoSearchEnabled && photoSearchToken ? ( + ) : null } actions={ - pictureWallEnabled ? ( + <> + {photoSearchEnabled ? ( + + ) : ( + + )} - ) : ( - - ) + } /> + {canReadSocialShare ? ( } - title="Event Photo Search" - description="Upload event photos and search by image" + icon={} + title="Social Share" + meta={socialShareMeta} + description="Multi-frame photo create, download, and share" headerAction={ - + socialShareEnabled && socialShareToken ? ( + + ) : null } actions={ - + <> + {socialShareEnabled ? ( + + ) : ( + + )} + + } /> + ) : null} + {canReadAiAvatar ? ( + } + title="AI Avatar" + meta={aiAvatarMeta} + description="AI head-swap avatars from visitor selfies" + headerAction={ + aiAvatarEnabled && aiAvatarToken ? ( + + ) : null + } + actions={ + <> + {aiAvatarEnabled ? ( + + ) : ( + + )} + + + } + /> + ) : null} {canReadPhotoWall ? ( } @@ -1636,6 +1966,14 @@ export function EventHomeModules() { /> ) : null} + {visitorThemeOpen && selectedEventId ? ( + setVisitorThemeOpen(false)} + onSaved={() => void loadVisitorThemeMeta()} + /> + ) : null} + {staticMicrositeOpen && selectedEventId ? ( setShowEventPhotoSearchPanel(false)} + onChanged={() => void loadPhotoSearchMeta()} /> ) : null} @@ -1671,6 +2010,26 @@ export function EventHomeModules() { /> ) : null} + {showSocialSharePanel && selectedEventId ? ( + setShowSocialSharePanel(false)} + onChanged={() => void loadSocialShareMeta()} + /> + ) : null} + + {showAiAvatarPanel && selectedEventId ? ( + setShowAiAvatarPanel(false)} + onChanged={() => void loadAiAvatarMeta()} + /> + ) : null} + {showEmailCampaignsPanel && selectedEventId ? (