/** * Activities — standalone Q&A / quiz / poll shells (Event recordKind=activity). */ import React, { useState, useMemo, useEffect, useCallback } from "react"; import { Search, X, LayoutGrid, List, Calendar as CalIcon, MessageCircle, BarChart3, HelpCircle, Users, CheckCircle2, Star, } from "lucide-react"; import { useBreakpoint } from "../../hooks/useBreakpoint"; import { useAuth } from "../../contexts/AuthContext"; import { portfolioListOrganizationId, eventsApi, sessionsApi, quizPollsApi } from "../../../services/api"; import type { Event } from "../../../services/api/types"; import { LoadingState } from "../shared"; import { FlatpickrDatetimeInput } from "../ui/FlatpickrDatetimeInput"; import { AddQuizPollModal, buildQuizPollSettingsPayload, datetimeLocalToIso, defaultActivityShellDateTimes, defaultQuizPollModalForm, type QuizPollModalForm, } from "../quiz/AddQuizPollModal"; import type { Question } from "../quiz/AddQuestionModal"; import { modalQuestionToApi } from "../quiz/quizQuestionMappers"; import { formatActivityRating, weightedActivityRating } from "./activityRatingDisplay"; function mapQuestionForQuizPollApi(q: Question, kind: "quiz" | "poll") { return modalQuestionToApi(q, kind); } type ActivityUiType = "Q&A Session" | "Quiz" | "Poll"; type ActivityStatusUi = "Active" | "Scheduled" | "Completed" | "Draft"; type ViewMode = "card" | "list" | "calendar"; interface ActivityRow { id: string; name: string; type: ActivityUiType; status: ActivityStatusUi; createdDate: string; subtitle?: string; participants: number; questions: number; responses: number; eventRatingAverage: number; eventRatingCount: number; sessionRatingAverage: number; sessionRatingCount: number; subtype: "qna" | "quiz" | "poll"; raw: Event; } function mapSubtype(s: string | null | undefined): ActivityUiType { if (s === "quiz") return "Quiz"; if (s === "poll") return "Poll"; return "Q&A Session"; } function mapStatus(st: string | undefined): ActivityStatusUi { const x = (st || "draft").toLowerCase(); if (x === "live") return "Active"; if (x === "upcoming") return "Scheduled"; if (x === "completed") return "Completed"; return "Draft"; } function formatListDate(iso: string | undefined): string { if (!iso) return "—"; try { return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }); } catch { return "—"; } } const shellInputStyle: React.CSSProperties = { width: "100%", boxSizing: "border-box", padding: "10px 12px", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", background: "var(--background)", color: "var(--foreground)", fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", }; async function createActivityShell( organizationId: string, name: string, subtype: "qna" | "quiz" | "poll", startLocal: string, endLocal: string, ): Promise<{ ok: true; eventId: string; sessionId: string } | { ok: false; error: string }> { const startIso = datetimeLocalToIso(startLocal); const endIso = datetimeLocalToIso(endLocal); if (!startIso || !endIso) { return { ok: false, error: "Invalid start or end date/time." }; } const start = new Date(startIso); let end = new Date(endIso); if (end <= start) end = new Date(start.getTime() + 60 * 60 * 1000); const res = await eventsApi.create({ name: name.trim(), type: "webinar", recordKind: "activity", activitySubtype: subtype, status: "draft", startDate: start.toISOString(), endDate: end.toISOString(), locationType: "virtual", organization: organizationId, }); if (res.error || !res.data?._id) { return { ok: false, error: res.error || "Could not create activity." }; } const eventId = res.data._id; const sesRes = await sessionsApi.list(eventId); const sessions = sesRes.data?.sessions ?? []; const sessionId = sessions[0]?._id; if (!sessionId) { return { ok: false, error: "Activity created but no session was found." }; } return { ok: true, eventId, sessionId }; } function eventToRow(ev: Event): ActivityRow { const st = ev.activityStats; return { id: ev._id, name: ev.name, type: mapSubtype(ev.activitySubtype), status: mapStatus(ev.status), createdDate: formatListDate(ev.createdAt), subtitle: ev.description?.trim() ? ev.description.trim().slice(0, 120) : ev.subtitle?.trim() || undefined, participants: st?.participantCount ?? 0, questions: st?.questionCount ?? 0, responses: st?.responseCount ?? 0, eventRatingAverage: st?.eventRatingAverage ?? 0, eventRatingCount: st?.eventRatingCount ?? 0, sessionRatingAverage: st?.sessionRatingAverage ?? 0, sessionRatingCount: st?.sessionRatingCount ?? 0, subtype: (ev.activitySubtype as "qna" | "quiz" | "poll") || "qna", raw: ev, }; } const TYPE_COLORS: Record = { "Q&A Session": { bg: "#E8EFFC", fg: "#0B4DD1", icon: }, "Quiz": { bg: "#FEF3C7", fg: "#D97706", icon: }, "Poll": { bg: "#ECFDF5", fg: "#059669", icon: }, }; const STATUS_BADGE: Record = { Active: { bg: "#dcfce7", color: "#15803d" }, Scheduled: { bg: "#dbeafe", color: "#1d4ed8" }, Completed: { bg: "#f1f5f9", color: "#475569" }, Draft: { bg: "#ffedd5", color: "#c2410c" }, }; function ActivitySidePanel({ title, subtitle, onClose, children, footer, }: { title: string; subtitle?: string; onClose: () => void; children: React.ReactNode; footer?: React.ReactNode; }) { return ( <>

{title}

{subtitle ? (

{subtitle}

) : null}
{children}
{footer ? (
{footer}
) : null}
); } export function ActivitiesPage({ onNavigateToActivity, }: { onNavigateToActivity?: (eventId: string) => void; }) { const { user } = useAuth(); const orgId = portfolioListOrganizationId(user); const [viewMode, setViewMode] = useState("card"); const [search, setSearch] = useState(""); const [filterType, setFilterType] = useState("all"); const [filterStatus, setFilterStatus] = useState("all"); const [searchFocused, setSearchFocused] = useState(false); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [loadErr, setLoadErr] = useState(null); const [qnaPanelOpen, setQnaPanelOpen] = useState(false); const [quizPollPanelOpen, setQuizPollPanelOpen] = useState(false); const [quizPollStep, setQuizPollStep] = useState<"shell" | "form">("shell"); const [quizPollKind, setQuizPollKind] = useState<"quiz" | "poll">("poll"); const [shellName, setShellName] = useState(""); const [shellStart, setShellStart] = useState(""); const [shellEnd, setShellEnd] = useState(""); const [shellBusy, setShellBusy] = useState(false); const [shellError, setShellError] = useState(null); const [createdEventId, setCreatedEventId] = useState(null); const [createdSessionId, setCreatedSessionId] = useState(null); const [qnaShellBusy, setQnaShellBusy] = useState(false); const [qnaShellError, setQnaShellError] = useState(null); const [quizPollFormError, setQuizPollFormError] = useState(null); const bp = useBreakpoint(); const isMobile = bp === "mobile"; /** Card grid: 1 col on small screens, 4 equal columns in a row otherwise. */ const activityCardGridColumns = bp === "mobile" ? "1fr" : "repeat(4, minmax(0, 1fr))"; const fetchActivities = useCallback(async () => { if (!orgId) { setRows([]); setLoading(false); return; } setLoading(true); setLoadErr(null); const res = await eventsApi.list({ organization: orgId, recordKind: "activity", enrich: true, limit: 200, }); setLoading(false); if (res.error || !res.data?.events) { setLoadErr(res.error || "Could not load activities."); setRows([]); return; } setRows(res.data.events.map(eventToRow)); }, [orgId]); useEffect(() => { void fetchActivities(); }, [fetchActivities]); const openQnaPanel = () => { const { start, end } = defaultActivityShellDateTimes(); setShellName(""); setShellStart(start); setShellEnd(end); setQnaShellError(null); setQnaPanelOpen(true); }; const openQuizPollPanel = () => { const { start, end } = defaultActivityShellDateTimes(); setShellName(""); setShellStart(start); setShellEnd(end); setQuizPollKind("poll"); setShellError(null); setQuizPollStep("shell"); setCreatedEventId(null); setCreatedSessionId(null); setQuizPollPanelOpen(true); }; const closeQuizPollPanel = () => { setQuizPollPanelOpen(false); setQuizPollStep("shell"); setCreatedEventId(null); setCreatedSessionId(null); setShellError(null); setQuizPollFormError(null); }; const quizPollFormInitial = useMemo(() => { if (quizPollStep !== "form") return undefined; const base = defaultQuizPollModalForm(); base.title = shellName.trim(); base.status = "Draft"; base.displayMode = "always_sched"; base.scheduledStart = shellStart; base.scheduledEnd = shellEnd; return { form: base, questions: [] as Question[] }; }, [quizPollStep, shellName, shellStart, shellEnd]); const submitQnaShell = () => { void (async () => { if (!orgId) return; setQnaShellError(null); const name = shellName.trim(); if (name.length < 3) { setQnaShellError("Name must be at least 3 characters."); return; } setQnaShellBusy(true); const r = await createActivityShell(orgId, name, "qna", shellStart, shellEnd); setQnaShellBusy(false); if (!r.ok) { setQnaShellError(r.error); return; } setQnaPanelOpen(false); void fetchActivities(); onNavigateToActivity?.(r.eventId); })(); }; const continueQuizPollShell = () => { void (async () => { if (!orgId) return; setShellError(null); const name = shellName.trim(); if (name.length < 3) { setShellError("Name must be at least 3 characters."); return; } setShellBusy(true); const r = await createActivityShell(orgId, name, quizPollKind, shellStart, shellEnd); setShellBusy(false); if (!r.ok) { setShellError(r.error); return; } setCreatedEventId(r.eventId); setCreatedSessionId(r.sessionId); setQuizPollFormError(null); setQuizPollStep("form"); })(); }; const saveQuizPollFromActivities = (data: { form: QuizPollModalForm; questions: Question[] }) => { void (async () => { if (!createdEventId || !createdSessionId) return; const kind = quizPollKind; setQuizPollFormError(null); const payload = { session: createdSessionId, title: data.form.title, type: kind, description: data.form.description || undefined, questions: data.questions.map(q => mapQuestionForQuizPollApi(q, kind)), settings: buildQuizPollSettingsPayload(data.form, kind), }; const res = await quizPollsApi.create(createdEventId, payload); if (res.error) { setQuizPollFormError(res.error); return; } closeQuizPollPanel(); void fetchActivities(); onNavigateToActivity?.(createdEventId); })(); }; const filteredActivities = useMemo(() => { return rows.filter(a => { const matchesSearch = search === "" || a.name.toLowerCase().includes(search.toLowerCase()) || (a.subtitle?.toLowerCase().includes(search.toLowerCase()) ?? false); const matchesType = filterType === "all" || a.type === filterType; const matchesStatus = filterStatus === "all" || a.status === filterStatus; return matchesSearch && matchesType && matchesStatus; }); }, [rows, search, filterType, filterStatus]); const totals = useMemo(() => { const base = rows; const eventRating = weightedActivityRating( base.map((a) => ({ average: a.eventRatingAverage, count: a.eventRatingCount })), ); const sessionRating = weightedActivityRating( base.map((a) => ({ average: a.sessionRatingAverage, count: a.sessionRatingCount })), ); return { count: base.length, active: base.filter(a => a.status === "Active").length, participants: base.reduce((s, a) => s + a.participants, 0), responses: base.reduce((s, a) => s + a.responses, 0), eventRating, sessionRating, }; }, [rows]); return (

Activities

Manage Q&A sessions, quizzes, and polls (standalone activity events)

{!orgId && (

Set up an organization in your portfolio to view and create activities.

)} {loadErr && (

{loadErr}

)} {loading && orgId ? ( ) : null} {!loading && orgId ? ( <>
setSearch(e.target.value)} placeholder="Search activities…" onFocus={() => setSearchFocused(true)} onBlur={() => setSearchFocused(false)} style={{ background: "transparent", border: "none", outline: "none", fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)", width: "100%" }} /> {search ? ( ) : null}
{([ { mode: "card" as ViewMode, icon: , label: "Cards" }, { mode: "list" as ViewMode, icon: , label: "List" }, { mode: "calendar" as ViewMode, icon: , label: "Calendar" }, ] as const).map(v => ( ))}
Showing {filteredActivities.length} {filteredActivities.length === 1 ? "activity" : "activities"}
{viewMode === "card" && (
{filteredActivities.map(activity => ( onNavigateToActivity?.(activity.id)} /> ))}
)} {viewMode === "list" && (
{filteredActivities.map((activity, i) => ( onNavigateToActivity?.(activity.id)} /> ))}
)} {viewMode === "calendar" && (

Calendar view coming soon

)} {filteredActivities.length === 0 && !loading ? (

{rows.length === 0 ? "No activities yet. Create one to get started." : "No activities match your filters."}

) : null} ) : null} {qnaPanelOpen && orgId ? ( { setQnaPanelOpen(false); setQnaShellError(null); }} footer={(
)} >
setShellName(e.target.value)} disabled={qnaShellBusy} placeholder="e.g. Keynote Q&A" style={{ ...shellInputStyle, marginBottom: "16px" }} />
{qnaShellError ?

{qnaShellError}

: null}
) : null} {quizPollPanelOpen && orgId ? (
) : undefined} > {quizPollStep === "shell" ? (
setShellName(e.target.value)} disabled={shellBusy} placeholder="e.g. Audience pulse check" style={{ ...shellInputStyle, marginBottom: "16px" }} />
{shellError ?

{shellError}

: null}
) : createdEventId && quizPollFormInitial ? (
{quizPollFormError ? (
{quizPollFormError}
) : null}
) : null} ) : null}
); } function StatCard({ label, value, color, sublabel }: { label: string; value: string; color?: string; sublabel?: string }) { return (
{value}
{label}
{sublabel ? (
{sublabel}
) : null}
); } function ActivityCard({ activity, onClick }: { activity: ActivityRow; onClick: () => void }) { const typeConfig = TYPE_COLORS[activity.type]; const statusConfig = STATUS_BADGE[activity.status]; return (
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(); } }} style={{ background: "var(--card)", borderRadius: "var(--radius-card)", borderWidth: "1px", borderStyle: "solid", borderColor: "var(--border)", padding: "20px", cursor: "pointer", transition: "border-color 0.15s, box-shadow 0.15s" }} onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = "var(--primary)"; (e.currentTarget as HTMLElement).style.boxShadow = "0 4px 12px rgba(11,77,209,0.1)"; }} onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = "var(--border)"; (e.currentTarget as HTMLElement).style.boxShadow = "none"; }} >

{activity.name}

{activity.status}
{typeConfig.icon} {activity.type}
{activity.subtitle ? (

{activity.subtitle}

) : null}
{activity.participants}
{activity.questions}
{activity.responses}
{formatActivityRating(activity.eventRatingAverage, activity.eventRatingCount)}
{formatActivityRating(activity.sessionRatingAverage, activity.sessionRatingCount)}
); } function ActivityListRow({ activity, isLast, onClick }: { activity: ActivityRow; isLast: boolean; onClick: () => void }) { const typeConfig = TYPE_COLORS[activity.type]; const statusConfig = STATUS_BADGE[activity.status]; return (
{ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(); } }} style={{ display: "flex", alignItems: "center", gap: "16px", padding: "16px 20px", borderBottom: isLast ? "none" : "1px solid var(--border)", cursor: "pointer", transition: "background 0.15s" }} onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = "var(--secondary)"; }} onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = "transparent"; }} >
{typeConfig.icon}

{activity.name}

{activity.createdDate}

{activity.type}
{activity.participants} participants
Event {formatActivityRating(activity.eventRatingAverage, activity.eventRatingCount)}
Session {formatActivityRating(activity.sessionRatingAverage, activity.sessionRatingCount)}
{activity.status}
); }