2089 lines
74 KiB
TypeScript
2089 lines
74 KiB
TypeScript
import React, { useCallback, useEffect, useState } from "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";
|
|
import { usePermission } from "../../hooks/usePermission";
|
|
import { eventsApi } from "../../../services/api/events";
|
|
import { eventStaffApi } from "../../../services/api/eventStaff";
|
|
import { quizPollsApi } from "../../../services/api/quizPolls";
|
|
import { badgeBatchesApi } from "../../../services/api/badgeBatches";
|
|
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";
|
|
import { ShareFormPanel } from "../forms/ShareFormPanel";
|
|
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";
|
|
import { AddQuizPollModal, buildQuizPollSettingsPayload } from "../quiz/AddQuizPollModal";
|
|
import type { QuizPollModalForm } from "../quiz/AddQuizPollModal";
|
|
import type { Question } from "../quiz/AddQuestionModal";
|
|
import { modalQuestionToApi } from "../quiz/quizQuestionMappers";
|
|
import {
|
|
sessionsApi,
|
|
tracksApi,
|
|
formsApi,
|
|
staticMicrositeApi,
|
|
staticMicrositePublicUrl,
|
|
visitorThemeApi,
|
|
exhibitorsApi,
|
|
type ApiExhibitor,
|
|
} from "../../../services/api";
|
|
import type { FormWithSubmissionCount } from "../../../services/api/types";
|
|
import { publicFormPageUrl } from "../../utils/publicFormUrl";
|
|
|
|
function ModuleCard({
|
|
icon,
|
|
title,
|
|
description,
|
|
meta,
|
|
headerAction,
|
|
actions,
|
|
}: {
|
|
icon: React.ReactNode;
|
|
title: string;
|
|
description?: string;
|
|
meta?: string;
|
|
headerAction?: React.ReactNode;
|
|
actions: React.ReactNode;
|
|
}) {
|
|
const bp = useBreakpoint();
|
|
const isMobile = bp === "mobile";
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
background: "var(--card)",
|
|
border: "1px solid var(--border)",
|
|
borderRadius: "var(--radius-card)",
|
|
padding: isMobile ? "14px" : "16px",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "12px",
|
|
minHeight: "auto",
|
|
}}
|
|
>
|
|
<div style={{ display: "flex", alignItems: "flex-start", gap: "10px", flex: 1 }}>
|
|
<div
|
|
style={{
|
|
width: "36px",
|
|
height: "36px",
|
|
borderRadius: "var(--radius-button)",
|
|
background: "color-mix(in srgb, var(--primary) 12%, transparent)",
|
|
color: "var(--primary)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{icon}
|
|
</div>
|
|
<div style={{ minWidth: 0, flex: 1 }}>
|
|
<div style={{ display: "flex", alignItems: "flex-start", gap: "6px" }}>
|
|
<p
|
|
style={{
|
|
fontFamily: "var(--font-inter)",
|
|
fontSize: "var(--text-base)",
|
|
fontWeight: "var(--font-weight-medium)",
|
|
color: "var(--foreground)",
|
|
margin: 0,
|
|
lineHeight: 1.3,
|
|
flex: 1,
|
|
minWidth: 0,
|
|
}}
|
|
>
|
|
{title}
|
|
</p>
|
|
{headerAction}
|
|
</div>
|
|
{description ? (
|
|
<p
|
|
style={{
|
|
fontSize: "var(--text-xs)",
|
|
fontFamily: "var(--font-noto-sans)",
|
|
color: "var(--muted)",
|
|
margin: "4px 0 0",
|
|
lineHeight: 1.4,
|
|
}}
|
|
>
|
|
{description}
|
|
</p>
|
|
) : null}
|
|
{meta ? (
|
|
<p
|
|
style={{
|
|
fontSize: "var(--text-xs)",
|
|
fontFamily: "var(--font-noto-sans)",
|
|
color: "var(--muted)",
|
|
margin: "6px 0 0",
|
|
}}
|
|
>
|
|
{meta}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
<div style={{ display: "flex", gap: "6px", flexWrap: "nowrap" }}>{actions}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function moduleButtonStyle(primary = false): React.CSSProperties {
|
|
return {
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: "6px",
|
|
flex: "1 1 0",
|
|
minWidth: 0,
|
|
padding: "6px 8px",
|
|
borderRadius: "var(--radius-button)",
|
|
border: primary ? "1px solid var(--primary)" : "1px solid var(--border)",
|
|
background: "var(--card)",
|
|
color: primary ? "var(--primary)" : "var(--card-foreground)",
|
|
fontSize: "var(--text-xs)",
|
|
fontFamily: "var(--font-noto-sans)",
|
|
fontWeight: "var(--font-weight-medium)",
|
|
cursor: "pointer",
|
|
};
|
|
}
|
|
|
|
function formDisplayType(apiType: string): string {
|
|
if (apiType === "feedback") return "Feedback";
|
|
if (apiType === "registration") return "Registration";
|
|
if (apiType === "web-check-in") return "Web check-in";
|
|
if (apiType === "other") return "Other";
|
|
if (apiType === "survey" || apiType === "custom") return "Other";
|
|
return apiType.charAt(0).toUpperCase() + apiType.slice(1);
|
|
}
|
|
|
|
const NEW_FORM_OPTIONS: { value: "registration" | "feedback" | "web-check-in" | "other"; label: string }[] = [
|
|
{ value: "registration", label: "Registration" },
|
|
{ value: "feedback", label: "Feedback" },
|
|
{ value: "web-check-in", label: "Web check-in" },
|
|
{ value: "other", label: "Other" },
|
|
];
|
|
|
|
function formPanelMode(apiType: string): "feedback" | "registration" | "other" | "web-check-in" {
|
|
if (apiType === "feedback") return "feedback";
|
|
if (apiType === "registration") return "registration";
|
|
if (apiType === "web-check-in") return "web-check-in";
|
|
return "other";
|
|
}
|
|
|
|
function formMetaLine(form: FormWithSubmissionCount): string {
|
|
const count = form.submissionCount ?? 0;
|
|
return `${formDisplayType(form.type)} · ${count.toLocaleString()} response${count === 1 ? "" : "s"}`;
|
|
}
|
|
|
|
function formLinkCopyButtonStyle(copied: boolean): React.CSSProperties {
|
|
return {
|
|
width: "26px",
|
|
height: "26px",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
borderRadius: "var(--radius-button)",
|
|
border: "1px solid var(--border)",
|
|
background: copied ? "#dcfce7" : "var(--card)",
|
|
color: copied ? "#15803d" : "var(--muted)",
|
|
cursor: "pointer",
|
|
flexShrink: 0,
|
|
padding: 0,
|
|
};
|
|
}
|
|
|
|
function formHeaderIconButtonStyle(): React.CSSProperties {
|
|
return {
|
|
...formLinkCopyButtonStyle(false),
|
|
color: "var(--muted)",
|
|
};
|
|
}
|
|
|
|
export function EventHomeModules() {
|
|
const { selectedEventId, setActiveNav, setPermissionsOpenAddOnce, setOpenAgendaSessionPanelOnce } = useAppNav();
|
|
const { user } = useAuth();
|
|
const { canModule } = usePermission();
|
|
const isSuperAdmin = user?.role === "super_admin";
|
|
const canReadForms = canModule("forms", "read");
|
|
const canWriteForms = canModule("forms", "write");
|
|
const canReadSessions = canModule("sessions", "read");
|
|
const canWriteSessions = canModule("sessions", "write");
|
|
const canReadUserMgmt = canModule("user_management", "read");
|
|
const canWriteUserMgmt = canModule("user_management", "write");
|
|
const canReadActivities =
|
|
canModule("live_polls", "read") || canModule("multiple_quiz_modes", "read");
|
|
const canWriteActivities =
|
|
canModule("live_polls", "write") || canModule("multiple_quiz_modes", "write");
|
|
const canReadBadges = canModule("custom_badge_and_print_file_creation", "read");
|
|
const canWriteBadges = canModule("custom_badge_and_print_file_creation", "write");
|
|
const canReadTicketing = canModule("ticketing_free_and_paid", "read");
|
|
const canReadMicrosite = canModule("event_website", "read");
|
|
const canReadRewards = canModule("gamification", "read");
|
|
const canReadWebCheckIn = canModule("web_check_in", "read");
|
|
const canWriteWebCheckIn = canModule("web_check_in", "write");
|
|
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");
|
|
const canWriteExhibitors = canModule("exhibitors", "write");
|
|
const bp = useBreakpoint();
|
|
const isMobile = bp === "mobile";
|
|
const isTablet = bp === "tablet";
|
|
const gridTemplateColumns = isMobile
|
|
? "1fr"
|
|
: isTablet
|
|
? "repeat(2, minmax(0, 1fr))"
|
|
: "repeat(auto-fill, minmax(240px, 1fr))";
|
|
const [eventForms, setEventForms] = useState<FormWithSubmissionCount[]>([]);
|
|
const [formEditTarget, setFormEditTarget] = useState<FormWithSubmissionCount | null>(null);
|
|
const [formResponsesTarget, setFormResponsesTarget] = useState<FormWithSubmissionCount | null>(null);
|
|
const [formShareTarget, setFormShareTarget] = useState<FormWithSubmissionCount | null>(null);
|
|
const [newFormMode, setNewFormMode] = useState<"registration" | "feedback" | "other" | "web-check-in" | null>(null);
|
|
const [newFormType, setNewFormType] = useState<"registration" | "feedback" | "web-check-in" | "other">("registration");
|
|
const [copiedFormLinkId, setCopiedFormLinkId] = useState<string | null>(null);
|
|
const [pollQuizCounts, setPollQuizCounts] = useState({ polls: 0, quizzes: 0 });
|
|
const [pollQuizLoading, setPollQuizLoading] = useState(false);
|
|
const [showAddPoll, setShowAddPoll] = useState(false);
|
|
const [showAddQuiz, setShowAddQuiz] = useState(false);
|
|
const [staffCounts, setStaffCounts] = useState({ members: 0, invites: 0 });
|
|
const [staffLoading, setStaffLoading] = useState(false);
|
|
const [badgeCount, setBadgeCount] = useState(0);
|
|
const [badgeLoading, setBadgeLoading] = useState(false);
|
|
const [eventName, setEventName] = useState<string | null>(null);
|
|
const [badgesOpen, setBadgesOpen] = useState(false);
|
|
const [showAddBadge, setShowAddBadge] = useState(false);
|
|
const [duplicateBadgeBatchId, setDuplicateBadgeBatchId] = useState<string | null>(null);
|
|
const [ticketCount, setTicketCount] = useState(0);
|
|
const [ticketLoading, setTicketLoading] = useState(false);
|
|
const [rewardRulesCount, setRewardRulesCount] = useState(0);
|
|
const [rewardLoading, setRewardLoading] = useState(false);
|
|
const [rewardRulesOpen, setRewardRulesOpen] = useState(false);
|
|
const [staticMicrositeOpen, setStaticMicrositeOpen] = useState(false);
|
|
const [staticMicrositeMeta, setStaticMicrositeMeta] = useState<string | null>(null);
|
|
const [staticMicrositeLoading, setStaticMicrositeLoading] = useState(false);
|
|
const [visitorThemeOpen, setVisitorThemeOpen] = useState(false);
|
|
const [visitorThemeMeta, setVisitorThemeMeta] = useState<string | null>(null);
|
|
const [visitorThemeLoading, setVisitorThemeLoading] = useState(false);
|
|
const [agendaCounts, setAgendaCounts] = useState({ tracks: 0, sessions: 0 });
|
|
const [agendaLoading, setAgendaLoading] = useState(false);
|
|
const [eventSettings, setEventSettings] = useState<Record<string, unknown>>({});
|
|
const [webCheckInEnabled, setWebCheckInEnabled] = useState(false);
|
|
const [webCheckInLoading, setWebCheckInLoading] = 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<string | null>(null);
|
|
const [copiedPhotoSearchLink, setCopiedPhotoSearchLink] = useState(false);
|
|
const [showPhotoWallPanel, setShowPhotoWallPanel] = useState(false);
|
|
const [photoWallEnabled, setPhotoWallEnabled] = useState(false);
|
|
const [photoWallLoading, setPhotoWallLoading] = useState(false);
|
|
const [photoWallCount, setPhotoWallCount] = useState(0);
|
|
const [photoWallUploadToken, setPhotoWallUploadToken] = useState<string | null>(null);
|
|
const [photoWallPresentToken, setPhotoWallPresentToken] = useState<string | null>(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<string | null>(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<string | null>(null);
|
|
const [copiedAiAvatarLink, setCopiedAiAvatarLink] = useState(false);
|
|
const [emailCampaignCount, setEmailCampaignCount] = useState(0);
|
|
const [emailCampaignLoading, setEmailCampaignLoading] = useState(false);
|
|
const [showCreateEmailCampaign, setShowCreateEmailCampaign] = useState(false);
|
|
const [showEmailCampaignsPanel, setShowEmailCampaignsPanel] = useState(false);
|
|
const [exhibitorCount, setExhibitorCount] = useState(0);
|
|
const [exhibitorLoading, setExhibitorLoading] = useState(false);
|
|
const [showExhibitorsPanel, setShowExhibitorsPanel] = useState(false);
|
|
const [showAddExhibitor, setShowAddExhibitor] = useState(false);
|
|
const [editingExhibitor, setEditingExhibitor] = useState<ApiExhibitor | null>(null);
|
|
const actionsDisabled = !selectedEventId;
|
|
const photoSearchHeaderButtonStyle: React.CSSProperties = {
|
|
width: "26px",
|
|
height: "26px",
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
borderRadius: "var(--radius-button)",
|
|
border: "1px solid var(--border)",
|
|
background: "var(--card)",
|
|
color: "var(--muted)",
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
flexShrink: 0,
|
|
padding: 0,
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!selectedEventId) {
|
|
setEventName(null);
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
void eventsApi.get(selectedEventId).then((res) => {
|
|
if (cancelled) return;
|
|
setEventName(res.data?.name ?? null);
|
|
setEventSettings((res.data?.settings as Record<string, unknown> | undefined) ?? {});
|
|
setWebCheckInEnabled(Boolean((res.data?.settings as Record<string, unknown> | undefined)?.webCheckInEnabled));
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [selectedEventId]);
|
|
|
|
const loadEventForms = useCallback(async () => {
|
|
if (!selectedEventId) {
|
|
setEventForms([]);
|
|
return;
|
|
}
|
|
try {
|
|
const forms = await formsApi.listForEvent(selectedEventId);
|
|
const sorted = [...forms].sort((a, b) =>
|
|
(a.name || "").localeCompare(b.name || "", undefined, { sensitivity: "base" }),
|
|
);
|
|
setEventForms(sorted);
|
|
} catch {
|
|
setEventForms([]);
|
|
}
|
|
}, [selectedEventId]);
|
|
|
|
useEffect(() => {
|
|
void loadEventForms();
|
|
}, [loadEventForms]);
|
|
|
|
const loadPollQuizCounts = useCallback(async () => {
|
|
if (!selectedEventId) {
|
|
setPollQuizCounts({ polls: 0, quizzes: 0 });
|
|
setPollQuizLoading(false);
|
|
return;
|
|
}
|
|
setPollQuizLoading(true);
|
|
try {
|
|
const res = await quizPollsApi.listByEvent(selectedEventId);
|
|
const items = res.data?.quizPolls ?? [];
|
|
setPollQuizCounts({
|
|
polls: items.filter((q) => q.type === "poll").length,
|
|
quizzes: items.filter((q) => q.type === "quiz").length,
|
|
});
|
|
} catch {
|
|
setPollQuizCounts({ polls: 0, quizzes: 0 });
|
|
} finally {
|
|
setPollQuizLoading(false);
|
|
}
|
|
}, [selectedEventId]);
|
|
|
|
useEffect(() => {
|
|
void loadPollQuizCounts();
|
|
}, [loadPollQuizCounts]);
|
|
|
|
const loadStaffCounts = useCallback(async () => {
|
|
if (!selectedEventId) {
|
|
setStaffCounts({ members: 0, invites: 0 });
|
|
setStaffLoading(false);
|
|
return;
|
|
}
|
|
setStaffLoading(true);
|
|
try {
|
|
const res = await eventStaffApi.listStaff(selectedEventId);
|
|
if (res.data) {
|
|
setStaffCounts({
|
|
members: res.data.members?.length ?? 0,
|
|
invites: res.data.invites?.length ?? 0,
|
|
});
|
|
} else {
|
|
setStaffCounts({ members: 0, invites: 0 });
|
|
}
|
|
} catch {
|
|
setStaffCounts({ members: 0, invites: 0 });
|
|
} finally {
|
|
setStaffLoading(false);
|
|
}
|
|
}, [selectedEventId]);
|
|
|
|
useEffect(() => {
|
|
void loadStaffCounts();
|
|
}, [loadStaffCounts]);
|
|
|
|
const loadBadgeCount = useCallback(async () => {
|
|
if (!selectedEventId) {
|
|
setBadgeCount(0);
|
|
setBadgeLoading(false);
|
|
return;
|
|
}
|
|
setBadgeLoading(true);
|
|
try {
|
|
const res = await badgeBatchesApi.list({ event: selectedEventId, limit: 100 });
|
|
setBadgeCount(res.data?.batches?.length ?? 0);
|
|
} catch {
|
|
setBadgeCount(0);
|
|
} finally {
|
|
setBadgeLoading(false);
|
|
}
|
|
}, [selectedEventId]);
|
|
|
|
useEffect(() => {
|
|
void loadBadgeCount();
|
|
}, [loadBadgeCount]);
|
|
|
|
const loadTicketCount = useCallback(async () => {
|
|
if (!selectedEventId) {
|
|
setTicketCount(0);
|
|
setTicketLoading(false);
|
|
return;
|
|
}
|
|
setTicketLoading(true);
|
|
try {
|
|
const tickets = await eventTicketsApi.list(selectedEventId);
|
|
setTicketCount(tickets.length);
|
|
} catch {
|
|
setTicketCount(0);
|
|
} finally {
|
|
setTicketLoading(false);
|
|
}
|
|
}, [selectedEventId]);
|
|
|
|
useEffect(() => {
|
|
void loadTicketCount();
|
|
}, [loadTicketCount]);
|
|
|
|
const loadRewardRulesMeta = useCallback(async () => {
|
|
if (!selectedEventId) {
|
|
setRewardRulesCount(0);
|
|
setRewardLoading(false);
|
|
return;
|
|
}
|
|
setRewardLoading(true);
|
|
try {
|
|
const res = await eventsApi.get(selectedEventId);
|
|
setRewardRulesCount(countConfiguredRewardRules(res.data?.rewardRules));
|
|
} catch {
|
|
setRewardRulesCount(0);
|
|
} finally {
|
|
setRewardLoading(false);
|
|
}
|
|
}, [selectedEventId]);
|
|
|
|
useEffect(() => {
|
|
void loadRewardRulesMeta();
|
|
}, [loadRewardRulesMeta]);
|
|
|
|
const loadStaticMicrositeMeta = useCallback(async () => {
|
|
if (!selectedEventId) {
|
|
setStaticMicrositeMeta(null);
|
|
setStaticMicrositeLoading(false);
|
|
return;
|
|
}
|
|
setStaticMicrositeLoading(true);
|
|
try {
|
|
const res = await staticMicrositeApi.getForEvent(selectedEventId);
|
|
if (res.data?.subdomain) {
|
|
const status = res.data.status === "published" ? "Published" : "Draft";
|
|
setStaticMicrositeMeta(`${staticMicrositePublicUrl(res.data.subdomain)} · ${status}`);
|
|
} else {
|
|
setStaticMicrositeMeta("Not configured");
|
|
}
|
|
} catch {
|
|
setStaticMicrositeMeta("Not configured");
|
|
} finally {
|
|
setStaticMicrositeLoading(false);
|
|
}
|
|
}, [selectedEventId]);
|
|
|
|
useEffect(() => {
|
|
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 });
|
|
setAgendaLoading(false);
|
|
return;
|
|
}
|
|
setAgendaLoading(true);
|
|
try {
|
|
const [trRes, sesRes] = await Promise.all([
|
|
tracksApi.list(selectedEventId),
|
|
sessionsApi.list(selectedEventId),
|
|
]);
|
|
const tracks = (trRes.data as { tracks?: unknown[] } | undefined)?.tracks ?? [];
|
|
const sessions = (sesRes.data as { sessions?: unknown[] } | undefined)?.sessions ?? [];
|
|
setAgendaCounts({ tracks: tracks.length, sessions: sessions.length });
|
|
} catch {
|
|
setAgendaCounts({ tracks: 0, sessions: 0 });
|
|
} finally {
|
|
setAgendaLoading(false);
|
|
}
|
|
}, [selectedEventId]);
|
|
|
|
useEffect(() => {
|
|
void loadAgendaMeta();
|
|
}, [loadAgendaMeta]);
|
|
|
|
const loadEmailCampaignMeta = useCallback(async () => {
|
|
if (!selectedEventId) {
|
|
setEmailCampaignCount(0);
|
|
setEmailCampaignLoading(false);
|
|
return;
|
|
}
|
|
setEmailCampaignLoading(true);
|
|
try {
|
|
const res = await campaignsApi.listForEvent(selectedEventId);
|
|
const campaigns = res.data?.campaigns ?? [];
|
|
setEmailCampaignCount(campaigns.filter((c) => c.channel === "email").length);
|
|
} catch {
|
|
setEmailCampaignCount(0);
|
|
} finally {
|
|
setEmailCampaignLoading(false);
|
|
}
|
|
}, [selectedEventId]);
|
|
|
|
useEffect(() => {
|
|
void loadEmailCampaignMeta();
|
|
}, [loadEmailCampaignMeta]);
|
|
|
|
const loadExhibitorMeta = useCallback(async () => {
|
|
if (!selectedEventId) {
|
|
setExhibitorCount(0);
|
|
setExhibitorLoading(false);
|
|
return;
|
|
}
|
|
setExhibitorLoading(true);
|
|
try {
|
|
const res = await exhibitorsApi.list(selectedEventId);
|
|
setExhibitorCount(res.data?.total ?? res.data?.exhibitors?.length ?? 0);
|
|
} catch {
|
|
setExhibitorCount(0);
|
|
} finally {
|
|
setExhibitorLoading(false);
|
|
}
|
|
}, [selectedEventId]);
|
|
|
|
useEffect(() => {
|
|
void loadExhibitorMeta();
|
|
}, [loadExhibitorMeta]);
|
|
|
|
const loadPhotoWallMeta = useCallback(async () => {
|
|
if (!selectedEventId || !canReadPhotoWall) {
|
|
setPhotoWallEnabled(false);
|
|
setPhotoWallCount(0);
|
|
setPhotoWallUploadToken(null);
|
|
setPhotoWallPresentToken(null);
|
|
setPhotoWallLoading(false);
|
|
return;
|
|
}
|
|
setPhotoWallLoading(true);
|
|
try {
|
|
const res = await photoWallApi.get(selectedEventId);
|
|
const data = res.data;
|
|
setPhotoWallEnabled(Boolean(data?.enabled));
|
|
setPhotoWallCount(data?.photoCounts?.total ?? 0);
|
|
setPhotoWallUploadToken(data?.uploadToken ?? null);
|
|
setPhotoWallPresentToken(data?.presentToken ?? null);
|
|
} catch {
|
|
setPhotoWallEnabled(false);
|
|
setPhotoWallCount(0);
|
|
setPhotoWallUploadToken(null);
|
|
setPhotoWallPresentToken(null);
|
|
} finally {
|
|
setPhotoWallLoading(false);
|
|
}
|
|
}, [selectedEventId, canReadPhotoWall]);
|
|
|
|
useEffect(() => {
|
|
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);
|
|
void loadBadgeCount();
|
|
};
|
|
|
|
const openAddBadge = () => {
|
|
setDuplicateBadgeBatchId(null);
|
|
setShowAddBadge(true);
|
|
};
|
|
|
|
const openDuplicateBadge = (batchId: string) => {
|
|
setBadgesOpen(false);
|
|
setDuplicateBadgeBatchId(batchId);
|
|
setShowAddBadge(true);
|
|
};
|
|
|
|
const handleFormEditSuccess = () => {
|
|
setFormEditTarget(null);
|
|
void loadEventForms();
|
|
};
|
|
|
|
const handleNewFormSuccess = () => {
|
|
setNewFormMode(null);
|
|
void loadEventForms();
|
|
};
|
|
|
|
const copyFormPublicLink = useCallback(
|
|
(form: FormWithSubmissionCount) => {
|
|
const url = publicFormPageUrl(form, selectedEventId);
|
|
void navigator.clipboard.writeText(url).then(() => {
|
|
setCopiedFormLinkId(form._id);
|
|
window.setTimeout(() => {
|
|
setCopiedFormLinkId((prev) => (prev === form._id ? null : prev));
|
|
}, 4000);
|
|
});
|
|
},
|
|
[selectedEventId],
|
|
);
|
|
|
|
const pollQuizMeta = !selectedEventId
|
|
? "Select an event to create polls and quizzes"
|
|
: pollQuizLoading
|
|
? "Loading…"
|
|
: `${pollQuizCounts.polls} poll${pollQuizCounts.polls === 1 ? "" : "s"} · ${pollQuizCounts.quizzes} quiz${pollQuizCounts.quizzes === 1 ? "" : "zes"}`;
|
|
const permissionsMeta = !selectedEventId
|
|
? "Select an event to manage team access"
|
|
: staffLoading
|
|
? "Loading…"
|
|
: `${staffCounts.members} member${staffCounts.members === 1 ? "" : "s"} · ${staffCounts.invites} pending invite${staffCounts.invites === 1 ? "" : "s"}`;
|
|
const badgeMeta = !selectedEventId
|
|
? "Select an event to manage badges"
|
|
: badgeLoading
|
|
? "Loading…"
|
|
: `${badgeCount} badge${badgeCount === 1 ? "" : "s"}`;
|
|
const ticketMeta = !selectedEventId
|
|
? "Select an event to configure tickets"
|
|
: ticketLoading
|
|
? "Loading…"
|
|
: `${ticketCount} ticket type${ticketCount === 1 ? "" : "s"}`;
|
|
const rewardMeta = !selectedEventId
|
|
? "Select an event to configure reward rules"
|
|
: rewardLoading
|
|
? "Loading…"
|
|
: 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
|
|
? "Loading…"
|
|
: staticMicrositeMeta || "Not configured";
|
|
const agendaMeta = !selectedEventId
|
|
? "Select an event to manage the agenda"
|
|
: agendaLoading
|
|
? "Loading…"
|
|
: `${agendaCounts.tracks} track${agendaCounts.tracks === 1 ? "" : "s"} · ${agendaCounts.sessions} session${agendaCounts.sessions === 1 ? "" : "s"}`;
|
|
const emailCampaignMeta = !selectedEventId
|
|
? "Select an event to manage email campaigns"
|
|
: emailCampaignLoading
|
|
? "Loading…"
|
|
: `${emailCampaignCount} email campaign${emailCampaignCount === 1 ? "" : "s"}`;
|
|
const exhibitorMeta = !selectedEventId
|
|
? "Select an event to manage exhibitors"
|
|
: exhibitorLoading
|
|
? "Loading…"
|
|
: `${exhibitorCount} exhibitor${exhibitorCount === 1 ? "" : "s"}`;
|
|
const webCheckInMeta = !selectedEventId
|
|
? "Select an event to configure web check-in"
|
|
: webCheckInLoading
|
|
? "Saving…"
|
|
: webCheckInEnabled
|
|
? "Enabled"
|
|
: "Disabled";
|
|
const photoWallMeta = !selectedEventId
|
|
? "Select an event to configure photo wall"
|
|
: photoWallLoading
|
|
? "Loading…"
|
|
: 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;
|
|
setWebCheckInLoading(true);
|
|
try {
|
|
const nextSettings = { ...(eventSettings || {}), webCheckInEnabled: enabled };
|
|
const res = await eventsApi.update(selectedEventId, { settings: nextSettings });
|
|
if (!res.error) {
|
|
setEventSettings(nextSettings);
|
|
setWebCheckInEnabled(enabled);
|
|
}
|
|
} finally {
|
|
setWebCheckInLoading(false);
|
|
}
|
|
};
|
|
|
|
const handlePhotoWallToggle = async (enabled: boolean) => {
|
|
if (!selectedEventId || !canWritePhotoWall) return;
|
|
setPhotoWallLoading(true);
|
|
try {
|
|
const res = await photoWallApi.update(selectedEventId, { enabled });
|
|
if (!res.error && res.data) {
|
|
setPhotoWallEnabled(Boolean(res.data.enabled));
|
|
setPhotoWallCount(res.data.photoCounts?.total ?? 0);
|
|
setPhotoWallUploadToken(res.data.uploadToken ?? null);
|
|
setPhotoWallPresentToken(res.data.presentToken ?? null);
|
|
}
|
|
} finally {
|
|
setPhotoWallLoading(false);
|
|
}
|
|
};
|
|
|
|
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
|
|
? photoWallUploadUrl(photoWallUploadToken)
|
|
: kind === "present" && photoWallPresentToken
|
|
? photoWallPresentUrl(photoWallPresentToken)
|
|
: "";
|
|
if (!url) return;
|
|
try {
|
|
await navigator.clipboard.writeText(url);
|
|
setCopiedPhotoWallLink(kind);
|
|
window.setTimeout(() => setCopiedPhotoWallLink(null), 1500);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
};
|
|
|
|
const handleSavePollQuiz = async (
|
|
kind: "poll" | "quiz",
|
|
data: { form: QuizPollModalForm; questions: Question[] },
|
|
) => {
|
|
if (!selectedEventId) return;
|
|
const sesRes = await sessionsApi.list(selectedEventId);
|
|
const firstSession = sesRes.data?.sessions?.[0];
|
|
const payload = {
|
|
...(firstSession?._id ? { session: firstSession._id } : {}),
|
|
title: data.form.title,
|
|
type: kind,
|
|
description: data.form.description || undefined,
|
|
questions: data.questions.map((q) => modalQuestionToApi(q, kind)),
|
|
settings: buildQuizPollSettingsPayload(data.form, kind),
|
|
};
|
|
const res = await quizPollsApi.create(selectedEventId, payload);
|
|
if (!res.error) {
|
|
if (kind === "poll") setShowAddPoll(false);
|
|
else setShowAddQuiz(false);
|
|
void loadPollQuizCounts();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<section
|
|
aria-label="Event modules"
|
|
style={{
|
|
display: "grid",
|
|
gridTemplateColumns,
|
|
gap: "12px",
|
|
}}
|
|
>
|
|
{canReadSessions ? (
|
|
<ModuleCard
|
|
icon={<CalendarDays size={18} />}
|
|
title="Agenda"
|
|
meta={agendaMeta}
|
|
actions={
|
|
<>
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setActiveNav("agenda")}
|
|
onMouseEnter={(e) => {
|
|
if (actionsDisabled) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (actionsDisabled) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
|
|
}}
|
|
>
|
|
View Agenda
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || !canWriteSessions}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled || !canWriteSessions ? 0.55 : 1,
|
|
cursor: actionsDisabled || !canWriteSessions ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => {
|
|
setOpenAgendaSessionPanelOnce(true);
|
|
setActiveNav("agenda");
|
|
}}
|
|
>
|
|
New Session
|
|
</button>
|
|
</>
|
|
}
|
|
/>
|
|
) : null}
|
|
{canReadForms ? eventForms.map((form) => {
|
|
const linkCopied = copiedFormLinkId === form._id;
|
|
return (
|
|
<ModuleCard
|
|
key={form._id}
|
|
icon={<FileText size={18} />}
|
|
title={form.name?.trim() || "Untitled form"}
|
|
meta={formMetaLine(form)}
|
|
headerAction={
|
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "4px", flexShrink: 0 }}>
|
|
<button
|
|
type="button"
|
|
title={linkCopied ? "Copied to clipboard" : "Copy public form link"}
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...formLinkCopyButtonStyle(linkCopied),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => copyFormPublicLink(form)}
|
|
onMouseEnter={(e) => {
|
|
if (actionsDisabled || linkCopied) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (linkCopied) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--muted)";
|
|
}}
|
|
>
|
|
{linkCopied ? <Check size={12} strokeWidth={3} /> : <Link2 size={12} />}
|
|
</button>
|
|
{isSuperAdmin ? (
|
|
<button
|
|
type="button"
|
|
title="Share form"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...formHeaderIconButtonStyle(),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setFormShareTarget(form)}
|
|
onMouseEnter={(e) => {
|
|
if (actionsDisabled) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--muted)";
|
|
}}
|
|
>
|
|
<Share2 size={12} />
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
}
|
|
actions={
|
|
<>
|
|
{canWriteForms ? (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setFormEditTarget(form)}
|
|
onMouseEnter={(e) => {
|
|
if (actionsDisabled) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (actionsDisabled) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
|
|
}}
|
|
>
|
|
Edit
|
|
</button>
|
|
) : null}
|
|
{canReadForms ? (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setFormResponsesTarget(form)}
|
|
>
|
|
Responses
|
|
</button>
|
|
) : null}
|
|
</>
|
|
}
|
|
/>
|
|
);
|
|
}) : null}
|
|
{canWriteForms ? (
|
|
<ModuleCard
|
|
icon={<Plus size={18} />}
|
|
title="Create New Form"
|
|
description="Add a form assigned to this event"
|
|
actions={
|
|
<>
|
|
<select
|
|
disabled={actionsDisabled}
|
|
value={newFormType}
|
|
onChange={(e) => setNewFormType(e.target.value as typeof newFormType)}
|
|
style={{
|
|
...moduleButtonStyle(),
|
|
flex: "1 1 0",
|
|
appearance: "auto",
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
>
|
|
{NEW_FORM_OPTIONS.map((opt) => (
|
|
<option key={opt.value} value={opt.value}>
|
|
{opt.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
flex: "0 0 auto",
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setNewFormMode(newFormType)}
|
|
>
|
|
Create
|
|
</button>
|
|
</>
|
|
}
|
|
/>
|
|
) : null}
|
|
{canReadUserMgmt ? (
|
|
<ModuleCard
|
|
icon={<ShieldCheck size={18} />}
|
|
title="Users & Permissions"
|
|
meta={permissionsMeta}
|
|
actions={
|
|
<>
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setActiveNav("permissions")}
|
|
onMouseEnter={(e) => {
|
|
if (actionsDisabled) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (actionsDisabled) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
|
|
}}
|
|
>
|
|
View
|
|
</button>
|
|
{canWriteUserMgmt ? (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => {
|
|
setPermissionsOpenAddOnce(true);
|
|
setActiveNav("permissions");
|
|
}}
|
|
>
|
|
Add New
|
|
</button>
|
|
) : null}
|
|
</>
|
|
}
|
|
/>
|
|
) : null}
|
|
{canReadActivities ? (
|
|
<ModuleCard
|
|
icon={<BarChart2 size={18} />}
|
|
title="Activities"
|
|
meta={pollQuizMeta}
|
|
actions={
|
|
<>
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || !canWriteActivities}
|
|
style={{
|
|
...moduleButtonStyle(),
|
|
opacity: actionsDisabled || !canWriteActivities ? 0.55 : 1,
|
|
cursor: actionsDisabled || !canWriteActivities ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setShowAddPoll(true)}
|
|
onMouseEnter={(e) => {
|
|
if (actionsDisabled || !canWriteActivities) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (actionsDisabled || !canWriteActivities) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
|
|
}}
|
|
>
|
|
Poll
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || !canWriteActivities}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled || !canWriteActivities ? 0.55 : 1,
|
|
cursor: actionsDisabled || !canWriteActivities ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setShowAddQuiz(true)}
|
|
>
|
|
Quiz
|
|
</button>
|
|
</>
|
|
}
|
|
/>
|
|
) : null}
|
|
{canReadBadges ? (
|
|
<ModuleCard
|
|
icon={<Tag size={18} />}
|
|
title="Tag & Badge Configuration"
|
|
meta={badgeMeta}
|
|
actions={
|
|
<>
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setBadgesOpen(true)}
|
|
onMouseEnter={(e) => {
|
|
if (actionsDisabled) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (actionsDisabled) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
|
|
}}
|
|
>
|
|
View Badges
|
|
</button>
|
|
{canWriteBadges ? (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={openAddBadge}
|
|
>
|
|
Add Badge
|
|
</button>
|
|
) : null}
|
|
</>
|
|
}
|
|
/>
|
|
) : null}
|
|
{canReadTicketing ? (
|
|
<ModuleCard
|
|
icon={<Ticket size={18} />}
|
|
title="Ticket configuration"
|
|
meta={ticketMeta}
|
|
actions={
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setActiveNav("ticketing")}
|
|
>
|
|
View
|
|
</button>
|
|
}
|
|
/>
|
|
) : null}
|
|
<ModuleCard
|
|
icon={<Palette size={18} />}
|
|
title="Visitor Theme"
|
|
description="Colors and background for visitor event, session, quiz, and photo pages"
|
|
meta={visitorThemeCardMeta}
|
|
actions={
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setVisitorThemeOpen(true)}
|
|
>
|
|
Manage
|
|
</button>
|
|
}
|
|
/>
|
|
{canReadMicrosite ? (
|
|
<ModuleCard
|
|
icon={<Globe size={18} />}
|
|
title="Microsite"
|
|
meta={micrositeMeta}
|
|
actions={
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setStaticMicrositeOpen(true)}
|
|
>
|
|
Manage
|
|
</button>
|
|
}
|
|
/>
|
|
) : null}
|
|
{canReadRewards ? (
|
|
<ModuleCard
|
|
icon={<Trophy size={18} />}
|
|
title="Reward Points Config"
|
|
meta={rewardMeta}
|
|
actions={
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setRewardRulesOpen(true)}
|
|
>
|
|
View
|
|
</button>
|
|
}
|
|
/>
|
|
) : null}
|
|
{canReadWebCheckIn ? (
|
|
<ModuleCard
|
|
icon={<Plus size={18} />}
|
|
title="Web check-in"
|
|
meta={webCheckInMeta}
|
|
description="Show or hide accommodation choice for participants"
|
|
actions={
|
|
webCheckInEnabled ? (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || webCheckInLoading || !canWriteWebCheckIn}
|
|
style={{
|
|
...moduleButtonStyle(),
|
|
opacity: actionsDisabled || webCheckInLoading || !canWriteWebCheckIn ? 0.55 : 1,
|
|
cursor:
|
|
actionsDisabled || webCheckInLoading || !canWriteWebCheckIn
|
|
? "not-allowed"
|
|
: "pointer",
|
|
}}
|
|
onClick={() => void handleWebCheckInToggle(false)}
|
|
>
|
|
Disable
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || webCheckInLoading || !canWriteWebCheckIn}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled || webCheckInLoading || !canWriteWebCheckIn ? 0.55 : 1,
|
|
cursor:
|
|
actionsDisabled || webCheckInLoading || !canWriteWebCheckIn
|
|
? "not-allowed"
|
|
: "pointer",
|
|
}}
|
|
onClick={() => void handleWebCheckInToggle(true)}
|
|
>
|
|
Enable
|
|
</button>
|
|
)
|
|
}
|
|
/>
|
|
) : null}
|
|
{canReadDocuments ? (
|
|
<ModuleCard
|
|
icon={<Upload size={18} />}
|
|
title="Upload Document"
|
|
description="Manage documents uploaded for this event"
|
|
actions={
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setShowDocumentsPanel(true)}
|
|
>
|
|
Manage
|
|
</button>
|
|
}
|
|
/>
|
|
) : null}
|
|
<ModuleCard
|
|
icon={<Image size={18} />}
|
|
title="Event Photo Search"
|
|
meta={photoSearchMeta}
|
|
description="Upload event photos and share visitor Find Photos"
|
|
headerAction={
|
|
photoSearchEnabled && photoSearchToken ? (
|
|
<button
|
|
type="button"
|
|
title="Copy visitor search link"
|
|
aria-label="Copy visitor search link"
|
|
disabled={actionsDisabled}
|
|
style={photoSearchHeaderButtonStyle}
|
|
onClick={() => void copyPhotoSearchLink()}
|
|
>
|
|
{copiedPhotoSearchLink ? <Check size={12} /> : <Link2 size={12} />}
|
|
</button>
|
|
) : null
|
|
}
|
|
actions={
|
|
<>
|
|
{photoSearchEnabled ? (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || photoSearchLoading}
|
|
style={{
|
|
...moduleButtonStyle(),
|
|
opacity: actionsDisabled || photoSearchLoading ? 0.55 : 1,
|
|
cursor: actionsDisabled || photoSearchLoading ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => void handlePhotoSearchToggle(false)}
|
|
>
|
|
Disable
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || photoSearchLoading}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled || photoSearchLoading ? 0.55 : 1,
|
|
cursor: actionsDisabled || photoSearchLoading ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => void handlePhotoSearchToggle(true)}
|
|
>
|
|
Enable
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(photoSearchEnabled),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setShowEventPhotoSearchPanel(true)}
|
|
>
|
|
Manage
|
|
</button>
|
|
</>
|
|
}
|
|
/>
|
|
{canReadSocialShare ? (
|
|
<ModuleCard
|
|
icon={<Share2 size={18} />}
|
|
title="Social Share"
|
|
meta={socialShareMeta}
|
|
description="Multi-frame photo create, download, and share"
|
|
headerAction={
|
|
socialShareEnabled && socialShareToken ? (
|
|
<button
|
|
type="button"
|
|
title="Copy visitor link"
|
|
onClick={() => void copySocialShareLink()}
|
|
style={formHeaderIconButtonStyle()}
|
|
>
|
|
{copiedSocialShareLink ? <Check size={14} /> : <Link2 size={14} />}
|
|
</button>
|
|
) : null
|
|
}
|
|
actions={
|
|
<>
|
|
{socialShareEnabled ? (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || socialShareLoading || !canWriteSocialShare}
|
|
style={{
|
|
...moduleButtonStyle(),
|
|
opacity: actionsDisabled || socialShareLoading || !canWriteSocialShare ? 0.55 : 1,
|
|
cursor:
|
|
actionsDisabled || socialShareLoading || !canWriteSocialShare
|
|
? "not-allowed"
|
|
: "pointer",
|
|
}}
|
|
onClick={() => void handleSocialShareToggle(false)}
|
|
>
|
|
Disable
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || socialShareLoading || !canWriteSocialShare}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled || socialShareLoading || !canWriteSocialShare ? 0.55 : 1,
|
|
cursor:
|
|
actionsDisabled || socialShareLoading || !canWriteSocialShare
|
|
? "not-allowed"
|
|
: "pointer",
|
|
}}
|
|
onClick={() => void handleSocialShareToggle(true)}
|
|
>
|
|
Enable
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(socialShareEnabled),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setShowSocialSharePanel(true)}
|
|
>
|
|
Manage
|
|
</button>
|
|
</>
|
|
}
|
|
/>
|
|
) : null}
|
|
{canReadAiAvatar ? (
|
|
<ModuleCard
|
|
icon={<Sparkles size={18} />}
|
|
title="AI Avatar"
|
|
meta={aiAvatarMeta}
|
|
description="AI head-swap avatars from visitor selfies"
|
|
headerAction={
|
|
aiAvatarEnabled && aiAvatarToken ? (
|
|
<button
|
|
type="button"
|
|
title="Copy visitor link"
|
|
onClick={() => void copyAiAvatarLink()}
|
|
style={formHeaderIconButtonStyle()}
|
|
>
|
|
{copiedAiAvatarLink ? <Check size={14} /> : <Link2 size={14} />}
|
|
</button>
|
|
) : null
|
|
}
|
|
actions={
|
|
<>
|
|
{aiAvatarEnabled ? (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || aiAvatarLoading || !canWriteAiAvatar}
|
|
style={{
|
|
...moduleButtonStyle(),
|
|
opacity: actionsDisabled || aiAvatarLoading || !canWriteAiAvatar ? 0.55 : 1,
|
|
cursor:
|
|
actionsDisabled || aiAvatarLoading || !canWriteAiAvatar
|
|
? "not-allowed"
|
|
: "pointer",
|
|
}}
|
|
onClick={() => void handleAiAvatarToggle(false)}
|
|
>
|
|
Disable
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || aiAvatarLoading || !canWriteAiAvatar}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled || aiAvatarLoading || !canWriteAiAvatar ? 0.55 : 1,
|
|
cursor:
|
|
actionsDisabled || aiAvatarLoading || !canWriteAiAvatar
|
|
? "not-allowed"
|
|
: "pointer",
|
|
}}
|
|
onClick={() => void handleAiAvatarToggle(true)}
|
|
>
|
|
Enable
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(aiAvatarEnabled),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setShowAiAvatarPanel(true)}
|
|
>
|
|
Manage
|
|
</button>
|
|
</>
|
|
}
|
|
/>
|
|
) : null}
|
|
{canReadPhotoWall ? (
|
|
<ModuleCard
|
|
icon={<Images size={18} />}
|
|
title="Photo Wall"
|
|
meta={photoWallMeta}
|
|
description="Visitor selfie uploads and projector wall"
|
|
headerAction={
|
|
photoWallEnabled && photoWallUploadToken && photoWallPresentToken ? (
|
|
<div style={{ display: "flex", gap: "4px" }}>
|
|
<button
|
|
type="button"
|
|
title="Copy visitor upload link"
|
|
onClick={() => void copyPhotoWallLink("upload")}
|
|
style={formHeaderIconButtonStyle()}
|
|
>
|
|
{copiedPhotoWallLink === "upload" ? <Check size={14} /> : <Link2 size={14} />}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
title="Copy presenter link"
|
|
onClick={() => void copyPhotoWallLink("present")}
|
|
style={formHeaderIconButtonStyle()}
|
|
>
|
|
{copiedPhotoWallLink === "present" ? <Check size={14} /> : <Share2 size={14} />}
|
|
</button>
|
|
</div>
|
|
) : null
|
|
}
|
|
actions={
|
|
<>
|
|
{photoWallEnabled ? (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || photoWallLoading || !canWritePhotoWall}
|
|
style={{
|
|
...moduleButtonStyle(),
|
|
opacity: actionsDisabled || photoWallLoading || !canWritePhotoWall ? 0.55 : 1,
|
|
cursor:
|
|
actionsDisabled || photoWallLoading || !canWritePhotoWall
|
|
? "not-allowed"
|
|
: "pointer",
|
|
}}
|
|
onClick={() => void handlePhotoWallToggle(false)}
|
|
>
|
|
Disable
|
|
</button>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || photoWallLoading || !canWritePhotoWall}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled || photoWallLoading || !canWritePhotoWall ? 0.55 : 1,
|
|
cursor:
|
|
actionsDisabled || photoWallLoading || !canWritePhotoWall
|
|
? "not-allowed"
|
|
: "pointer",
|
|
}}
|
|
onClick={() => void handlePhotoWallToggle(true)}
|
|
>
|
|
Enable
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(photoWallEnabled),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setShowPhotoWallPanel(true)}
|
|
>
|
|
Manage
|
|
</button>
|
|
</>
|
|
}
|
|
/>
|
|
) : null}
|
|
{canReadEmailCampaigns ? (
|
|
<ModuleCard
|
|
icon={<Mail size={18} />}
|
|
title="Email Campaign"
|
|
meta={emailCampaignMeta}
|
|
actions={
|
|
<>
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setShowEmailCampaignsPanel(true)}
|
|
onMouseEnter={(e) => {
|
|
if (actionsDisabled) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (actionsDisabled) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
|
|
}}
|
|
>
|
|
Manage
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || !canWriteEmailCampaigns}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled || !canWriteEmailCampaigns ? 0.55 : 1,
|
|
cursor: actionsDisabled || !canWriteEmailCampaigns ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setShowCreateEmailCampaign(true)}
|
|
>
|
|
Create
|
|
</button>
|
|
</>
|
|
}
|
|
/>
|
|
) : null}
|
|
{canReadExhibitors ? (
|
|
<ModuleCard
|
|
icon={<Store size={18} />}
|
|
title="Exhibitors"
|
|
meta={exhibitorMeta}
|
|
description="Companies and booths for this event"
|
|
actions={
|
|
<>
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled}
|
|
style={{
|
|
...moduleButtonStyle(),
|
|
opacity: actionsDisabled ? 0.55 : 1,
|
|
cursor: actionsDisabled ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => setShowExhibitorsPanel(true)}
|
|
onMouseEnter={(e) => {
|
|
if (actionsDisabled) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--primary)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--primary)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (actionsDisabled) return;
|
|
(e.currentTarget as HTMLElement).style.borderColor = "var(--border)";
|
|
(e.currentTarget as HTMLElement).style.color = "var(--card-foreground)";
|
|
}}
|
|
>
|
|
Manage
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={actionsDisabled || !canWriteExhibitors}
|
|
style={{
|
|
...moduleButtonStyle(true),
|
|
opacity: actionsDisabled || !canWriteExhibitors ? 0.55 : 1,
|
|
cursor: actionsDisabled || !canWriteExhibitors ? "not-allowed" : "pointer",
|
|
}}
|
|
onClick={() => {
|
|
setEditingExhibitor(null);
|
|
setShowAddExhibitor(true);
|
|
}}
|
|
>
|
|
Add exhibitor
|
|
</button>
|
|
</>
|
|
}
|
|
/>
|
|
) : null}
|
|
</section>
|
|
|
|
{formShareTarget ? (
|
|
<ShareFormPanel formId={formShareTarget._id} onClose={() => setFormShareTarget(null)} />
|
|
) : null}
|
|
|
|
{formEditTarget && selectedEventId ? (
|
|
<CreateFormPanel
|
|
mode={formPanelMode(formEditTarget.type)}
|
|
eventId={selectedEventId}
|
|
formId={formEditTarget._id}
|
|
onClose={() => setFormEditTarget(null)}
|
|
onSuccess={handleFormEditSuccess}
|
|
/>
|
|
) : null}
|
|
|
|
{formResponsesTarget ? (
|
|
<FormSubmissionsPanel
|
|
formId={formResponsesTarget._id}
|
|
formName={formResponsesTarget.name?.trim() || "Form"}
|
|
onClose={() => setFormResponsesTarget(null)}
|
|
/>
|
|
) : null}
|
|
|
|
{newFormMode && selectedEventId ? (
|
|
<CreateFormPanel
|
|
mode={newFormMode}
|
|
eventId={selectedEventId}
|
|
onClose={() => setNewFormMode(null)}
|
|
onSuccess={handleNewFormSuccess}
|
|
/>
|
|
) : null}
|
|
|
|
{showAddPoll ? (
|
|
<AddQuizPollModal
|
|
kind="poll"
|
|
eventId={selectedEventId ?? undefined}
|
|
onClose={() => setShowAddPoll(false)}
|
|
onSave={(data) => void handleSavePollQuiz("poll", data)}
|
|
/>
|
|
) : null}
|
|
|
|
{showAddQuiz ? (
|
|
<AddQuizPollModal
|
|
kind="quiz"
|
|
eventId={selectedEventId ?? undefined}
|
|
onClose={() => setShowAddQuiz(false)}
|
|
onSave={(data) => void handleSavePollQuiz("quiz", data)}
|
|
/>
|
|
) : null}
|
|
|
|
{badgesOpen && selectedEventId ? (
|
|
<EventBadgesPanel
|
|
eventId={selectedEventId}
|
|
eventName={eventName ?? undefined}
|
|
onClose={() => setBadgesOpen(false)}
|
|
onAddBadge={() => {
|
|
setBadgesOpen(false);
|
|
openAddBadge();
|
|
}}
|
|
onDuplicateBadge={openDuplicateBadge}
|
|
/>
|
|
) : null}
|
|
|
|
{showAddBadge && selectedEventId ? (
|
|
<CreateBadgePanel
|
|
key={duplicateBadgeBatchId ?? "create"}
|
|
initialEventId={selectedEventId}
|
|
duplicateFromBatchId={duplicateBadgeBatchId}
|
|
onClose={() => {
|
|
setShowAddBadge(false);
|
|
setDuplicateBadgeBatchId(null);
|
|
}}
|
|
onSuccess={handleBadgeSuccess}
|
|
/>
|
|
) : null}
|
|
|
|
{rewardRulesOpen && selectedEventId ? (
|
|
<RewardRulesSettingsPanel
|
|
eventId={selectedEventId}
|
|
onClose={() => setRewardRulesOpen(false)}
|
|
onSaved={() => void loadRewardRulesMeta()}
|
|
/>
|
|
) : null}
|
|
|
|
{visitorThemeOpen && selectedEventId ? (
|
|
<VisitorThemeSettingsPanel
|
|
eventId={selectedEventId}
|
|
onClose={() => setVisitorThemeOpen(false)}
|
|
onSaved={() => void loadVisitorThemeMeta()}
|
|
/>
|
|
) : null}
|
|
|
|
{staticMicrositeOpen && selectedEventId ? (
|
|
<ManageStaticMicrositePanel
|
|
eventId={selectedEventId}
|
|
onClose={() => setStaticMicrositeOpen(false)}
|
|
onSaved={() => void loadStaticMicrositeMeta()}
|
|
/>
|
|
) : null}
|
|
|
|
{showDocumentsPanel && selectedEventId ? (
|
|
<EventDocumentsPanel
|
|
eventId={selectedEventId}
|
|
eventName={eventName ?? undefined}
|
|
onClose={() => setShowDocumentsPanel(false)}
|
|
/>
|
|
) : null}
|
|
|
|
{showEventPhotoSearchPanel && selectedEventId ? (
|
|
<EventPhotoSearchPanel
|
|
open
|
|
eventId={selectedEventId}
|
|
eventName={eventName ?? undefined}
|
|
onClose={() => setShowEventPhotoSearchPanel(false)}
|
|
onChanged={() => void loadPhotoSearchMeta()}
|
|
/>
|
|
) : null}
|
|
|
|
{showPhotoWallPanel && selectedEventId ? (
|
|
<PhotoWallManagePanel
|
|
eventId={selectedEventId}
|
|
eventName={eventName ?? undefined}
|
|
canWrite={canWritePhotoWall}
|
|
onClose={() => setShowPhotoWallPanel(false)}
|
|
onChanged={() => void loadPhotoWallMeta()}
|
|
/>
|
|
) : null}
|
|
|
|
{showSocialSharePanel && selectedEventId ? (
|
|
<SocialShareManagePanel
|
|
eventId={selectedEventId}
|
|
eventName={eventName ?? undefined}
|
|
canWrite={canWriteSocialShare}
|
|
onClose={() => setShowSocialSharePanel(false)}
|
|
onChanged={() => void loadSocialShareMeta()}
|
|
/>
|
|
) : null}
|
|
|
|
{showAiAvatarPanel && selectedEventId ? (
|
|
<AiAvatarManagePanel
|
|
eventId={selectedEventId}
|
|
eventName={eventName ?? undefined}
|
|
canWrite={canWriteAiAvatar}
|
|
onClose={() => setShowAiAvatarPanel(false)}
|
|
onChanged={() => void loadAiAvatarMeta()}
|
|
/>
|
|
) : null}
|
|
|
|
{showEmailCampaignsPanel && selectedEventId ? (
|
|
<EventEmailCampaignsPanel
|
|
eventId={selectedEventId}
|
|
eventName={eventName ?? undefined}
|
|
onClose={() => setShowEmailCampaignsPanel(false)}
|
|
onChanged={() => void loadEmailCampaignMeta()}
|
|
/>
|
|
) : null}
|
|
|
|
{showCreateEmailCampaign && selectedEventId ? (
|
|
<CreateEmailCampaignPanel
|
|
eventId={selectedEventId}
|
|
onClose={() => setShowCreateEmailCampaign(false)}
|
|
onSuccess={() => void loadEmailCampaignMeta()}
|
|
/>
|
|
) : null}
|
|
|
|
{showExhibitorsPanel && selectedEventId ? (
|
|
<EventExhibitorsPanel
|
|
eventId={selectedEventId}
|
|
eventName={eventName ?? undefined}
|
|
canWrite={canWriteExhibitors}
|
|
onClose={() => setShowExhibitorsPanel(false)}
|
|
onAdd={() => {
|
|
setShowExhibitorsPanel(false);
|
|
setEditingExhibitor(null);
|
|
setShowAddExhibitor(true);
|
|
}}
|
|
onEdit={(ex) => {
|
|
setShowExhibitorsPanel(false);
|
|
setEditingExhibitor(ex);
|
|
setShowAddExhibitor(true);
|
|
}}
|
|
onChanged={() => void loadExhibitorMeta()}
|
|
/>
|
|
) : null}
|
|
|
|
{showAddExhibitor && selectedEventId ? (
|
|
<AddExhibitorPanel
|
|
key={editingExhibitor?._id ?? "create"}
|
|
eventId={selectedEventId}
|
|
eventName={eventName ?? undefined}
|
|
exhibitor={editingExhibitor}
|
|
onClose={() => {
|
|
setShowAddExhibitor(false);
|
|
setEditingExhibitor(null);
|
|
}}
|
|
onSuccess={() => {
|
|
void loadExhibitorMeta();
|
|
setShowExhibitorsPanel(true);
|
|
}}
|
|
/>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|