EventBuzz_Console/src/app/components/activities/ActivitiesPage.tsx

869 lines
40 KiB
TypeScript

/**
* 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<ActivityUiType, { bg: string; fg: string; icon: React.ReactNode }> = {
"Q&A Session": { bg: "#E8EFFC", fg: "#0B4DD1", icon: <MessageCircle size={14} /> },
"Quiz": { bg: "#FEF3C7", fg: "#D97706", icon: <HelpCircle size={14} /> },
"Poll": { bg: "#ECFDF5", fg: "#059669", icon: <BarChart3 size={14} /> },
};
const STATUS_BADGE: Record<ActivityStatusUi, { bg: string; color: string }> = {
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 (
<>
<div onClick={onClose} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", zIndex: 200 }} role="presentation" />
<div
style={{
position: "fixed",
top: 0,
right: 0,
height: "100%",
width: "min(720px, 100vw)",
background: "var(--card)",
zIndex: 201,
display: "flex",
flexDirection: "column",
boxShadow: "-4px 0 32px rgba(0,0,0,0.18)",
animation: "activityPanelIn 0.25s ease",
}}
>
<style>{`@keyframes activityPanelIn { from { transform: translateX(100%); } to { transform: translateX(0); } }`}</style>
<div style={{ padding: "16px 20px", borderBottom: "1px solid var(--border)", flexShrink: 0, display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "12px" }}>
<div>
<h2 style={{ fontFamily: "var(--font-inter)", fontSize: "var(--text-lg)", fontWeight: "var(--font-weight-medium)", color: "var(--foreground)", margin: 0 }}>{title}</h2>
{subtitle ? (
<p style={{ fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", margin: "6px 0 0", lineHeight: 1.45 }}>{subtitle}</p>
) : null}
</div>
<button type="button" onClick={onClose} style={{ background: "none", border: "none", cursor: "pointer", color: "var(--muted)", padding: 4 }}>
<X size={20} />
</button>
</div>
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", overflow: "hidden" }}>
{children}
</div>
{footer ? (
<div style={{ padding: "14px 20px", borderTop: "1px solid var(--border)", flexShrink: 0 }}>{footer}</div>
) : null}
</div>
</>
);
}
export function ActivitiesPage({
onNavigateToActivity,
}: {
onNavigateToActivity?: (eventId: string) => void;
}) {
const { user } = useAuth();
const orgId = portfolioListOrganizationId(user);
const [viewMode, setViewMode] = useState<ViewMode>("card");
const [search, setSearch] = useState("");
const [filterType, setFilterType] = useState<string>("all");
const [filterStatus, setFilterStatus] = useState<string>("all");
const [searchFocused, setSearchFocused] = useState(false);
const [rows, setRows] = useState<ActivityRow[]>([]);
const [loading, setLoading] = useState(true);
const [loadErr, setLoadErr] = useState<string | null>(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<string | null>(null);
const [createdEventId, setCreatedEventId] = useState<string | null>(null);
const [createdSessionId, setCreatedSessionId] = useState<string | null>(null);
const [qnaShellBusy, setQnaShellBusy] = useState(false);
const [qnaShellError, setQnaShellError] = useState<string | null>(null);
const [quizPollFormError, setQuizPollFormError] = useState<string | null>(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 (
<div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "12px", flexWrap: "wrap" }}>
<div>
<h1 style={{ fontFamily: "var(--font-inter)", fontSize: isMobile ? "var(--text-xl)" : "var(--text-2xl)", fontWeight: "var(--font-weight-medium)", color: "var(--foreground)", margin: "0 0 6px" }}>
Activities
</h1>
<p style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", margin: 0 }}>
Manage Q&A sessions, quizzes, and polls (standalone activity events)
</p>
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px", alignItems: "center" }}>
<button
type="button"
onClick={() => orgId && openQnaPanel()}
disabled={!orgId}
style={{
display: "flex",
alignItems: "center",
gap: "6px",
padding: "7px 16px",
borderRadius: "var(--radius-button)",
background: orgId ? "var(--primary)" : "var(--muted)",
color: "var(--primary-foreground)",
border: "none",
cursor: orgId ? "pointer" : "not-allowed",
fontSize: "var(--text-sm)",
fontFamily: "var(--font-noto-sans)",
fontWeight: "var(--font-weight-medium)",
opacity: orgId ? 1 : 0.7,
}}
>
<MessageCircle size={14} /> Create Q&amp;A Session
</button>
<button
type="button"
onClick={() => orgId && openQuizPollPanel()}
disabled={!orgId}
style={{
display: "flex",
alignItems: "center",
gap: "6px",
padding: "7px 16px",
borderRadius: "var(--radius-button)",
background: orgId ? "var(--card)" : "var(--muted)",
color: orgId ? "var(--foreground)" : "var(--muted-foreground)",
border: "1px solid var(--border)",
cursor: orgId ? "pointer" : "not-allowed",
fontSize: "var(--text-sm)",
fontFamily: "var(--font-noto-sans)",
fontWeight: "var(--font-weight-medium)",
}}
>
<BarChart3 size={14} /> Create Quiz/Poll
</button>
</div>
</div>
{!orgId && (
<p style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", margin: 0 }}>
Set up an organization in your portfolio to view and create activities.
</p>
)}
{loadErr && (
<p style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--destructive)", margin: 0 }}>{loadErr}</p>
)}
{loading && orgId ? (
<LoadingState message="Loading activities…" />
) : null}
{!loading && orgId ? (
<>
<div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr 1fr" : "repeat(3, 1fr)", gap: "12px" }}>
<StatCard label="Total Activities" value={String(totals.count)} />
<StatCard label="Active" value={String(totals.active)} color="#15803d" />
<StatCard label="Total Participants" value={String(totals.participants)} />
<StatCard label="Total Responses" value={String(totals.responses)} />
<StatCard
label="Event Rating"
value={formatActivityRating(totals.eventRating.average, totals.eventRating.count)}
sublabel={totals.eventRating.count ? `${totals.eventRating.count} ratings` : "No ratings yet"}
color="#d97706"
/>
<StatCard
label="Session Rating"
value={formatActivityRating(totals.sessionRating.average, totals.sessionRating.count)}
sublabel={totals.sessionRating.count ? `${totals.sessionRating.count} ratings` : "No ratings yet"}
color="#d97706"
/>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" }}>
<div style={{ display: "flex", alignItems: "center", gap: "7px", padding: "0 10px", height: "34px", flex: isMobile ? "1 1 100%" : "1 1 180px", background: "var(--card)", borderTopWidth: "1px", borderRightWidth: "1px", borderBottomWidth: "1px", borderLeftWidth: "1px", borderTopStyle: "solid", borderRightStyle: "solid", borderBottomStyle: "solid", borderLeftStyle: "solid", borderTopColor: searchFocused ? "var(--ring)" : "var(--border)", borderRightColor: searchFocused ? "var(--ring)" : "var(--border)", borderBottomColor: searchFocused ? "var(--ring)" : "var(--border)", borderLeftColor: searchFocused ? "var(--ring)" : "var(--border)", borderRadius: "var(--radius-button)", transition: "border-top-color 0.15s, border-right-color 0.15s, border-bottom-color 0.15s, border-left-color 0.15s" }}>
<Search size={13} style={{ color: "var(--muted)", flexShrink: 0 }} />
<input
value={search}
onChange={e => 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 ? (
<button type="button" onClick={() => setSearch("")} style={{ background: "none", border: "none", cursor: "pointer", color: "var(--muted)", padding: 0, display: "flex" }}>
<X size={12} />
</button>
) : null}
</div>
<select
value={filterType}
onChange={e => setFilterType(e.target.value)}
style={{ padding: "0 32px 0 10px", height: "34px", borderRadius: "var(--radius-button)", borderWidth: "1px", borderStyle: "solid", borderColor: "var(--border)", background: "var(--card)", fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)", cursor: "pointer", outline: "none" }}
>
<option value="all">All Types</option>
<option value="Q&A Session">Q&A Session</option>
<option value="Quiz">Quiz</option>
<option value="Poll">Poll</option>
</select>
<select
value={filterStatus}
onChange={e => setFilterStatus(e.target.value)}
style={{ padding: "0 32px 0 10px", height: "34px", borderRadius: "var(--radius-button)", borderWidth: "1px", borderStyle: "solid", borderColor: "var(--border)", background: "var(--card)", fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)", cursor: "pointer", outline: "none" }}
>
<option value="all">All Status</option>
<option value="Active">Active</option>
<option value="Scheduled">Scheduled</option>
<option value="Completed">Completed</option>
<option value="Draft">Draft</option>
</select>
<div style={{ flex: 1 }} />
<div style={{ display: "flex", gap: "4px", borderRadius: "var(--radius-button)", background: "var(--secondary)", padding: "3px" }}>
{([
{ mode: "card" as ViewMode, icon: <LayoutGrid size={14} />, label: "Cards" },
{ mode: "list" as ViewMode, icon: <List size={14} />, label: "List" },
{ mode: "calendar" as ViewMode, icon: <CalIcon size={14} />, label: "Calendar" },
] as const).map(v => (
<button
key={v.mode}
type="button"
onClick={() => setViewMode(v.mode)}
title={v.label}
style={{ display: "flex", alignItems: "center", justifyContent: "center", width: "32px", height: "28px", borderRadius: "var(--radius-button)", background: viewMode === v.mode ? "var(--card)" : "transparent", color: viewMode === v.mode ? "var(--primary)" : "var(--muted)", border: "none", cursor: "pointer", transition: "all 0.15s" }}
>
{v.icon}
</button>
))}
</div>
</div>
<div style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)" }}>
Showing {filteredActivities.length} {filteredActivities.length === 1 ? "activity" : "activities"}
</div>
{viewMode === "card" && (
<div style={{ display: "grid", gridTemplateColumns: activityCardGridColumns, gap: "16px" }}>
{filteredActivities.map(activity => (
<ActivityCard
key={activity.id}
activity={activity}
onClick={() => onNavigateToActivity?.(activity.id)}
/>
))}
</div>
)}
{viewMode === "list" && (
<div style={{ background: "var(--card)", borderRadius: "var(--radius-card)", borderWidth: "1px", borderStyle: "solid", borderColor: "var(--border)", overflow: "hidden" }}>
{filteredActivities.map((activity, i) => (
<ActivityListRow
key={activity.id}
activity={activity}
isLast={i === filteredActivities.length - 1}
onClick={() => onNavigateToActivity?.(activity.id)}
/>
))}
</div>
)}
{viewMode === "calendar" && (
<div style={{ padding: "60px 20px", textAlign: "center", background: "var(--card)", borderRadius: "var(--radius-card)", borderWidth: "1px", borderStyle: "solid", borderColor: "var(--border)" }}>
<CalIcon size={32} style={{ color: "var(--muted)", margin: "0 auto 16px", display: "block" }} />
<p style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)" }}>
Calendar view coming soon
</p>
</div>
)}
{filteredActivities.length === 0 && !loading ? (
<div style={{ padding: "60px 20px", textAlign: "center", background: "var(--card)", borderRadius: "var(--radius-card)", borderWidth: "1px", borderStyle: "solid", borderColor: "var(--border)" }}>
<Search size={32} style={{ color: "var(--muted)", margin: "0 auto 16px", display: "block" }} />
<p style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)" }}>
{rows.length === 0 ? "No activities yet. Create one to get started." : "No activities match your filters."}
</p>
</div>
) : null}
</>
) : null}
{qnaPanelOpen && orgId ? (
<ActivitySidePanel
title="Create Q&amp;A Session"
subtitle="Name is used for the event and the session. Track: Default."
onClose={() => { setQnaPanelOpen(false); setQnaShellError(null); }}
footer={(
<div style={{ display: "flex", justifyContent: "flex-end", gap: "10px" }}>
<button type="button" onClick={() => { setQnaPanelOpen(false); setQnaShellError(null); }} disabled={qnaShellBusy} style={{ padding: "8px 16px", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", background: "var(--card)", cursor: qnaShellBusy ? "not-allowed" : "pointer", fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)" }}>
Cancel
</button>
<button type="button" onClick={submitQnaShell} disabled={qnaShellBusy} style={{ padding: "8px 16px", borderRadius: "var(--radius-button)", border: "none", background: qnaShellBusy ? "var(--muted)" : "var(--primary)", color: "var(--primary-foreground)", cursor: qnaShellBusy ? "not-allowed" : "pointer", fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", fontWeight: "var(--font-weight-medium)" }}>
{qnaShellBusy ? "Creating…" : "Create"}
</button>
</div>
)}
>
<div style={{ padding: "20px", overflowY: "auto" }}>
<label style={{ display: "block", fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", marginBottom: "6px" }}>Name of activity</label>
<input
value={shellName}
onChange={e => setShellName(e.target.value)}
disabled={qnaShellBusy}
placeholder="e.g. Keynote Q&amp;A"
style={{ ...shellInputStyle, marginBottom: "16px" }}
/>
<div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "1fr 1fr", gap: "16px" }}>
<div>
<label style={{ display: "block", fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", marginBottom: "6px" }}>Start</label>
<FlatpickrDatetimeInput value={shellStart} onChange={setShellStart} style={shellInputStyle} disabled={qnaShellBusy} />
</div>
<div>
<label style={{ display: "block", fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", marginBottom: "6px" }}>End</label>
<FlatpickrDatetimeInput value={shellEnd} onChange={setShellEnd} style={shellInputStyle} disabled={qnaShellBusy} />
</div>
</div>
{qnaShellError ? <p style={{ fontSize: "var(--text-sm)", color: "var(--destructive)", marginTop: "14px", marginBottom: 0 }}>{qnaShellError}</p> : null}
</div>
</ActivitySidePanel>
) : null}
{quizPollPanelOpen && orgId ? (
<ActivitySidePanel
title={quizPollStep === "shell" ? "Create Quiz / Poll" : (quizPollKind === "quiz" ? "New quiz" : "New poll")}
subtitle={quizPollStep === "shell" ? "Step 1: name and schedule (event & session; track: Default). Step 2: same form as Quiz & Polls in the event dashboard." : undefined}
onClose={closeQuizPollPanel}
footer={quizPollStep === "shell" ? (
<div style={{ display: "flex", justifyContent: "flex-end", gap: "10px" }}>
<button type="button" onClick={closeQuizPollPanel} disabled={shellBusy} style={{ padding: "8px 16px", borderRadius: "var(--radius-button)", border: "1px solid var(--border)", background: "var(--card)", cursor: shellBusy ? "not-allowed" : "pointer", fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)" }}>
Cancel
</button>
<button type="button" onClick={continueQuizPollShell} disabled={shellBusy} style={{ padding: "8px 16px", borderRadius: "var(--radius-button)", border: "none", background: shellBusy ? "var(--muted)" : "var(--primary)", color: "var(--primary-foreground)", cursor: shellBusy ? "not-allowed" : "pointer", fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", fontWeight: "var(--font-weight-medium)" }}>
{shellBusy ? "Creating…" : "Continue to form"}
</button>
</div>
) : undefined}
>
{quizPollStep === "shell" ? (
<div style={{ padding: "20px", overflowY: "auto" }}>
<label style={{ display: "block", fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", marginBottom: "6px" }}>Name of activity</label>
<input
value={shellName}
onChange={e => setShellName(e.target.value)}
disabled={shellBusy}
placeholder="e.g. Audience pulse check"
style={{ ...shellInputStyle, marginBottom: "16px" }}
/>
<label style={{ display: "block", fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", marginBottom: "6px" }}>Type</label>
<select
value={quizPollKind}
onChange={e => setQuizPollKind(e.target.value as "quiz" | "poll")}
disabled={shellBusy}
style={{ ...shellInputStyle, marginBottom: "16px", cursor: shellBusy ? "not-allowed" : "pointer" }}
>
<option value="poll">Poll</option>
<option value="quiz">Quiz</option>
</select>
<div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "1fr 1fr", gap: "16px" }}>
<div>
<label style={{ display: "block", fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", marginBottom: "6px" }}>Start</label>
<FlatpickrDatetimeInput value={shellStart} onChange={setShellStart} style={shellInputStyle} disabled={shellBusy} />
</div>
<div>
<label style={{ display: "block", fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", marginBottom: "6px" }}>End</label>
<FlatpickrDatetimeInput value={shellEnd} onChange={setShellEnd} style={shellInputStyle} disabled={shellBusy} />
</div>
</div>
{shellError ? <p style={{ fontSize: "var(--text-sm)", color: "var(--destructive)", marginTop: "14px", marginBottom: 0 }}>{shellError}</p> : null}
</div>
) : createdEventId && quizPollFormInitial ? (
<div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }}>
{quizPollFormError ? (
<div style={{ padding: "10px 20px", borderBottom: "1px solid var(--border)", background: "var(--secondary)", color: "var(--destructive)", fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)" }}>
{quizPollFormError}
</div>
) : null}
<AddQuizPollModal
key={`${createdEventId}-${quizPollKind}`}
variant="panel"
kind={quizPollKind}
eventId={createdEventId}
initial={quizPollFormInitial}
onClose={closeQuizPollPanel}
onSave={saveQuizPollFromActivities}
/>
</div>
) : null}
</ActivitySidePanel>
) : null}
</div>
);
}
function StatCard({ label, value, color, sublabel }: { label: string; value: string; color?: string; sublabel?: string }) {
return (
<div style={{ background: "var(--card)", borderRadius: "var(--radius-card)", borderWidth: "1px", borderStyle: "solid", borderColor: "var(--border)", padding: "16px 20px" }}>
<div style={{ fontSize: "var(--text-2xl)", fontFamily: "var(--font-inter)", fontWeight: "var(--font-weight-medium)", color: color || "var(--foreground)", marginBottom: "4px" }}>
{value}
</div>
<div style={{ fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", textTransform: "uppercase", letterSpacing: "0.5px" }}>
{label}
</div>
{sublabel ? (
<div style={{ fontSize: "10px", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", marginTop: "6px" }}>
{sublabel}
</div>
) : null}
</div>
);
}
function ActivityCard({ activity, onClick }: { activity: ActivityRow; onClick: () => void }) {
const typeConfig = TYPE_COLORS[activity.type];
const statusConfig = STATUS_BADGE[activity.status];
return (
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={e => { 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"; }}
>
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "12px", marginBottom: "12px" }}>
<h3 style={{ fontSize: "var(--text-base)", fontFamily: "var(--font-inter)", fontWeight: "var(--font-weight-medium)", color: "var(--foreground)", margin: 0, flex: 1 }}>
{activity.name}
</h3>
<span style={{ padding: "3px 10px", borderRadius: "999px", background: statusConfig.bg, color: statusConfig.color, fontSize: "10px", fontFamily: "var(--font-noto-sans)", fontWeight: "600", flexShrink: 0, textTransform: "uppercase", letterSpacing: "0.3px" }}>
{activity.status}
</span>
</div>
<div style={{ display: "inline-flex", alignItems: "center", gap: "6px", padding: "4px 10px", borderRadius: "var(--radius-button)", background: typeConfig.bg, marginBottom: "16px" }}>
{typeConfig.icon}
<span style={{ fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", fontWeight: "var(--font-weight-medium)", color: typeConfig.fg }}>
{activity.type}
</span>
</div>
{activity.subtitle ? (
<p style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", margin: "0 0 16px", lineHeight: 1.4 }}>
{activity.subtitle}
</p>
) : null}
<div style={{ display: "flex", gap: "16px", paddingTop: "12px", borderTop: "1px solid var(--border)", flexWrap: "wrap" }}>
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<Users size={14} style={{ color: "var(--muted)" }} />
<span style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)" }}>{activity.participants}</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<HelpCircle size={14} style={{ color: "var(--muted)" }} />
<span style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)" }}>{activity.questions}</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
<CheckCircle2 size={14} style={{ color: "var(--muted)" }} />
<span style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)" }}>{activity.responses}</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "6px" }} title="Event rating">
<Star size={14} style={{ color: "#d97706" }} />
<span style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)" }}>
{formatActivityRating(activity.eventRatingAverage, activity.eventRatingCount)}
</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "6px" }} title="Session rating">
<Star size={14} style={{ color: "#d97706", opacity: 0.75 }} />
<span style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)" }}>
{formatActivityRating(activity.sessionRatingAverage, activity.sessionRatingCount)}
</span>
</div>
</div>
</div>
);
}
function ActivityListRow({ activity, isLast, onClick }: { activity: ActivityRow; isLast: boolean; onClick: () => void }) {
const typeConfig = TYPE_COLORS[activity.type];
const statusConfig = STATUS_BADGE[activity.status];
return (
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={e => { 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"; }}
>
<div style={{ width: "36px", height: "36px", borderRadius: "var(--radius-button)", background: typeConfig.bg, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, color: typeConfig.fg }}>
{typeConfig.icon}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<h4 style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-inter)", fontWeight: "var(--font-weight-medium)", color: "var(--foreground)", margin: "0 0 3px" }}>
{activity.name}
</h4>
<p style={{ fontSize: "var(--text-xs)", fontFamily: "var(--font-noto-sans)", color: "var(--muted)", margin: 0 }}>
{activity.createdDate}
</p>
</div>
<div style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)", minWidth: "100px" }}>
{activity.type}
</div>
<div style={{ display: "flex", gap: "12px", minWidth: "120px", flexWrap: "wrap" }}>
<div style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)" }}>
{activity.participants} participants
</div>
<div style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)" }}>
Event {formatActivityRating(activity.eventRatingAverage, activity.eventRatingCount)}
</div>
<div style={{ fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)" }}>
Session {formatActivityRating(activity.sessionRatingAverage, activity.sessionRatingCount)}
</div>
</div>
<span style={{ padding: "3px 10px", borderRadius: "999px", background: statusConfig.bg, color: statusConfig.color, fontSize: "10px", fontFamily: "var(--font-noto-sans)", fontWeight: "600", flexShrink: 0, textTransform: "uppercase" }}>
{activity.status}
</span>
</div>
);
}