Compare commits

..

2 Commits

9 changed files with 336 additions and 33 deletions

View File

@ -1,5 +1,9 @@
import React, { useCallback, useEffect, useState } from "react";
<<<<<<< HEAD
import { BarChart2, CalendarDays, FileText, Plus, ShieldCheck, Tag, Ticket, Trophy, Upload } from "lucide-react";
=======
import { BarChart2, CalendarDays, Check, FileText, Globe, Link2, Plus, ShieldCheck, Tag, Ticket, Trophy } from "lucide-react";
>>>>>>> aed52029c61eaa3a9376202a3d34d546a6f56b28
import { useBreakpoint } from "../../hooks/useBreakpoint";
import { useAppNav } from "../../contexts/AppNavContext";
import { eventsApi } from "../../../services/api/events";
@ -13,6 +17,7 @@ import { CreateFormPanel } from "../panels/CreateFormPanel";
import { FormSubmissionsPanel } from "../panels/FormSubmissionsPanel";
import { CreateBadgePanel } from "../panels/CreateBadgePanel";
import { EventBadgesPanel } from "./EventBadgesPanel";
import { EventDocumentsPanel } from "./EventDocumentsPanel";
import { AddQuizPollModal, buildQuizPollSettingsPayload } from "../quiz/AddQuizPollModal";
import type { QuizPollModalForm } from "../quiz/AddQuizPollModal";
import type { Question } from "../quiz/AddQuestionModal";
@ -215,6 +220,7 @@ export function EventHomeModules() {
const [eventSettings, setEventSettings] = useState<Record<string, unknown>>({});
const [webCheckInEnabled, setWebCheckInEnabled] = useState(false);
const [webCheckInLoading, setWebCheckInLoading] = useState(false);
const [showDocumentsPanel, setShowDocumentsPanel] = useState(false);
useEffect(() => {
if (!selectedEventId) {
@ -870,32 +876,39 @@ export function EventHomeModules() {
meta={webCheckInMeta}
description="Show or hide accommodation choice for participants"
actions={
<>
webCheckInEnabled ? (
<button
type="button"
disabled={actionsDisabled || webCheckInLoading}
style={{
...moduleButtonStyle(),
opacity: actionsDisabled || webCheckInLoading ? 0.55 : 1,
cursor: actionsDisabled || webCheckInLoading ? "not-allowed" : "pointer",
cursor:
actionsDisabled || webCheckInLoading
? "not-allowed"
: "pointer",
}}
onClick={() => void handleWebCheckInToggle(true)}
onClick={() => void handleWebCheckInToggle(false)}
>
Enable
Disable
</button>
) : (
<button
type="button"
disabled={actionsDisabled || webCheckInLoading}
style={{
...moduleButtonStyle(true),
opacity: actionsDisabled || webCheckInLoading ? 0.55 : 1,
cursor: actionsDisabled || webCheckInLoading ? "not-allowed" : "pointer",
cursor:
actionsDisabled || webCheckInLoading
? "not-allowed"
: "pointer",
}}
onClick={() => void handleWebCheckInToggle(false)}
onClick={() => void handleWebCheckInToggle(true)}
>
Disable
Enable
</button>
</>
)
}
/>
<ModuleCard
@ -975,6 +988,25 @@ export function EventHomeModules() {
</>
}
/>
<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>
}
/>
</section>
{formEditTarget && selectedEventId ? (
@ -1056,11 +1088,19 @@ export function EventHomeModules() {
/>
) : null}
<<<<<<< HEAD
{showDocumentsPanel && selectedEventId ? (
<EventDocumentsPanel
eventId={selectedEventId}
eventName={eventName ?? undefined}
onClose={() => setShowDocumentsPanel(false)}
=======
{staticMicrositeOpen && selectedEventId ? (
<ManageStaticMicrositePanel
eventId={selectedEventId}
onClose={() => setStaticMicrositeOpen(false)}
onSaved={() => void loadStaticMicrositeMeta()}
>>>>>>> aed52029c61eaa3a9376202a3d34d546a6f56b28
/>
) : null}
</>

View File

@ -1429,8 +1429,10 @@ export function AllParticipantsPage() {
bulkDialog={bulk.bulkDialog}
bulkBusy={bulk.bulkBusy}
selectedCount={selectedIds.length}
tagBulkInput={bulk.tagBulkInput}
onTagBulkInputChange={bulk.setTagBulkInput}
selectedTagValues={bulk.selectedTagValues}
onToggleTag={bulk.toggleSelectedTag}
availableTags={tagOptions}
removeTagOptions={Array.from(new Set(list.filter((participant) => selectedIds.includes(participant.id)).flatMap((participant) => participant.tags)))}
newListName={bulk.newListName}
onNewListNameChange={bulk.setNewListName}
newListDesc={bulk.newListDesc}
@ -1442,7 +1444,7 @@ export function AllParticipantsPage() {
addToListId={bulk.addToListId}
onAddToListIdChange={bulk.setAddToListId}
onClose={() => bulk.setBulkDialog(null)}
onApplyTags={(action) => void bulk.runBulkTags(action)}
onApplyTags={(action, overrideTags) => void bulk.runBulkTags(action, overrideTags)}
onAssignEvents={() => void bulk.runBulkAssignEvents()}
onCreateList={() => void bulk.runBulkNewList()}
onAddToList={() => void bulk.runBulkAddToList()}

View File

@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useState } from "react";
import { X, Download, ChevronLeft, ChevronRight } from "lucide-react";
import { X, Download, ChevronLeft, ChevronRight, MailPlus } from "lucide-react";
import { formsApi } from "../../../services/api";
import { useAuth } from "../../contexts/AuthContext";
import type { Form, FormField, FormSubmissionRecord } from "../../../services/api";
import { buildFormSubmissionsExportFilename } from "../../utils/formExportFilename";
@ -28,10 +29,16 @@ function participantEmail(
return p.email?.trim() || "—";
}
<<<<<<< HEAD
function participantName(p: FormSubmissionRecord["participant"]): string {
if (!p || typeof p === "string") return "there";
return [p.firstName, p.lastName].filter(Boolean).join(" ").trim() || "there";
=======
function eventNameFromForm(form: Form | null): string {
const ev = form?.event;
if (ev && typeof ev === "object" && ev.name?.trim()) return ev.name.trim();
return "Event";
>>>>>>> aed52029c61eaa3a9376202a3d34d546a6f56b28
}
type Props = {
@ -58,6 +65,12 @@ export function FormSubmissionsPanel({ formId, formName, onClose }: Props) {
const [loading, setLoading] = useState(true);
const [err, setErr] = useState<string | null>(null);
const [exporting, setExporting] = useState(false);
const [replyTarget, setReplyTarget] = useState<FormSubmissionRecord | null>(null);
const [replySubject, setReplySubject] = useState("");
const [replyBody, setReplyBody] = useState("");
const [replying, setReplying] = useState(false);
const [replyMessage, setReplyMessage] = useState<string | null>(null);
const { user } = useAuth();
const fieldLabels = (formDef?.fields || [])
.map((f: FormField) => f.label?.trim())
@ -138,6 +151,41 @@ export function FormSubmissionsPanel({ formId, formName, onClose }: Props) {
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
const openReply = (submission: FormSubmissionRecord) => {
const email = participantEmail(submission.participant);
if (email === "—") {
setReplyMessage("This submission does not include an email address.");
return;
}
setReplyTarget(submission);
setReplySubject(`Re: Your response to ${formName}`);
setReplyBody(`Hi ${participantName(submission.participant)},\n\nThank you for your response.\n\nBest regards,\n${user?.firstName || user?.email || "EventBuzz"}`);
setReplyMessage(null);
};
const sendReply = async () => {
if (!replyTarget) return;
const to = participantEmail(replyTarget.participant);
if (!to || to === "—") {
setReplyMessage("Please choose a submission with a valid email address.");
return;
}
setReplying(true);
setReplyMessage(null);
try {
await formsApi.replyToSubmission(formId, { to, subject: replySubject, body: replyBody });
setReplyMessage("Email sent successfully.");
setReplyTarget(null);
setReplySubject("");
setReplyBody("");
} catch (e) {
setReplyMessage(e instanceof Error ? e.message : "Failed to send reply.");
} finally {
setReplying(false);
}
};
return (
<>
<div
@ -391,6 +439,7 @@ export function FormSubmissionsPanel({ formId, formName, onClose }: Props) {
>
<thead>
<tr style={{ background: "var(--secondary)", borderBottom: "1px solid var(--border)" }}>
<th style={{ textAlign: "left", padding: "10px 12px", whiteSpace: "nowrap" }}>Reply</th>
<th style={{ textAlign: "left", padding: "10px 12px", whiteSpace: "nowrap" }}>Submitted</th>
<th style={{ textAlign: "left", padding: "10px 12px", whiteSpace: "nowrap" }}>Email</th>
{tableFieldLabels.map((lab) => (
@ -413,6 +462,29 @@ export function FormSubmissionsPanel({ formId, formName, onClose }: Props) {
const o = dataToObj(s.data);
return (
<tr key={s._id} style={{ borderBottom: "1px solid var(--border)", verticalAlign: "top" }}>
<td style={{ padding: "10px 12px" }}>
<button
type="button"
onClick={() => openReply(s)}
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
padding: "6px 10px",
borderRadius: "var(--radius-button)",
border: "1px solid var(--border)",
background: "var(--secondary)",
color: "var(--foreground)",
cursor: "pointer",
fontSize: "var(--text-xs)",
fontFamily: "var(--font-noto-sans)",
fontWeight: 600,
}}
>
<MailPlus size={14} />
Reply
</button>
</td>
<td style={{ padding: "10px 12px", whiteSpace: "nowrap", color: "var(--foreground)" }}>
{formatWhen(s.submittedAt)}
</td>
@ -439,6 +511,53 @@ export function FormSubmissionsPanel({ formId, formName, onClose }: Props) {
)}
</div>
{replyTarget ? (
<div
style={{
padding: "16px 18px",
borderTop: "1px solid var(--border)",
background: "var(--background)",
display: "flex",
flexDirection: "column",
gap: 10,
flexShrink: 0,
}}
>
<div style={{ display: "flex", justifyContent: "space-between", gap: 12, alignItems: "center" }}>
<div>
<p style={{ margin: 0, fontSize: "var(--text-sm)", fontWeight: 600, color: "var(--foreground)" }}>
Reply to {participantEmail(replyTarget.participant)}
</p>
<p style={{ margin: "2px 0 0", fontSize: "var(--text-xs)", color: "var(--muted)" }}>
This sends a real email from the connected mailbox configuration.
</p>
</div>
<button type="button" onClick={() => { setReplyTarget(null); setReplySubject(""); setReplyBody(""); setReplyMessage(null); }} style={{ border: "none", background: "transparent", color: "var(--muted)", cursor: "pointer" }}>
Cancel
</button>
</div>
<input
value={replySubject}
onChange={(e) => setReplySubject(e.target.value)}
placeholder="Subject"
style={{ padding: "8px 10px", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", background: "var(--card)", color: "var(--foreground)" }}
/>
<textarea
value={replyBody}
onChange={(e) => setReplyBody(e.target.value)}
placeholder="Write your message here"
rows={8}
style={{ padding: "8px 10px", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", background: "var(--card)", color: "var(--foreground)", resize: "vertical" }}
/>
{replyMessage ? <p style={{ margin: 0, fontSize: "var(--text-xs)", color: replyMessage.includes("success") ? "var(--success)" : "var(--destructive)" }}>{replyMessage}</p> : null}
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<button type="button" onClick={() => void sendReply()} disabled={replying} style={{ padding: "8px 14px", borderRadius: "var(--radius-button)", border: "none", background: "var(--primary)", color: "var(--primary-foreground)", cursor: replying ? "not-allowed" : "pointer", opacity: replying ? 0.7 : 1 }}>
{replying ? "Sending…" : "Send reply"}
</button>
</div>
</div>
) : null}
<div
style={{
padding: "12px 18px",

View File

@ -1,6 +1,8 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { Check, X } from "lucide-react";
import type { Event } from "../../../services/api/types";
import type { ParticipantListRecord } from "../../../services/api/participantLists";
import { tagChipStyle } from "./participantTagUtils";
export type BulkDialogKind = null | "addTags" | "removeTags" | "assignEvents" | "newList" | "addToList";
@ -8,8 +10,10 @@ export function ParticipantBulkDialogs({
bulkDialog,
bulkBusy,
selectedCount,
tagBulkInput,
onTagBulkInputChange,
selectedTagValues,
onToggleTag,
availableTags,
removeTagOptions,
newListName,
onNewListNameChange,
newListDesc,
@ -29,8 +33,10 @@ export function ParticipantBulkDialogs({
bulkDialog: BulkDialogKind;
bulkBusy: boolean;
selectedCount: number;
tagBulkInput: string;
onTagBulkInputChange: (v: string) => void;
selectedTagValues: string[];
onToggleTag: (tag: string) => void;
availableTags: string[];
removeTagOptions: string[];
newListName: string;
onNewListNameChange: (v: string) => void;
newListDesc: string;
@ -42,11 +48,32 @@ export function ParticipantBulkDialogs({
addToListId: string;
onAddToListIdChange: (id: string) => void;
onClose: () => void;
onApplyTags: (action: "add_tags" | "remove_tags") => void;
onApplyTags: (action: "add_tags" | "remove_tags", overrideTags?: string[]) => void;
onAssignEvents: () => void;
onCreateList: () => void;
onAddToList: () => void;
}) {
const [manualTagInput, setManualTagInput] = useState("");
useEffect(() => {
if (bulkDialog !== "addTags" && bulkDialog !== "removeTags") setManualTagInput("");
}, [bulkDialog]);
const addManualTags = () => {
const tokens = manualTagInput
.split(/[\n,;]+/)
.map((token) => token.trim())
.filter(Boolean);
if (!tokens.length) return [];
tokens.forEach((token) => {
if (!selectedTagValues.includes(token)) onToggleTag(token);
});
setManualTagInput("");
return Array.from(new Set([...selectedTagValues, ...tokens]));
};
const overlay = (children: React.ReactNode) => (
<div
role="dialog"
@ -71,13 +98,110 @@ export function ParticipantBulkDialogs({
);
if (bulkDialog === "addTags" || bulkDialog === "removeTags") {
const tagOptions = bulkDialog === "removeTags" ? removeTagOptions : availableTags;
const emptyMessage = bulkDialog === "removeTags" ? "No tags are currently assigned to the selected participants." : "No tags are available yet for this event.";
return overlay(
<>
<h3 style={{ fontFamily: "var(--font-inter)", fontSize: "var(--text-base)", margin: "0 0 8px" }}>{bulkDialog === "addTags" ? "Add tags" : "Remove tags"}</h3>
<p style={{ fontSize: "var(--text-xs)", color: "var(--muted)", margin: "0 0 12px", fontFamily: "var(--font-noto-sans)" }}>Apply to {selectedCount} selected (comma-separated).</p>
<input value={tagBulkInput} onChange={(e) => onTagBulkInputChange(e.target.value)} placeholder="e.g. VIP, Sponsor" style={{ width: "100%", padding: "8px 12px", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", fontSize: "var(--text-sm)", boxSizing: "border-box" }} />
<p style={{ fontSize: "var(--text-xs)", color: "var(--muted)", margin: "0 0 12px", fontFamily: "var(--font-noto-sans)" }}>Type a tag or select one below to apply to {selectedCount} selected participant(s).</p>
<input
value={manualTagInput}
onChange={(e) => setManualTagInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === "," || e.key === ";") {
e.preventDefault();
addManualTags();
}
}}
placeholder={bulkDialog === "addTags" ? "Type a tag and press Enter" : "Type a tag and press Enter"}
style={{ width: "100%", padding: "8px 12px", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", fontSize: "var(--text-sm)", boxSizing: "border-box", marginBottom: "10px" }}
/>
{selectedTagValues.length > 0 && (
<div style={{ marginBottom: "10px" }}>
<div style={{ fontSize: "11px", fontFamily: "var(--font-noto-sans)", fontWeight: 600, color: "var(--muted)", textTransform: "uppercase", letterSpacing: "0.04em", marginBottom: "6px" }}>Selected</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
{selectedTagValues.map((tag) => {
const tc = tagChipStyle(tag);
return (
<button
key={tag}
type="button"
onClick={() => onToggleTag(tag)}
style={{
display: "inline-flex",
alignItems: "center",
gap: "6px",
padding: "6px 10px",
borderRadius: "999px",
border: "1px solid var(--border)",
background: tc.bg,
color: tc.color,
cursor: "pointer",
fontSize: "var(--text-sm)",
fontFamily: "var(--font-noto-sans)",
}}
>
<X size={12} />
{tag}
</button>
);
})}
</div>
</div>
)}
{tagOptions.length === 0 ? (
<p style={{ fontSize: "var(--text-sm)", color: "var(--muted)", margin: "0 0 8px" }}>{emptyMessage}</p>
) : (
<div>
<div style={{ fontSize: "11px", fontFamily: "var(--font-noto-sans)", fontWeight: 600, color: "var(--muted)", textTransform: "uppercase", letterSpacing: "0.04em", marginBottom: "6px" }}></div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px", marginBottom: "10px" }}>
{tagOptions
.filter(tag => !selectedTagValues.includes(tag))
.map((tag) => {
const active = selectedTagValues.includes(tag);
const tc = tagChipStyle(tag);
return (
<button
key={tag}
type="button"
onClick={() => onToggleTag(tag)}
style={{
display: "inline-flex",
alignItems: "center",
gap: "6px",
padding: "6px 10px",
borderRadius: "999px",
border: `1px solid ${active ? "var(--primary)" : "var(--border)"}`,
background: active ? tc.bg : "var(--card)",
color: active ? tc.color : "var(--card-foreground)",
cursor: "pointer",
fontSize: "var(--text-sm)",
fontFamily: "var(--font-noto-sans)",
boxShadow: active ? `inset 0 0 0 1px ${tc.color}25` : "none",
}}
>
{active ? <Check size={12} /> : null}
{tag}
</button>
);
})}
</div>
</div>
)}
{footer(
<button type="button" disabled={bulkBusy} onClick={() => onApplyTags(bulkDialog === "addTags" ? "add_tags" : "remove_tags")} style={{ padding: "7px 14px", borderRadius: "var(--radius-button)", border: "none", background: "var(--primary)", color: "var(--primary-foreground)", cursor: "pointer", fontSize: "var(--text-sm)" }}>Apply</button>
<button
type="button"
disabled={bulkBusy || (selectedTagValues.length === 0 && !manualTagInput.trim())}
onClick={() => {
const manualTokens = addManualTags();
const effectiveTags = manualTokens.length ? manualTokens : selectedTagValues;
onApplyTags(bulkDialog === "addTags" ? "add_tags" : "remove_tags", effectiveTags);
}}
style={{ padding: "7px 14px", borderRadius: "var(--radius-button)", border: "none", background: "var(--primary)", color: "var(--primary-foreground)", cursor: "pointer", fontSize: "var(--text-sm)" }}
>
Apply
</button>
)}
</>
);

View File

@ -1093,8 +1093,10 @@ export function ParticipantsPage({ compactChrome = false }: ParticipantsPageProp
bulkDialog={bulk.bulkDialog}
bulkBusy={bulk.bulkBusy}
selectedCount={selectedIds.length}
tagBulkInput={bulk.tagBulkInput}
onTagBulkInputChange={bulk.setTagBulkInput}
selectedTagValues={bulk.selectedTagValues}
onToggleTag={bulk.toggleSelectedTag}
availableTags={tagOptions}
removeTagOptions={Array.from(new Set(list.filter((participant) => selectedIds.includes(participant.id)).flatMap((participant) => participant.tags)))}
newListName={bulk.newListName}
onNewListNameChange={bulk.setNewListName}
newListDesc={bulk.newListDesc}
@ -1106,7 +1108,7 @@ export function ParticipantsPage({ compactChrome = false }: ParticipantsPageProp
addToListId={bulk.addToListId}
onAddToListIdChange={bulk.setAddToListId}
onClose={() => bulk.setBulkDialog(null)}
onApplyTags={(action) => void bulk.runBulkTags(action)}
onApplyTags={(action, overrideTags) => void bulk.runBulkTags(action, overrideTags)}
onAssignEvents={() => void bulk.runBulkAssignEvents()}
onCreateList={() => void bulk.runBulkNewList()}
onAddToList={() => void bulk.runBulkAddToList()}

View File

@ -4,7 +4,6 @@ import { participantListsApi } from "../../../services/api/participantLists";
import { eventsApi } from "../../../services/api/events";
import type { Event } from "../../../services/api/types";
import type { ParticipantListRecord } from "../../../services/api/participantLists";
import { parseTagsBulk } from "./participantTagUtils";
import type { BulkDialogKind } from "./ParticipantBulkDialogs";
export function useParticipantBulkActions(options: {
@ -19,7 +18,7 @@ export function useParticipantBulkActions(options: {
const [bulkDialog, setBulkDialog] = useState<BulkDialogKind>(null);
const [bulkBusy, setBulkBusy] = useState(false);
const [tagBulkInput, setTagBulkInput] = useState("");
const [selectedTagValues, setSelectedTagValues] = useState<string[]>([]);
const [newListName, setNewListName] = useState("");
const [newListDesc, setNewListDesc] = useState("");
const [eventsPick, setEventsPick] = useState<Event[]>([]);
@ -27,8 +26,12 @@ export function useParticipantBulkActions(options: {
const [fixedLists, setFixedLists] = useState<ParticipantListRecord[]>([]);
const [addToListId, setAddToListId] = useState("");
const toggleSelectedTag = (tag: string) => {
setSelectedTagValues((prev) => prev.includes(tag) ? prev.filter((value) => value !== tag) : [...prev, tag]);
};
const openBulkDialog = async (kind: NonNullable<BulkDialogKind>) => {
setTagBulkInput("");
setSelectedTagValues([]);
setNewListName("");
setNewListDesc("");
setAddToListId("");
@ -63,8 +66,8 @@ export function useParticipantBulkActions(options: {
});
};
const runBulkTags = async (action: "add_tags" | "remove_tags") => {
const tags = parseTagsBulk(tagBulkInput);
const runBulkTags = async (action: "add_tags" | "remove_tags", overrideTags?: string[]) => {
const tags = (overrideTags?.length ? overrideTags : selectedTagValues).filter(Boolean);
if (!tags.length) return;
setBulkBusy(true);
const res = await participantsApi.bulk({ action, participantIds: selectedIds, tags });
@ -158,8 +161,8 @@ export function useParticipantBulkActions(options: {
bulkDialog,
setBulkDialog,
bulkBusy,
tagBulkInput,
setTagBulkInput,
selectedTagValues,
toggleSelectedTag,
newListName,
setNewListName,
newListDesc,

View File

@ -118,6 +118,15 @@ export const formsApi = {
},
/** CSV of all rows matching the same filters as the list (not paginated). */
async replyToSubmission(
formId: string,
payload: { to: string; subject?: string; body?: string }
): Promise<{ sent: boolean }> {
const r = await api.post<{ sent: boolean }>(`/forms/${formId}/submissions/reply`, payload);
if (r.error) throw new Error(r.error);
return r.data ?? { sent: false };
},
async exportSubmissionsCsv(
formId: string,
params?: { q?: string; dateFrom?: string; dateTo?: string }

View File

@ -19,6 +19,7 @@ export interface ListMediaParams {
page?: number;
limit?: number;
type?: string;
eventId?: string;
}
export async function listMedia(params: ListMediaParams = {}): Promise<{ media: LibraryContent[]; total: number }> {
@ -27,7 +28,9 @@ export async function listMedia(params: ListMediaParams = {}): Promise<{ media:
q.set('page', String(params.page ?? 1));
q.set('limit', String(params.limit ?? 200));
if (params.type) q.set('type', params.type);
const r = await api.get<{ media: LibraryContent[]; total: number }>(`/library/media?${q.toString()}`);
if (params.eventId) q.set('eventId', params.eventId);
const endpoint = params.eventId ? '/media' : '/library/media';
const r = await api.get<{ media: LibraryContent[]; total: number }>(`${endpoint}?${q.toString()}`);
if (r.error) throw new Error(r.error);
return { media: r.data?.media ?? [], total: r.data?.total ?? 0 };
}

View File

@ -624,6 +624,7 @@ export interface Speaker {
export interface LibraryContent {
_id: string;
organization?: string;
eventId?: string;
name?: string;
description?: string;
visibility?: 'private' | 'public';
@ -634,7 +635,7 @@ export interface LibraryContent {
tags?: string[];
collectionName?: string;
s3Key?: string;
uploadedBy?: string;
uploadedBy?: string | { firstName?: string; lastName?: string; email?: string; phone?: string };
createdAt?: string;
updatedAt?: string;
}