This commit is contained in:
Ravi Gulhane 2026-08-17 13:17:19 +05:30
commit 432842ee13
40 changed files with 1258 additions and 664 deletions

View File

@ -26,9 +26,14 @@ import { AiAvatarPage } from "./pages/AiAvatarPage";
import { CodeEntryPage } from "./pages/CodeEntryPage"; import { CodeEntryPage } from "./pages/CodeEntryPage";
import { PrivacyPolicyPage } from "./pages/PrivacyPolicyPage"; import { PrivacyPolicyPage } from "./pages/PrivacyPolicyPage";
import { TermsOfServicePage } from "./pages/TermsOfServicePage"; import { TermsOfServicePage } from "./pages/TermsOfServicePage";
import { useEventTheme } from "./hooks/useEventTheme";
import { ThemeGate } from "./components/ThemeGate";
export default function App() { export default function App() {
const { ready: themeReady } = useEventTheme();
return ( return (
<ThemeGate ready={themeReady}>
<div className="flex min-h-dvh flex-1 flex-col"> <div className="flex min-h-dvh flex-1 flex-col">
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
@ -62,5 +67,6 @@ export default function App() {
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
</div> </div>
</ThemeGate>
); );
} }

View File

@ -13,17 +13,17 @@ export type AgendaRowData = {
function SpeakerLineView({ name, role }: SpeakerLine) { function SpeakerLineView({ name, role }: SpeakerLine) {
const pill = const pill =
role === "Speaker" ? "bg-[#f0fbd7]" : "bg-[#fbefd7]"; role === "Speaker" ? "bg-emerald-50" : "bg-yellow-100";
return ( return (
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-[#ededed]"> <div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-zinc-100">
<User className="size-3 text-[#24282f]/70" strokeWidth={2} /> <User className="size-3 text-gray-600" strokeWidth={2} />
</div> </div>
<span className="text-xs font-medium tracking-wide text-[#24282f]">{name}</span> <span className="text-xs font-medium tracking-wide text-[var(--theme-card-text-color)]">{name}</span>
</div> </div>
<span <span
className={`rounded px-2 py-0.5 text-[10px] font-normal tracking-wide text-[#24282f] ${pill}`} className={`rounded px-2 py-0.5 text-[10px] font-normal tracking-wide ${pill}`}
> >
{role} {role}
</span> </span>
@ -39,37 +39,36 @@ export function AgendaRow({ row }: AgendaRowProps) {
return ( return (
<div className="relative flex gap-2 pb-3"> <div className="relative flex gap-2 pb-3">
<div className="relative z-[1] flex shrink-0 flex-col items-center"> <div className="relative z-[1] flex shrink-0 flex-col items-center">
<div className="flex size-10 items-center justify-center rounded-full bg-[#c2deff]"> <div className="flex size-10 items-center justify-center rounded-full bg-[var(--theme-button-color)]/15">
<User className="size-4 text-[#24282f]/80" strokeWidth={2} /> <User className="size-4 text-[var(--theme-card-text-color)]" strokeWidth={2} />
</div> </div>
</div> </div>
<div className="flex min-w-0 flex-1 flex-col gap-2"> <div className="flex min-w-0 flex-1 flex-col gap-2">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 flex-col gap-1"> <div className="flex min-w-0 flex-col gap-1 text-[var(--theme-card-text-color)]">
<div className="flex items-center gap-1 text-[10px] tracking-wide text-[#24282f]"> <div className="flex items-center gap-1 text-[10px] tracking-wide">
<span>{row.time}</span> <span>{row.time}</span>
<span className="h-3 w-px bg-[rgba(36,40,47,0.5)]" /> <span className="h-3 w-px bg-zinc-300" />
<span>{row.duration}</span> <span>{row.duration}</span>
</div> </div>
<h3 className="text-sm font-semibold leading-[22px] tracking-wide text-[#24282f]"> <h3 className="text-sm font-semibold leading-[22px] tracking-wide">
{row.trackTitle} {row.trackTitle}
</h3> </h3>
<div <div
className="inline-flex w-fit items-center rounded px-2 py-0.5 text-[10px] tracking-wide text-[#24282f]" className="inline-flex w-fit items-center rounded px-2 py-0.5 text-[10px] tracking-wide bg-zinc-200 text-[var(--theme-card-text-color)]"
style={{ backgroundColor: "rgba(94, 147, 239, 0.14)" }}
> >
{row.sessionType} {row.sessionType}
</div> </div>
</div> </div>
<button <button
type="button" type="button"
className="shrink-0 text-[#1d66ef]" className="shrink-0 text-[var(--theme-button-color)]"
aria-label="Bookmark session" aria-label="Bookmark session"
> >
<Bookmark className="size-5" strokeWidth={1.75} /> <Bookmark className="size-5" strokeWidth={1.75} />
</button> </button>
</div> </div>
<p className="text-xs leading-normal tracking-wide text-[rgba(36,40,47,0.87)]"> <p className="text-xs leading-normal tracking-wide text-[var(--theme-card-text-color)]/80">
{row.description} {row.description}
</p> </p>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">

View File

@ -35,39 +35,39 @@ export type AgendaRowTrackV2Data = {
export function AgendaRowTrackV2({ row }: { row: AgendaRowTrackV2Data }) { export function AgendaRowTrackV2({ row }: { row: AgendaRowTrackV2Data }) {
const [detailsOpen, setDetailsOpen] = useState(false); const [detailsOpen, setDetailsOpen] = useState(false);
const joinBtnClass = row.joinLive const joinBtnClass = row.joinLive
? "inline-flex h-8 w-[84px] items-center justify-between rounded-full bg-[#e67e00] pl-[18px] pr-2 text-xs font-semibold uppercase text-white shadow-[0_0_0_2px_rgba(239,141,29,0.5),0_4px_14px_rgba(239,141,29,0.45)] ring-2 ring-[#ffb347]/40" ? "inline-flex h-8 w-[84px] items-center justify-between rounded-full theme-btn-bg pl-[18px] pr-2 text-xs font-semibold uppercase text-white shadow-sm ring-2 ring-[var(--theme-button-color)]/40"
: "inline-flex h-8 w-[84px] items-center justify-between rounded-full bg-[#ef8d1d] pl-[18px] pr-2 text-xs font-semibold uppercase text-white shadow-[0_4px_4px_rgba(0,0,0,0.15)]"; : "inline-flex h-8 w-[84px] items-center justify-between rounded-full theme-btn-bg transition-colors duration-300 ease-in-out hover:bg-[var(--theme-button-color)]/90 pl-[18px] pr-2 text-xs font-semibold uppercase text-white shadow-sm";
return ( return (
<div className="relative w-full pb-4"> <div className="relative w-full pb-4">
<div className="flex min-w-0 w-full flex-col gap-2"> <div className="flex min-w-0 w-full flex-col gap-2">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2 text-[var(--theme-card-text-color)]">
<div className="flex min-w-0 flex-col gap-1"> <div className="flex min-w-0 flex-col gap-1">
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1"> <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
<span <span
className="inline-flex shrink-0 rounded px-2 py-0.5 text-[10px] text-[#24282f]" className="inline-flex shrink-0 rounded px-2 py-0.5 text-[10px] bg-zinc-200 text-gray-600"
style={{ backgroundColor: "rgba(94, 147, 239, 0.14)" }}
> >
{row.sessionType} {row.sessionType}
</span> </span>
<div className="flex min-w-0 flex-wrap items-center gap-1 text-[10px] tracking-wide text-[#24282f]"> <div className="flex min-w-0 flex-wrap items-center gap-1 text-[10px] tracking-wide">
{row.sessionDateLabel ? ( {row.sessionDateLabel ? (
<> <>
<span className="shrink-0">{row.sessionDateLabel}</span> <span className="shrink-0">{row.sessionDateLabel}</span>
<span className="h-3 w-px shrink-0 bg-[rgba(36,40,47,0.5)]" /> <span className="h-3 w-px shrink-0 bg-[var(--theme-card-text-color)]/50" />
</> </>
) : null} ) : null}
<span className="shrink-0">{row.time}</span> <span className="shrink-0">{row.time}</span>
<span className="h-3 w-px shrink-0 bg-[rgba(36,40,47,0.5)]" /> <span className="h-3 w-px shrink-0 bg-[var(--theme-card-text-color)]/50" />
<span className="shrink-0">{row.duration}</span> <span className="shrink-0">{row.duration}</span>
</div> </div>
</div> </div>
<h3 className="text-sm font-semibold leading-[22px] text-[#24282f]"> <h3 className="text-sm font-semibold leading-[22px]">
{row.trackTitle} {row.trackTitle}
</h3> </h3>
{row.trackLocation?.trim() ? ( {row.trackLocation?.trim() ? (
<div className="flex items-start gap-1.5 text-xs text-[rgba(36,40,47,0.75)]"> <div className="flex items-start gap-1.5 text-xs text-[var(--theme-card-text-color)]/80">
<MapPin className="mt-0.5 size-3.5 shrink-0 text-[#1e72ef]" strokeWidth={1.75} /> <MapPin className="mt-0.5 size-3.5 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="leading-snug">{row.trackLocation.trim()}</span> <span className="leading-snug">{row.trackLocation.trim()}</span>
</div> </div>
) : null} ) : null}
@ -93,7 +93,7 @@ export function AgendaRowTrackV2({ row }: { row: AgendaRowTrackV2Data }) {
e.stopPropagation(); e.stopPropagation();
row.bookmark!.onToggle(); row.bookmark!.onToggle();
}} }}
className="rounded-md p-0.5 text-[#1d66ef] hover:bg-[#1d66ef]/10" className="rounded-md p-0.5 text-[var(--theme-button-color)]"
aria-label={row.bookmark.isBookmarked ? "Remove bookmark" : "Bookmark session"} aria-label={row.bookmark.isBookmarked ? "Remove bookmark" : "Bookmark session"}
aria-pressed={row.bookmark.isBookmarked} aria-pressed={row.bookmark.isBookmarked}
> >
@ -111,7 +111,7 @@ export function AgendaRowTrackV2({ row }: { row: AgendaRowTrackV2Data }) {
e.stopPropagation(); e.stopPropagation();
setDetailsOpen((o) => !o); setDetailsOpen((o) => !o);
}} }}
className="mt-[18px] rounded px-1 py-1 text-[#8e8e93] hover:bg-black/5 hover:text-[#24282f]" className="mt-[18px] rounded px-1 py-1 text-[var(--theme-card-text-color)]/80 hover:bg-black/5 hover:text-[var(--theme-card-text-color)]]"
aria-expanded={detailsOpen} aria-expanded={detailsOpen}
aria-label={detailsOpen ? "Hide session details" : "Show session details"} aria-label={detailsOpen ? "Hide session details" : "Show session details"}
> >
@ -123,15 +123,15 @@ export function AgendaRowTrackV2({ row }: { row: AgendaRowTrackV2Data }) {
</div> </div>
</div> </div>
{detailsOpen ? ( {detailsOpen ? (
<div className="flex min-w-0 flex-col gap-2"> <div className="flex min-w-0 flex-col gap-2 text-[var(--theme-card-text-color)]/80">
{row.description?.trim() ? ( {row.description?.trim() ? (
<p className="text-xs leading-normal text-[rgba(36,40,47,0.87)]">{row.description}</p> <p className="text-xs leading-normal">{row.description}</p>
) : null} ) : null}
<div className="flex min-w-0 items-center gap-1.5"> <div className="flex min-w-0 items-center gap-1.5">
<span className="shrink-0 rounded bg-[#f0fbd7] px-2 py-0.5 text-[10px] text-[#24282f]"> <span className="shrink-0 rounded bg-green-100 text-green-800 px-2 py-0.5 text-[10px]">
Speaker Speaker
</span> </span>
<p className="min-w-0 flex-1 text-xs font-medium leading-snug text-[#24282f] line-clamp-1"> <p className="min-w-0 flex-1 text-xs font-medium leading-snug line-clamp-1">
{row.speakerNames.join(", ")} {row.speakerNames.join(", ")}
</p> </p>
</div> </div>

View File

@ -79,20 +79,20 @@ export function AskQuestionModal({
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby={`ask-question-title${idSuffix}`} aria-labelledby={`ask-question-title${idSuffix}`}
className="relative z-10 w-full max-w-[min(100%,402px)] rounded-t-2xl bg-white px-5 pb-8 pt-5 shadow-[0_-8px_32px_rgba(0,0,0,0.12)] sm:mx-4 sm:rounded-2xl sm:shadow-xl" className="relative z-10 w-full max-w-[min(100%,402px)] rounded-t-2xl card-bg px-5 pb-8 pt-5 shadow-md sm:mx-4 sm:rounded-2xl sm:shadow-xl"
> >
<div className="mb-4 flex items-start justify-between gap-3"> <div className="mb-4 flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<h3 id={`ask-question-title${idSuffix}`} className="text-lg font-semibold text-[#24282f]"> <h3 id={`ask-question-title${idSuffix}`} className="text-lg font-semibold text-[var(--theme-card-text-color)]">
Ask a Question Ask a Question
</h3> </h3>
<p className="mt-1 text-sm leading-snug text-[rgba(36,40,47,0.65)]"> <p className="mt-2 text-sm leading-snug text-[var(--theme-card-text-color)]/80">
Submit your question to the session moderator. Submit your question to the session moderator.
</p> </p>
</div> </div>
<button <button
type="button" type="button"
className="shrink-0 rounded-full p-1.5 text-[#24282f] hover:bg-[#f2f2f7]" className="shrink-0 rounded-full p-1.5 text-[var(--theme-card-text-color)] transition-colors duration-300 ease-in-out"
aria-label="Close" aria-label="Close"
onClick={close} onClick={close}
> >
@ -110,13 +110,13 @@ export function AskQuestionModal({
placeholder="Type your question here..." placeholder="Type your question here..."
rows={5} rows={5}
disabled={submitting} disabled={submitting}
className="min-h-[120px] w-full resize-y rounded-xl border border-[#c7c7cc] bg-[#f2f2f7]/40 px-3 py-3 text-sm text-[#24282f] placeholder:text-[rgba(36,40,47,0.45)] focus:border-[#ef8d1d] focus:outline-none focus:ring-2 focus:ring-[#ef8d1d]/30 disabled:opacity-60" className="min-h-[120px] w-full resize-y rounded-xl border border-[var(--theme-card-text-color)]/10 bg-[var(--theme-card-color)]/40 px-3 py-3 text-sm text-[var(--theme-card-text-color)] placeholder:text-[var(--theme-card-text-color)]/60 transition-all duration-300 ease-in-out focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[var(--theme-button-color)]/30 disabled:opacity-60"
/> />
{submitError ? <p className="text-sm text-red-600">{submitError}</p> : null} {submitError ? <p className="text-sm text-red-600">{submitError}</p> : null}
<button <button
type="submit" type="submit"
disabled={submitting} disabled={submitting}
className="w-full rounded-xl bg-[#ef8d1d] py-3 text-sm font-semibold text-white shadow-[0_4px_4px_rgba(0,0,0,0.12)] transition hover:bg-[#e0800c] active:scale-[0.99] disabled:opacity-60" className="w-full rounded-lg theme-btn-bg py-3 text-sm font-semibold text-white shadow-md transition-all duration-300 ease-in-out transition hover:bg-[var(--theme-button-color)]/80 disabled:opacity-60"
> >
{submitting ? "Submitting…" : "Submit Question"} {submitting ? "Submitting…" : "Submit Question"}
</button> </button>

View File

@ -53,7 +53,7 @@ export function AudienceQuestionCard({
const shell = const shell =
variant === "embedded" variant === "embedded"
? "flex flex-col gap-4 p-4" ? "flex flex-col gap-4 p-4"
: "flex flex-col gap-4 rounded-md border border-[#f2f2f7] bg-white p-4 shadow-[0_1px_4px_rgba(12,12,13,0.05)]"; : "flex flex-col gap-4 rounded-lg card-bg p-4 shadow-md";
const doVote = useCallback( const doVote = useCallback(
async (direction: "up" | "down") => { async (direction: "up" | "down") => {
@ -86,7 +86,7 @@ export function AudienceQuestionCard({
type="button" type="button"
disabled={!canVote || voting} disabled={!canVote || voting}
onClick={() => void doVote("up")} onClick={() => void doVote("up")}
className={`flex items-center gap-2 rounded-full border-2 px-2 py-2 transition ${ className={`flex items-center gap-2 rounded-full border-2 px-2 py-1 transition ${
userVote === "up" userVote === "up"
? "border-[#34c759] bg-[#34c759]/10 text-[#34c759]" ? "border-[#34c759] bg-[#34c759]/10 text-[#34c759]"
: "border-[#34c759] bg-white text-[#34c759] hover:bg-[#34c759]/5" : "border-[#34c759] bg-white text-[#34c759] hover:bg-[#34c759]/5"
@ -94,7 +94,7 @@ export function AudienceQuestionCard({
aria-label={canVote ? "Upvote" : undefined} aria-label={canVote ? "Upvote" : undefined}
> >
<ThumbsUp className="size-4 shrink-0" strokeWidth={2} /> <ThumbsUp className="size-4 shrink-0" strokeWidth={2} />
<span className="text-[10px] font-medium tracking-wide">{item.up}</span> <span className="text-sm font-medium tracking-wide">{item.up}</span>
</button> </button>
); );
@ -103,14 +103,14 @@ export function AudienceQuestionCard({
type="button" type="button"
disabled={!canVote || voting} disabled={!canVote || voting}
onClick={() => void doVote("down")} onClick={() => void doVote("down")}
className={`flex items-center gap-2 rounded-full border-2 px-2 py-2 transition ${ className={`flex items-center gap-2 rounded-full border-2 px-2 py-1 transition ${
userVote === "down" userVote === "down"
? "border-[#ff3b30] bg-[#ff3b30]/10 text-[#ff3b30]" ? "border-[#ff3b30] bg-[#ff3b30]/10 text-[#ff3b30]"
: "border-[#ff3b30] bg-white text-[#ff3b30] hover:bg-[#ff3b30]/5" : "border-[#ff3b30] bg-white text-[#ff3b30] hover:bg-[#ff3b30]/5"
} disabled:cursor-not-allowed disabled:opacity-45`} } disabled:cursor-not-allowed disabled:opacity-45`}
aria-label={canVote ? "Downvote" : undefined} aria-label={canVote ? "Downvote" : undefined}
> >
<span className="text-[10px] font-medium tracking-wide">{item.down}</span> <span className="text-xs font-medium tracking-wide">{item.down}</span>
<ThumbsDown className="size-4 shrink-0" strokeWidth={2} /> <ThumbsDown className="size-4 shrink-0" strokeWidth={2} />
</button> </button>
); );
@ -118,12 +118,12 @@ export function AudienceQuestionCard({
return ( return (
<div className={shell}> <div className={shell}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex size-8 items-center justify-center rounded-full bg-[#f2f2f7]"> <div className="flex size-10 items-center justify-center rounded-full bg-[var(--theme-button-color)]/10">
<User className="size-4 text-[#24282f]/70" strokeWidth={2} /> <User className="size-5 text-[var(theme-card-text-color)]/70" strokeWidth={2} />
</div> </div>
<span className="text-base font-semibold tracking-wide text-[#24282f]">{item.author}</span> <span className="text-sm font-semibold tracking-wide text-[var(theme-card-text-color)]">{item.author}</span>
</div> </div>
<p className="text-base leading-[22px] tracking-wide text-[#24282f]">{item.text}</p> <p className="text-sm leading-[22px] tracking-wide text-[var(theme-card-text-color)]">{item.text}</p>
<div className="flex flex-col items-end gap-1"> <div className="flex flex-col items-end gap-1">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
{canVote ? ( {canVote ? (

View File

@ -23,10 +23,12 @@ export function DynamicFeedbackForm({
eventId, eventId,
schema, schema,
variant = "light", variant = "light",
className = '',
}: { }: {
eventId: string; eventId: string;
schema: VisitorPublicFeedbackForm; schema: VisitorPublicFeedbackForm;
variant?: "light" | "gradient"; variant?: "light" | "gradient";
className?: string | '';
}) { }) {
const [values, setValues] = useState<Record<string, unknown>>({}); const [values, setValues] = useState<Record<string, unknown>>({});
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@ -86,11 +88,11 @@ export function DynamicFeedbackForm({
<div <div
className={ className={
variant === "gradient" variant === "gradient"
? "rounded-xl bg-white p-6 text-center shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]" ? "rounded-md card-bg p-4 text-center shadow-md"
: "rounded-md bg-white p-4 text-center shadow-[0_1px_4px_rgba(12,12,13,0.05)]" : "rounded-md card-bg p-4 text-center shadow-md"
} }
> >
<p className="font-inter text-sm font-semibold text-[#24282f]">Thank you</p> <p className="font-inter text-sm font-semibold">Thank you</p>
<p className="mt-2 rounded-md border border-[#badbcc] bg-[#d1e7dd] px-4 py-3 text-sm font-bold text-[#198754]"> <p className="mt-2 rounded-md border border-[#badbcc] bg-[#d1e7dd] px-4 py-3 text-sm font-bold text-[#198754]">
{schema.thankYouMessage?.trim() || "Your response has been recorded."} {schema.thankYouMessage?.trim() || "Your response has been recorded."}
</p> </p>
@ -100,13 +102,13 @@ export function DynamicFeedbackForm({
const fieldShell = const fieldShell =
variant === "gradient" variant === "gradient"
? "rounded-xl bg-white p-6 shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]" ? `rounded-md p-4 pb-2 ${className}`
: "rounded-md bg-white p-4 shadow-[0_1px_4px_rgba(12,12,13,0.05)]"; : `rounded-md p-4 pb-2 ${className}`;
return ( return (
<form onSubmit={onSubmit} className={fieldShell}> <form onSubmit={onSubmit} className={fieldShell}>
{schema.description?.trim() ? ( {schema.description?.trim() ? (
<p className="mb-4 text-sm leading-snug text-[rgba(36,40,47,0.67)]">{stripHtml(schema.description.trim())}</p> <p className="mb-4 text-sm leading-snug text-[var(--theme-card-text-color)]/80">{stripHtml(schema.description.trim())}</p>
) : null} ) : null}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{fields.map((f) => ( {fields.map((f) => (
@ -120,7 +122,7 @@ export function DynamicFeedbackForm({
<button <button
type="submit" type="submit"
disabled={submitting} disabled={submitting}
className="mt-4 w-full rounded-full bg-[#ef8d1d] py-3 text-xs font-semibold uppercase tracking-wide text-white shadow-[0_4px_4px_rgba(0,0,0,0.15)] disabled:opacity-60" className="mt-4 w-full rounded-lg bg-[var(--theme-button-color)] py-3 text-sm font-semibold uppercase tracking-wide text-white shadow-[0_4px_4px_rgba(0,0,0,0.15)] transition-colors duration-300 ease-in-out hover:bg-[var(--theme-button-color)]/90 disabled:opacity-60"
> >
{submitting ? "Submitting…" : "Submit"} {submitting ? "Submitting…" : "Submit"}
</button> </button>
@ -154,12 +156,12 @@ function FieldInput({
const t = (f.type || "text").toLowerCase(); const t = (f.type || "text").toLowerCase();
const baseInput = const baseInput =
"w-full rounded-lg border border-[rgba(116,112,112,0.3)] bg-[rgba(157,157,172,0.07)] px-3 py-2 font-inter text-sm text-[#24282f] placeholder:text-[rgba(36,40,47,0.45)]"; "w-full rounded-lg border border-[rgba(116,112,112,0.3)] bg-[rgba(157,157,172,0.07)] px-3 py-2 font-inter text-sm text-[#24282f] transition-all duration-300 ease-in-out focus:border-[#ef8d1d focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[#ef8d1d]/30 focus:ring-[var(--theme-button-color)]/30 placeholder:text-[rgba(36,40,47,0.45)]";
if (t === "file") { if (t === "file") {
return ( return (
<p className="text-sm text-[rgba(36,40,47,0.67)]"> <p className="text-sm text-[rgba(36,40,47,0.67)]">
<span className="font-semibold text-[#24282f]">{label}</span> <span className="font-semibold text-[var(--theme-card-text-color)]">{label}</span>
<span className="mt-1 block">File upload isn&apos;t supported in the mobile app yet.</span> <span className="mt-1 block">File upload isn&apos;t supported in the mobile app yet.</span>
</p> </p>
); );
@ -170,11 +172,11 @@ function FieldInput({
const selected = Number.isFinite(n) ? n : 0; const selected = Number.isFinite(n) ? n : 0;
return ( return (
<fieldset className="flex flex-col gap-2"> <fieldset className="flex flex-col gap-2">
<legend className="text-sm font-semibold text-[#24282f]"> <legend className="text-sm font-semibold text-[var(--theme-card-text-color)]">
{label} {label}
{f.required ? <span className="text-red-600"> *</span> : null} {f.required ? <span className="text-red-600"> *</span> : null}
</legend> </legend>
<div className="flex gap-2"> <div className="flex gap-2 mt-1">
{Array.from({ length: 5 }, (_, i) => { {Array.from({ length: 5 }, (_, i) => {
const star = i + 1; const star = i + 1;
return ( return (
@ -182,11 +184,11 @@ function FieldInput({
key={star} key={star}
type="button" type="button"
onClick={() => onChange(label, star)} onClick={() => onChange(label, star)}
className="text-[#ef8d1d]" className="text-[var(--theme-button-color)]"
aria-label={`${star} star${star === 1 ? "" : "s"}`} aria-label={`${star} star${star === 1 ? "" : "s"}`}
> >
<Star <Star
className={`size-7 ${star <= selected ? "fill-[#ef8d1d]" : "fill-transparent"}`} className={`size-7 ${star <= selected ? "fill-[var(--theme-button-color)]" : "fill-transparent"}`}
strokeWidth={1.75} strokeWidth={1.75}
/> />
</button> </button>
@ -223,13 +225,13 @@ function FieldInput({
if (t === "radio") { if (t === "radio") {
return ( return (
<fieldset className="flex flex-col gap-2"> <fieldset className="flex flex-col gap-2">
<legend className="text-sm font-semibold text-[#24282f]"> <legend className="text-sm font-semibold text-[var(--theme-card-text-color)]">
{label} {label}
{f.required ? <span className="text-red-600"> *</span> : null} {f.required ? <span className="text-red-600"> *</span> : null}
</legend> </legend>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{(opts.length ? opts : ["Yes", "No"]).map((o) => ( {(opts.length ? opts : ["Yes", "No"]).map((o) => (
<label key={o} className="flex items-center gap-2 text-sm text-[#24282f]"> <label key={o} className="flex items-center gap-2 text-sm text-[var(--theme-card-text-color)]">
<input type="radio" name={label} checked={value === o} onChange={() => onChange(label, o)} /> <input type="radio" name={label} checked={value === o} onChange={() => onChange(label, o)} />
{o} {o}
</label> </label>
@ -247,13 +249,13 @@ function FieldInput({
}; };
return ( return (
<fieldset className="flex flex-col gap-2"> <fieldset className="flex flex-col gap-2">
<legend className="text-sm font-semibold text-[#24282f]"> <legend className="text-sm font-semibold text-[var(--theme-card-text-color)]">
{label} {label}
{f.required ? <span className="text-red-600"> *</span> : null} {f.required ? <span className="text-red-600"> *</span> : null}
</legend> </legend>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{opts.map((o) => ( {opts.map((o) => (
<label key={o} className="flex items-center gap-2 text-sm text-[#24282f]"> <label key={o} className="flex items-center gap-2 text-sm text-[var(--theme-card-text-color)]">
<input type="checkbox" checked={arr.includes(o)} onChange={() => toggle(o)} /> <input type="checkbox" checked={arr.includes(o)} onChange={() => toggle(o)} />
{o} {o}
</label> </label>
@ -265,7 +267,7 @@ function FieldInput({
if (t === "checkbox") { if (t === "checkbox") {
return ( return (
<label className="flex items-center gap-2 text-sm text-[#24282f]"> <label className="flex items-center gap-2 text-sm text-[var(--theme-card-text-color)]">
<input <input
type="checkbox" type="checkbox"
checked={value === true || value === "yes" || value === "Yes"} checked={value === true || value === "yes" || value === "Yes"}
@ -282,7 +284,7 @@ function FieldInput({
if (t === "email" || t === "phone") { if (t === "email" || t === "phone") {
return ( return (
<label className="flex flex-col gap-2"> <label className="flex flex-col gap-2">
<span className="text-sm font-semibold text-[#24282f]"> <span className="text-sm font-semibold text-[var(--theme-card-text-color)]">
{label} {label}
{f.required ? <span className="text-red-600"> *</span> : null} {f.required ? <span className="text-red-600"> *</span> : null}
</span> </span>
@ -300,7 +302,7 @@ function FieldInput({
if (t === "textarea") { if (t === "textarea") {
return ( return (
<label className="flex flex-col gap-2"> <label className="flex flex-col gap-2">
<span className="text-sm font-semibold text-[#24282f]"> <span className="text-sm font-semibold text-[var(--theme-card-text-color)]">
{label} {label}
{f.required ? <span className="text-red-600"> *</span> : null} {f.required ? <span className="text-red-600"> *</span> : null}
</span> </span>
@ -324,7 +326,7 @@ function FieldInput({
if (t === "text" && longText) { if (t === "text" && longText) {
return ( return (
<label className="flex flex-col gap-2"> <label className="flex flex-col gap-2">
<span className="text-sm font-semibold text-[#24282f]"> <span className="text-sm font-semibold text-[var(--theme-card-text-color)]">
{label} {label}
{f.required ? <span className="text-red-600"> *</span> : null} {f.required ? <span className="text-red-600"> *</span> : null}
</span> </span>
@ -341,7 +343,7 @@ function FieldInput({
return ( return (
<label className="flex flex-col gap-2"> <label className="flex flex-col gap-2">
<span className="text-sm font-semibold text-[#24282f]"> <span className="text-sm font-semibold text-[var(--theme-card-text-color)]">
{label} {label}
{f.required ? <span className="text-red-600"> *</span> : null} {f.required ? <span className="text-red-600"> *</span> : null}
</span> </span>

View File

@ -4,13 +4,14 @@ export type EventBannerProps = {
/** Event banner URL from login-preview (`bannerImage` or `image`) */ /** Event banner URL from login-preview (`bannerImage` or `image`) */
imageUrl?: string | null; imageUrl?: string | null;
alt?: string; alt?: string;
className?: string | '';
}; };
export function EventBanner({ imageUrl, alt = "" }: EventBannerProps) { export function EventBanner({ imageUrl, alt = "", className = "" }: EventBannerProps) {
const src = imageUrl?.trim(); const src = imageUrl?.trim();
if (src) { if (src) {
return ( return (
<div className="w-full shrink-0 overflow-hidden bg-[#e5e5ea]"> <div className={`w-full shrink-0 overflow-hidden bg-[#e5e5ea] ${className}`}>
<img <img
src={src} src={src}
alt={alt} alt={alt}
@ -22,8 +23,8 @@ export function EventBanner({ imageUrl, alt = "" }: EventBannerProps) {
return ( return (
<div <div
className="eb-gradient-banner flex h-[130px] shrink-0 items-center justify-center border-y border-[#f2f2f7] px-8 py-10 sm:px-16" className={`eb-gradient-banner flex h-[130px] shrink-0 items-center justify-center border-y border-[#f2f2f7] px-8 py-10 sm:px-16"
data-name="Event Logo" data-name="Event Logo ${className}`}
> >
<EventBuzzLogo <EventBuzzLogo
variant="white" variant="white"

View File

@ -172,7 +172,7 @@ export function LandingAgendaCards({
return ( return (
<div <div
key={day.dateKey} key={day.dateKey}
className="w-full rounded-lg border-x-0 border-[rgba(116,112,112,0.2)] bg-white shadow-sm sm:border-x" className="w-full rounded-lg border border-[var(--theme-card-text-color)]/5"
> >
<button <button
type="button" type="button"
@ -181,34 +181,34 @@ export function LandingAgendaCards({
aria-expanded={isOpen} aria-expanded={isOpen}
> >
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<Calendar className="size-4 shrink-0 text-[#1e72ef]" strokeWidth={1.75} /> <Calendar className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="font-inter text-sm font-semibold text-[#24282f]">{day.dateLabel}</span> <span className="text-sm font-semibold">{day.dateLabel}</span>
</div> </div>
<ChevronDown <ChevronDown
className={`size-4 shrink-0 text-[#1e72ef] transition-transform ${isOpen ? "rotate-180" : ""}`} className={`size-4 shrink-0 text-[var(--theme-card-text-color)]/70 transition-transform ${isOpen ? "rotate-180" : ""}`}
strokeWidth={2} strokeWidth={2}
aria-hidden aria-hidden
/> />
</button> </button>
{isOpen ? ( {isOpen ? (
<div className="w-full border-t border-[#f2f2f7] pb-4 pt-1"> <div className="w-full border-t border-[var(--theme-card-text-color)]/5 pb-4 pt-1">
{day.trackGroups.map((tg, tgIdx) => ( {day.trackGroups.map((tg, tgIdx) => (
<div key={tg.trackId} className="w-full"> <div key={tg.trackId} className="w-full">
{tgIdx > 0 ? ( {tgIdx > 0 ? (
<div <div
className="my-5 w-full border-t-[3px] border-[#1e72ef] shadow-[0_1px_0_rgba(12,12,13,0.06)]" className="my-5 w-full border-t-[3px] border-[var(--theme-button-color)] shadow-[0_1px_0_rgba(12,12,13,0.06)]"
role="separator" role="separator"
aria-hidden aria-hidden
/> />
) : null} ) : null}
<div className="px-4 pt-2"> <div className="px-4 pt-2">
<p className="font-inter text-xs font-semibold uppercase tracking-wide text-[#8e8e93]"> <p className="font-inter text-xs font-semibold uppercase tracking-wide text-[var(--theme-card-text-color)]/60">
{tg.trackName} {tg.trackName}
</p> </p>
{tg.trackHall ? ( {tg.trackHall ? (
<div className="mt-2 flex items-start gap-2 text-sm text-[#24282f]"> <div className="mt-2 flex items-start gap-2 text-sm text-[var(--theme-card-text-color)]">
<MapPin <MapPin
className="mt-0.5 size-4 shrink-0 text-[#1e72ef]" className="mt-0.5 size-4 shrink-0 text-[var(--theme-button-color)]"
strokeWidth={1.75} strokeWidth={1.75}
/> />
<span className="font-inter leading-snug">{tg.trackHall}</span> <span className="font-inter leading-snug">{tg.trackHall}</span>
@ -216,7 +216,7 @@ export function LandingAgendaCards({
) : null} ) : null}
</div> </div>
<div className="relative mt-3 w-full max-w-none px-0"> <div className="relative mt-3 w-full max-w-none px-0">
<div className="relative w-full max-w-none rounded-none bg-[#fafafa] p-4 shadow-[0_1px_4px_rgba(12,12,13,0.05)] sm:rounded-md"> <div className="relative w-full max-w-none rounded-none px-4">
<div className="flex w-full flex-col gap-4"> <div className="flex w-full flex-col gap-4">
{tg.sessions.map((s) => ( {tg.sessions.map((s) => (
<AgendaRowTrackV2 <AgendaRowTrackV2
@ -257,7 +257,7 @@ export function LandingMySessionsList({
return ( return (
<div className="mt-3 w-full max-w-none"> <div className="mt-3 w-full max-w-none">
<div className="relative w-full"> <div className="relative w-full">
<div className="relative w-full max-w-none rounded-none bg-[#fafafa] p-4 shadow-[0_1px_4px_rgba(12,12,13,0.05)] sm:rounded-md"> <div className="relative w-full max-w-none rounded-none py-2 border-b border-[var(--theme-card-text-color)]/5 last:border-b-0">
<div className="flex w-full flex-col gap-4"> <div className="flex w-full flex-col gap-4">
{sessions.map((s) => ( {sessions.map((s) => (
<AgendaRowTrackV2 <AgendaRowTrackV2

View File

@ -27,7 +27,7 @@ export function EventRatingBadge({ ratings }: { ratings: EventRatingsSummary | n
); );
})} })}
</div> </div>
<span className="font-inter text-[#8e8e93]"> <span className="text-[var(--theme-card-text-color)]/60">
{average.toFixed(1)} ({count}) {average.toFixed(1)} ({count})
</span> </span>
</> </>

View File

@ -131,7 +131,7 @@ export function HeaderMobile({
return ( return (
<header <header
className="sticky top-0 z-20 flex h-[60px] shrink-0 items-center border-b border-white/40 bg-white px-4 shadow-[0_1px_4px_rgba(12,12,13,0.05),0_1px_4px_rgba(12,12,13,0.1)]" className="sticky top-0 z-20 flex h-[60px] shrink-0 items-center border-b border-white/40 bg-[var(--theme-header-color)] backdrop-blur-sm px-4 shadow-xs"
data-name="Header_Mobile" data-name="Header_Mobile"
> >
<div className="flex w-full items-center justify-between gap-2"> <div className="flex w-full items-center justify-between gap-2">
@ -140,13 +140,13 @@ export function HeaderMobile({
<button <button
type="button" type="button"
onClick={() => (onBack ? onBack() : navigate(-1))} onClick={() => (onBack ? onBack() : navigate(-1))}
className="flex size-10 shrink-0 items-center justify-center rounded-full bg-[#f2f2f7] text-[#1d66ef]" className="flex size-10 shrink-0 items-center justify-center rounded-full bg-[var(--theme-button-color)]/10 text-[var(--theme-button-color)]/70"
aria-label="Back" aria-label="Back"
> >
<ChevronLeft className="size-6" strokeWidth={2} /> <ChevronLeft className="size-6" strokeWidth={2} />
</button> </button>
) : null} ) : null}
<div className="flex h-10 min-w-0 max-w-[min(200px,42vw)] items-center justify-center rounded border border-[#f2f2f7] bg-white px-2 py-1.5"> <div className="flex h-10 min-w-0 max-w-[min(200px,42vw)] items-center justify-center px-2 py-1.5">
{logoSrc ? ( {logoSrc ? (
<img <img
src={logoSrc} src={logoSrc}
@ -169,17 +169,17 @@ export function HeaderMobile({
<div className="flex shrink-0 items-center justify-end gap-2 sm:gap-4"> <div className="flex shrink-0 items-center justify-end gap-2 sm:gap-4">
<Link <Link
to={rewardsHref} to={rewardsHref}
className="flex items-center gap-1.5 rounded-full border border-[#ef8d1d] px-3 py-2 sm:gap-2 sm:px-4" className="flex items-center gap-1.5 rounded-full border border-[var(--theme-button-color)] transition-all duration-300 ease-in-out hover:bg-[var(--theme-button-color)]/5 px-3 py-2 sm:gap-2 sm:px-4"
> >
<Trophy className="size-5 shrink-0 text-[#ef8d1d] sm:size-6" strokeWidth={1.75} /> <Trophy className="size-5 shrink-0 text-[var(--theme-button-color)] sm:size-6" strokeWidth={1.75} />
<span className="min-w-[1.25rem] text-sm font-medium tabular-nums tracking-wide text-[#24282f]"> <span className="min-w-[1.25rem] text-sm font-medium tabular-nums tracking-wide text-[var(--theme-button-color)]">
{pointsLabel} {pointsLabel}
</span> </span>
</Link> </Link>
<button <button
type="button" type="button"
onClick={handleLogout} onClick={handleLogout}
className="flex size-10 shrink-0 items-center justify-center rounded-full bg-[#ef8d1d] text-white" className="flex size-10 shrink-0 items-center justify-center rounded-full bg-[var(--theme-button-color)] transition-colors duration-300 ease-in-out hover:bg-[var(--theme-button-color)]/90 text-white"
aria-label="Sign out" aria-label="Sign out"
> >
<LogOut className="size-5 sm:size-6" strokeWidth={2} /> <LogOut className="size-5 sm:size-6" strokeWidth={2} />

View File

@ -61,19 +61,19 @@ export function HyperNavigationFAB({
const ratingsTo = eventId ? `/ratings?${ratingsQs.toString()}` : "/ratings"; const ratingsTo = eventId ? `/ratings?${ratingsQs.toString()}` : "/ratings";
const items: MenuItem[] = [ const items: MenuItem[] = [
{ label: "Event Home", to: eventHome, icon: <Home className="size-4 shrink-0 text-[#1e72ef]" strokeWidth={1.75} /> }, { label: "Event Home", to: eventHome, icon: <Home className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} /> },
{ label: "Rewards", to: rewards, icon: <Gift className="size-4 shrink-0 text-[#1e72ef]" strokeWidth={1.75} /> }, { label: "Rewards", to: rewards, icon: <Gift className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} /> },
{ label: "Leaderboard", to: leaderboard, icon: <Trophy className="size-4 shrink-0 text-[#1e72ef]" strokeWidth={1.75} /> }, { label: "Leaderboard", to: leaderboard, icon: <Trophy className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} /> },
...(showFeedbackNav ...(showFeedbackNav
? [{ label: "Feedback", to: feedbackTo, icon: <MessageCircle className="size-4 shrink-0 text-[#1e72ef]" strokeWidth={1.75} /> }] ? [{ label: "Feedback", to: feedbackTo, icon: <MessageCircle className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} /> }]
: []), : []),
...(showRatingsNav ...(showRatingsNav
? [{ label: "Ratings", to: ratingsTo, icon: <Star className="size-4 shrink-0 text-[#1e72ef]" strokeWidth={1.75} /> }] ? [{ label: "Ratings", to: ratingsTo, icon: <Star className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} /> }]
: []), : []),
{ {
label: "All Questions", label: "All Questions",
to: allQuestions, to: allQuestions,
icon: <MessageCircleQuestion className="size-4 shrink-0 text-[#1e72ef]" strokeWidth={1.75} />, icon: <MessageCircleQuestion className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />,
}, },
]; ];
@ -126,7 +126,7 @@ export function HyperNavigationFAB({
aria-expanded={open} aria-expanded={open}
aria-haspopup="menu" aria-haspopup="menu"
aria-label={open ? "Close quick navigation" : "Open quick navigation"} aria-label={open ? "Close quick navigation" : "Open quick navigation"}
className="relative flex size-12 shrink-0 items-center justify-center rounded-full border-2 border-white text-white shadow-[0_14px_18px_rgba(0,0,0,0.2),0_2px_3px_rgba(0,0,0,0.17)] eb-gradient-fab" className="relative flex size-12 shrink-0 items-center justify-center rounded-full border-2 border-white/60 text-white shadow-[0_14px_18px_rgba(0,0,0,0.2),0_2px_3px_rgba(0,0,0,0.17)] eb-gradient-fa theme-btn-bg"
> >
<span <span
className="pointer-events-none absolute inset-[-2px] rounded-full shadow-[inset_0_6px_4px_rgba(0,0,0,0.25)]" className="pointer-events-none absolute inset-[-2px] rounded-full shadow-[inset_0_6px_4px_rgba(0,0,0,0.25)]"

View File

@ -16,7 +16,7 @@ export function MobileShell({
}: MobileShellProps) { }: MobileShellProps) {
return ( return (
<div <div
className={`mx-auto flex min-h-dvh w-full max-w-[402px] flex-1 flex-col bg-[#f2f2f7] ${className}`} className={`mx-auto flex min-h-dvh w-full max-w-[402px] flex-1 flex-col bg-[#f2f2f7 bg-transparent ${className}`}
> >
<div className="flex min-h-0 flex-1 flex-col">{children}</div> <div className="flex min-h-0 flex-1 flex-col">{children}</div>
{showPoweredByFooter ? <PoweredByFooter /> : null} {showPoweredByFooter ? <PoweredByFooter /> : null}

View File

@ -47,16 +47,16 @@ export function PhotoWallCropModal({
}; };
return ( return (
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/55 p-4 sm:items-center"> <div className="fixed inset-0 z-50 flex items-end justify-center bg-black/70 p-4 sm:items-center">
<div className="flex w-full max-w-[420px] flex-col overflow-hidden rounded-2xl bg-white shadow-xl"> <div className="flex w-full max-w-[420px] flex-col overflow-hidden rounded-2xl card-bg shadow-xl">
<div className="border-b border-[#f2f2f7] px-4 py-3"> <div className="border-b border-[var(--theme-card-border-color)]/5 px-4 py-3">
<p className="font-inter text-base font-semibold text-[#24282f]">Crop your photo</p> <p className="font-inter text-base font-semibold text-[var(--theme-card-text-color)]">Crop your photo</p>
<p className="mt-0.5 font-inter text-xs text-[rgba(36,40,47,0.65)]"> <p className="mt-0.5 font-inter text-xs text-[var(--theme-card-text-color)]/80">
Adjust to match the wall photo ratio before uploading. Adjust to match the wall photo ratio before uploading.
</p> </p>
</div> </div>
<div className="relative h-[280px] w-full bg-[#1a1d24] sm:h-[320px]"> <div className="relative h-[280px] w-full bg-gray-800 sm:h-[320px]">
<Cropper <Cropper
image={imageSrc} image={imageSrc}
crop={crop} crop={crop}
@ -70,7 +70,7 @@ export function PhotoWallCropModal({
<div className="flex flex-col gap-3 px-4 py-4"> <div className="flex flex-col gap-3 px-4 py-4">
<label className="flex flex-col gap-1.5"> <label className="flex flex-col gap-1.5">
<span className="font-inter text-xs font-semibold text-[#24282f]">Zoom</span> <span className="font-inter text-xs font-semibold text-[var(--theme-card-text-color)]">Zoom</span>
<input <input
type="range" type="range"
min={1} min={1}
@ -78,7 +78,7 @@ export function PhotoWallCropModal({
step={0.05} step={0.05}
value={zoom} value={zoom}
onChange={(e) => setZoom(Number(e.target.value))} onChange={(e) => setZoom(Number(e.target.value))}
className="w-full accent-[#ef8d1d]" className="w-full accent-[var(--theme-button-color)]"
/> />
</label> </label>
@ -93,7 +93,7 @@ export function PhotoWallCropModal({
type="button" type="button"
onClick={onCancel} onClick={onCancel}
disabled={working} disabled={working}
className="flex-1 rounded-full border border-[rgba(116,112,112,0.3)] bg-white py-3 text-xs font-semibold uppercase tracking-wide text-[#24282f] disabled:opacity-60" className="flex-1 rounded-full border border-zinc-300 bg-zinc-200 py-3 text-xs font-semibold uppercase tracking-wide text-gray-600 disabled:opacity-60"
> >
Cancel Cancel
</button> </button>
@ -101,7 +101,7 @@ export function PhotoWallCropModal({
type="button" type="button"
onClick={() => void confirm()} onClick={() => void confirm()}
disabled={working || !croppedAreaPixels} disabled={working || !croppedAreaPixels}
className="flex-1 rounded-full border border-[rgba(214,200,200,0.6)] bg-[#ef8d1d] py-3 text-xs font-semibold uppercase tracking-wide text-white shadow-[0_4px_4px_rgba(0,0,0,0.15)] disabled:opacity-60" className="flex-1 rounded-full theme-btn-bg py-3 text-xs font-semibold uppercase tracking-wide text-white shadow-sm disabled:opacity-60"
> >
{working ? "Cropping…" : "Use photo"} {working ? "Cropping…" : "Use photo"}
</button> </button>

View File

@ -75,7 +75,7 @@ export function PhotoWallEventHeader({
if (d.showDateRange && dateLine) { if (d.showDateRange && dateLine) {
metaItems.push( metaItems.push(
<span key="date" className="inline-flex items-center gap-1.5"> <span key="date" className="inline-flex items-center gap-1.5">
<Calendar className="size-3.5 shrink-0" strokeWidth={1.75} /> <Calendar className="size-3.5 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
{dateLine} {dateLine}
</span>, </span>,
); );
@ -85,7 +85,7 @@ export function PhotoWallEventHeader({
if (showLoc) { if (showLoc) {
metaItems.push( metaItems.push(
<span key="loc" className="inline-flex items-center gap-1.5"> <span key="loc" className="inline-flex items-center gap-1.5">
<MapPin className="size-3.5 shrink-0" strokeWidth={1.75} /> <MapPin className="size-3.5 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
{venue} {venue}
</span>, </span>,
); );
@ -105,7 +105,7 @@ export function PhotoWallEventHeader({
return ( return (
<header className="flex w-full flex-col items-center gap-3 text-center"> <header className="flex w-full flex-col items-center gap-3 text-center">
{showLogo ? ( {showLogo ? (
<div className="flex h-[103px] w-full items-center justify-center rounded-xl border border-[#f2f2f7] px-4"> <div className="flex h-[103px] w-full items-center justify-center rounded-xl px-4">
<img <img
src={event.logoUrl!} src={event.logoUrl!}
alt={event.name} alt={event.name}
@ -121,22 +121,22 @@ export function PhotoWallEventHeader({
{showTitle || showSubtitle || metaItems.length > 0 ? ( {showTitle || showSubtitle || metaItems.length > 0 ? (
<div className="flex w-full flex-col items-center gap-2"> <div className="flex w-full flex-col items-center gap-2">
{eyebrowText ? ( {eyebrowText ? (
<p className="font-inter text-base font-semibold text-[#24282f]">{eyebrowText}</p> <p className="text-base font-semibold text-[var(--theme-card-text-color)]/80">{eyebrowText}</p>
) : null} ) : null}
{showTitle ? ( {showTitle ? (
<h1 className="font-inter text-[28px] font-bold leading-tight text-[#24282f] sm:text-[32px]"> <h1 className="text-[24px] font-bold leading-tight text-[var(--theme-card-text-color)] sm:text-[26px]">
{event.name} {event.name}
</h1> </h1>
) : null} ) : null}
{showSubtitle ? ( {showSubtitle ? (
<p className="font-inter text-sm font-medium text-[#24282f]/80">{event.subtitle}</p> <p className="text-sm font-medium text-[var(--theme-card-text-color)]/80">{event.subtitle}</p>
) : null} ) : null}
{metaItems.length > 0 ? ( {metaItems.length > 0 ? (
<div className="flex flex-wrap items-center justify-center gap-x-2 gap-y-1 text-xs text-[#24282f]"> <div className="flex flex-wrap items-center justify-center gap-x-2 gap-y-1 text-xs text-[var(--theme-card-text-color)]/80">
{metaItems.map((item, index) => ( {metaItems.map((item, index) => (
<span key={index} className="inline-flex items-center gap-1.5 font-inter"> <span key={index} className="inline-flex items-center gap-1.5 font-inter">
{index > 0 ? ( {index > 0 ? (
<span aria-hidden="true" className="text-[rgba(36,40,47,0.4)]"> <span aria-hidden="true" className="text-[var(--theme-card-text-color)]/40">
· ·
</span> </span>
) : null} ) : null}
@ -147,7 +147,7 @@ export function PhotoWallEventHeader({
) : null} ) : null}
</div> </div>
) : eyebrowText ? ( ) : eyebrowText ? (
<p className="font-inter text-base font-semibold text-[#24282f]">{eyebrowText}</p> <p className="font-inter text-base font-semibold text-[var(--theme-card-text-color)]">{eyebrowText}</p>
) : null} ) : null}
</header> </header>
); );

View File

@ -93,16 +93,16 @@ export function SessionQuestionsCard({
return ( return (
<section <section
className="relative z-10 flex w-full max-w-none flex-col gap-4 rounded-lg border border-[rgba(116,112,112,0.2)] bg-white py-4 shadow-sm" className="relative z-10 flex w-full max-w-none flex-col gap-4 rounded-lg card-bg py-4 shadow-md"
data-name="Session Questions" data-name="Session Questions"
> >
<div className="flex h-8 items-center justify-between gap-2 px-4"> <div className="flex h-8 items-center justify-between gap-2 px-4">
<div className="flex min-w-0 flex-1 items-center gap-2"> <div className="flex min-w-0 flex-1 items-center gap-2">
<h2 className="truncate text-lg font-semibold tracking-wide text-[#24282f]"> <h2 className="truncate text-md font-semibold tracking-wide">
{title} {title}
</h2> </h2>
{total > 0 ? ( {total > 0 ? (
<span className="shrink-0 rounded bg-[#e5e5ea] px-2 py-0.5 text-[10px] tracking-wide text-[#24282f]"> <span className="shrink-0 rounded bg-zinc-200 px-2 py-0.5 text-[10px] tracking-wide text-zinc-700">
{safeIndex + 1}/{total} {safeIndex + 1}/{total}
</span> </span>
) : null} ) : null}
@ -111,26 +111,26 @@ export function SessionQuestionsCard({
<div className="flex shrink-0 gap-1"> <div className="flex shrink-0 gap-1">
<button <button
type="button" type="button"
className="rounded-full bg-[#f2f2f7] p-1" className="rounded-full bg-zinc-300 p-1"
aria-label="Previous question" aria-label="Previous question"
onClick={goPrev} onClick={goPrev}
> >
<ChevronLeft className="size-4 text-[#24282f]" /> <ChevronLeft className="size-4 text-zinc-700" />
</button> </button>
<button <button
type="button" type="button"
className="rounded-full bg-[#f2f2f7] p-1" className="rounded-full bg-zinc-300 p-1"
aria-label="Next question" aria-label="Next question"
onClick={goNext} onClick={goNext}
> >
<ChevronRight className="size-4 text-[#24282f]" /> <ChevronRight className="size-4 text-zinc-700" />
</button> </button>
</div> </div>
) : null} ) : null}
</div> </div>
<div className="border-t border-[#f2f2f7]"> <div className="border-t border-[var(--theme-card-text-color)]/5">
{live && loading ? ( {live && loading ? (
<p className="px-4 py-6 text-center text-sm text-[rgba(36,40,47,0.55)]">Loading questions…</p> <p className="px-4 py-6 text-center text-sm text-[var(--theme-card-text-color)]/80">Loading questions…</p>
) : live && error ? ( ) : live && error ? (
<p className="px-4 py-6 text-center text-sm text-red-600">{error}</p> <p className="px-4 py-6 text-center text-sm text-red-600">{error}</p>
) : current ? ( ) : current ? (
@ -143,19 +143,19 @@ export function SessionQuestionsCard({
/> />
</div> </div>
) : ( ) : (
<p className="px-4 py-6 text-center text-sm text-[rgba(36,40,47,0.55)]">No questions yet.</p> <p className="px-4 py-6 text-center text-sm text-[var(--theme-card-text-color)]/80">No questions yet.</p>
)} )}
</div> </div>
<div className="flex items-center justify-between gap-3 px-4"> <div className="flex items-center justify-between gap-3 px-4">
{viewAllTo ? ( {viewAllTo ? (
<Link <Link
to={viewAllTo} to={viewAllTo}
className="font-inter text-xs font-semibold uppercase tracking-wider text-[#1e72ef]" className="text-xs font-semibold uppercase tracking-wider theme-btn-link"
> >
View All View All
</Link> </Link>
) : ( ) : (
<span className="font-inter text-xs font-semibold uppercase tracking-wider text-[rgba(36,40,47,0.35)]"> <span className="text-xs font-semibold uppercase tracking-wider text-[var(--theme-card-text-color)]/40">
View All View All
</span> </span>
)} )}
@ -163,7 +163,7 @@ export function SessionQuestionsCard({
type="button" type="button"
onClick={() => live && setAskModalOpen(true)} onClick={() => live && setAskModalOpen(true)}
disabled={!live} disabled={!live}
className="shrink-0 rounded-lg bg-[#ef8d1d] px-3 py-2 text-xs font-semibold text-white shadow-[0_2px_4px_rgba(0,0,0,0.1)] transition hover:bg-[#e0800c] disabled:opacity-50" className="shrink-0 rounded-lg bg-[var(--theme-button-color)] px-3 py-2 text-xs font-semibold text-white shadow-md transition-all duration-300 ease-in-out transition disabled:opacity-50"
> >
Ask your Question Ask your Question
</button> </button>

View File

@ -7,7 +7,7 @@ function typeBadgeClass(type: string): string {
const t = type.toLowerCase(); const t = type.toLowerCase();
if (t === "poll") return "bg-[#eaddff]"; if (t === "poll") return "bg-[#eaddff]";
if (t === "survey") return "bg-[#e5e5ea]"; if (t === "survey") return "bg-[#e5e5ea]";
return "bg-[#ebffee]"; return "bg-green-100";
} }
function typeLabel(type: string): string { function typeLabel(type: string): string {
@ -44,7 +44,7 @@ export function SessionQuizPollsSection({ eventId, sessionId }: SessionQuizPolls
}).toString()}`; }).toString()}`;
return ( return (
<div className="w-full max-w-none rounded-lg border border-[rgba(116,112,112,0.2)] bg-white shadow-sm"> <div className="w-full max-w-none rounded-lg card-bg shadow-md">
<button <button
type="button" type="button"
className="flex w-full items-center justify-between gap-2 px-4 py-3 text-left" className="flex w-full items-center justify-between gap-2 px-4 py-3 text-left"
@ -52,46 +52,46 @@ export function SessionQuizPollsSection({ eventId, sessionId }: SessionQuizPolls
aria-expanded={open} aria-expanded={open}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-semibold text-[#24282f]">Quiz &amp; Polls</span> <span className="text-md font-semibold text-[var(--theme-card-text-color)]">Quiz &amp; Polls</span>
<span className="rounded bg-[#ebffee] px-2 py-0.5 text-[10px] uppercase text-[#24282f]"> <span className="rounded bg-green-100 px-2 py-0.5 text-[10px] uppercase text-gray-800">
{loading ? "…" : `${items.length} Active`} {loading ? "…" : `${items.length} Active`}
</span> </span>
</div> </div>
{open ? ( {open ? (
<ChevronUp className="size-4 shrink-0 text-[#1d66ef]" strokeWidth={2} /> <ChevronUp className="size-4 shrink-0 text-[var(--theme-card-text-color)]/70" strokeWidth={2} />
) : ( ) : (
<ChevronDown className="size-4 shrink-0 text-[#1d66ef]" strokeWidth={2} /> <ChevronDown className="size-4 shrink-0 text-[var(--theme-card-text-color)]/70" strokeWidth={2} />
)} )}
</button> </button>
{open ? ( {open ? (
<div className="border-t border-[#f2f2f7] px-4 pb-4 pt-1"> <div className="border-t border-[var(--theme-card-text-color)]/5 px-4 pb-4 pt-1">
{!live ? ( {!live ? (
<p className="py-3 text-center text-sm text-[rgba(36,40,47,0.55)]">Join a session to see quizzes.</p> <p className="py-3 text-center text-sm text-[var(--theme-card-text-color)]/70">Join a session to see quizzes.</p>
) : error ? ( ) : error ? (
<p className="py-3 text-center text-sm text-red-600">{error}</p> <p className="py-3 text-center text-sm text-red-600">{error}</p>
) : loading ? ( ) : loading ? (
<p className="py-3 text-center text-sm text-[rgba(36,40,47,0.55)]">Loading quizzes…</p> <p className="py-3 text-center text-sm text-[var(--theme-card-text-color)]/70">Loading quizzes…</p>
) : items.length === 0 ? ( ) : items.length === 0 ? (
<p className="py-3 text-center text-sm text-[rgba(36,40,47,0.55)]">No live quizzes or polls right now.</p> <p className="py-3 text-center text-sm text-[var(--theme-card-text-color)]/70">No live quizzes or polls right now.</p>
) : ( ) : (
<ul className="flex flex-col gap-0"> <ul className="flex flex-col gap-0">
{items.map((item) => ( {items.map((item) => (
<li <li
key={item._id} key={item._id}
className="flex items-center justify-between gap-2 border-b border-[#f2f2f7] py-3 last:border-0" className="flex items-center justify-between gap-2 border-b border-[var(--theme-card-text-color)]/5 py-3 last:border-0 text-[var(--theme-card-text-color)]"
> >
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<span <span
className={`inline-block rounded px-2 py-0.5 text-[10px] font-medium text-[#24282f] ${typeBadgeClass(item.type)}`} className={`inline-block rounded px-2 py-0.5 text-[10px] font-medium text-gray-800 ${typeBadgeClass(item.type)}`}
> >
{typeLabel(item.type).toUpperCase()} {typeLabel(item.type).toUpperCase()}
</span> </span>
<p className="mt-1 truncate text-sm font-medium text-[#24282f]">{item.title || "Untitled"}</p> <p className="mt-1 truncate text-sm font-medium">{item.title || "Untitled"}</p>
</div> </div>
<Link <Link
to={takeHref(item._id)} to={takeHref(item._id)}
className="shrink-0 rounded bg-[#0b4dd1] px-3 py-1.5 text-center text-xs font-semibold text-white transition hover:bg-[#0a43b8]" className="shrink-0 rounded theme-btn-bg px-3 py-1.5 text-center text-xs font-semibold text-white"
> >
{actionLabel(item.type)} {actionLabel(item.type)}
</Link> </Link>

View File

@ -0,0 +1,59 @@
import { useEffect, useRef, useState, type ReactNode } from "react";
import { ThemeLoadingScreen } from "./ThemeLoadingScreen";
/** Keep loader up at least this long so fast networks don't flash it. */
const MIN_VISIBLE_MS = 900;
/** Opacity transition duration — must match CSS. */
const FADE_MS = 480;
type ThemeGateProps = {
/** Theme fetch finished (success or failure). */
ready: boolean;
children: ReactNode;
};
/**
* Shows a full-page loader until theme is ready, holds briefly, then fades out
* over the already-themed app content.
*/
export function ThemeGate({ ready, children }: ThemeGateProps) {
const [overlayVisible, setOverlayVisible] = useState(!ready);
const [fading, setFading] = useState(false);
const shownAtRef = useRef(Date.now());
const fadeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const holdTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
if (!ready) {
if (holdTimerRef.current) clearTimeout(holdTimerRef.current);
if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);
shownAtRef.current = Date.now();
setFading(false);
setOverlayVisible(true);
return;
}
const elapsed = Date.now() - shownAtRef.current;
const holdMs = Math.max(0, MIN_VISIBLE_MS - elapsed);
holdTimerRef.current = setTimeout(() => {
setFading(true);
fadeTimerRef.current = setTimeout(() => {
setOverlayVisible(false);
setFading(false);
}, FADE_MS);
}, holdMs);
return () => {
if (holdTimerRef.current) clearTimeout(holdTimerRef.current);
if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current);
};
}, [ready]);
return (
<>
{ready ? children : null}
{overlayVisible ? <ThemeLoadingScreen fading={fading} /> : null}
</>
);
}

View File

@ -0,0 +1,25 @@
type ThemeLoadingScreenProps = {
/** When true, opacity animates to 0 before unmount. */
fading?: boolean;
};
/** Full-viewport gate while public visitor theme CSS vars are applied. */
export function ThemeLoadingScreen({ fading = false }: ThemeLoadingScreenProps) {
return (
<div
className={`eb-theme-loader fixed inset-0 z-[10000] flex min-h-dvh w-full flex-col items-center justify-center bg-[#151937 bg-linear-to-t from-stone-900/95 to-gray-950/95 backdrop-blur-xl ${
fading ? "eb-theme-loader--out" : ""
}`}
role="status"
aria-live="polite"
aria-busy={!fading}
aria-label="Loading theme"
>
<div
className="size-10 animate-spin rounded-full border-[3px] border-white/25 border-t-white"
aria-hidden
/>
<span className="sr-only">Loading…</span>
</div>
);
}

View File

@ -0,0 +1,64 @@
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { getPublicVisitorTheme } from "../lib/api";
import { applyEventTheme } from "../theme/eventTheme";
import { readStoredVisitorContext } from "../types/visitorContext";
function resolveEventIdFromSearch(searchParams: URLSearchParams): string {
const fromEventId = searchParams.get("event_id")?.trim() || "";
if (fromEventId) return fromEventId;
const fromEventIdCamel = searchParams.get("eventId")?.trim() || "";
if (fromEventIdCamel) return fromEventIdCamel;
const type = searchParams.get("type");
const id = searchParams.get("id")?.trim() || "";
if (type === "event" && id) return id;
return readStoredVisitorContext()?.eventId || "";
}
export type UseEventThemeResult = {
/** Event id used for the public theme request (empty when none). */
eventId: string;
/**
* True when either there is no event to theme, or the theme request has
* finished (success or failure). Gate UI on this to avoid a default→theme flash.
*/
ready: boolean;
};
/**
* Fetches GET /api/public/visitor-theme/:eventId (from URL / stored context)
* and applies CSS variables to `<html>`.
*/
export function useEventTheme(): UseEventThemeResult {
const [searchParams] = useSearchParams();
const eventId = resolveEventIdFromSearch(searchParams);
const [ready, setReady] = useState(!eventId);
useEffect(() => {
if (!eventId) {
setReady(true);
return;
}
let cancelled = false;
setReady(false);
getPublicVisitorTheme(eventId)
.then((data) => {
if (cancelled) return;
applyEventTheme(data.theme);
setReady(true);
})
.catch(() => {
if (cancelled) return;
// Still unblock UI; keep defaults / any previously applied theme.
setReady(true);
});
return () => {
cancelled = true;
};
}, [eventId]);
return { eventId, ready };
}

View File

@ -15,15 +15,20 @@
html { html {
min-height: 100dvh; min-height: 100dvh;
background-color: #f2f2f7; background-color: var(--theme-bg-color, #f2f2f7);
background-image: var(--theme-bg-image, none);
background-size: var(--theme-bg-size, auto);
background-repeat: var(--theme-bg-repeat, no-repeat);
background-position: var(--theme-bg-position, center center);
background-attachment: fixed;
} }
body { body {
margin: 0; margin: 0;
min-height: 100dvh; min-height: 100dvh;
font-family: var(--font-sans); font-family: var(--theme-body-font, var(--font-sans));
color: var(--color-eb-text); color: var(--color-eb-text);
background-color: #f2f2f7; background: transparent;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
@ -31,35 +36,42 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100dvh; min-height: 100dvh;
background-color: #f2f2f7; background: transparent;
} }
.eb-gradient-banner { .eb-gradient-banner {
background: linear-gradient( background: linear-gradient(
34.8deg, 34.8deg,
rgb(29, 102, 239) 0%, var(--theme-primary-color, rgb(29, 102, 239)) 0%,
rgb(30, 121, 240) 45%, var(--theme-primary-color, rgb(30, 121, 240)) 45%,
rgb(189, 53, 230) 100% var(--theme-secondary-color, rgb(189, 53, 230)) 100%
); );
} }
.eb-gradient-fab { .eb-gradient-fab {
background: linear-gradient( background: linear-gradient(
65deg, 65deg,
rgb(29, 102, 239) 0%, var(--theme-primary-color, rgb(29, 102, 239)) 0%,
rgb(30, 121, 240) 40%, var(--theme-primary-color, rgb(30, 121, 240)) 40%,
rgb(189, 53, 230) 100% var(--theme-secondary-color, rgb(189, 53, 230)) 100%
); );
} }
/* Auth / modal shells (Figma EventBuzz_Event_Mobile_V1_Register) */ /* Auth / modal shells (Figma EventBuzz_Event_Mobile_V1_Register) */
.eb-gradient-auth { .eb-gradient-auth {
background: linear-gradient( background-color: var(--theme-bg-color, transparent);
background-image: var(
--theme-bg-image,
linear-gradient(
77.92deg, 77.92deg,
rgb(29, 102, 239) 0%, rgb(29, 102, 239) 0%,
rgb(30, 121, 240) 45%, rgb(30, 121, 240) 45%,
rgb(189, 53, 230) 100% rgb(189, 53, 230) 100%
)
); );
background-size: var(--theme-bg-size, cover);
background-repeat: var(--theme-bg-repeat, no-repeat);
background-position: var(--theme-bg-position, center center);
} }
/* Rewards / Leaderboard “Total Points” card */ /* Rewards / Leaderboard “Total Points” card */
@ -116,6 +128,36 @@ body {
animation: eb-live-blink 1.2s ease-in-out infinite; animation: eb-live-blink 1.2s ease-in-out infinite;
} }
.card-bg {
background-color: var(--theme-card-color, #ffffff);
color: var(--theme-card-text-color, #333333);
backdrop-filter: blur(10px);
}
.theme-btn-bg {
background-color: var(--theme-button-color, #ef8d1d);
color: var(--theme-button-text-color, #ffffff);
}
.theme-btn-link {
color: var(--theme-button-color, #ef8d1d);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
text-decoration-color: var(--theme-button-color, #ef8d1d);
text-decoration-style: solid;
text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
/* @layer utilities {
.btn-bg {
background-color: var(--theme-button-color, #ef8d1d);
color: var(--theme-button-text-color, #ffffff);
}
.btn-bg:hover {
background-color: var(--theme-primary-color, #ef8d1d);
color: var(--theme-button-text-color, #ffffff);
}
} */
@keyframes eb-live-blink { @keyframes eb-live-blink {
0%, 0%,
100% { 100% {
@ -127,3 +169,14 @@ body {
transform: scale(0.85); transform: scale(0.85);
} }
} }
/* Theme bootstrap loader — held briefly then faded over themed content */
.eb-theme-loader {
opacity: 1;
transition: opacity 0.48s ease;
pointer-events: auto;
}
.eb-theme-loader--out {
opacity: 0;
pointer-events: none;
}

View File

@ -1,6 +1,11 @@
import type { AttendeeParticipant } from "./attendeeAuth"; import type { AttendeeParticipant } from "./attendeeAuth";
import { getAttendeeToken } from "./attendeeAuth"; import { getAttendeeToken } from "./attendeeAuth";
import type { LoginPreviewResponse, ResolveCodeResponse, VisitorJoinType } from "../types/visitorContext"; import type {
EventThemeConfig,
LoginPreviewResponse,
ResolveCodeResponse,
VisitorJoinType,
} from "../types/visitorContext";
function getBaseUrl(): string { function getBaseUrl(): string {
const raw = import.meta.env.VITE_API_BASE_URL; const raw = import.meta.env.VITE_API_BASE_URL;
@ -68,6 +73,30 @@ export async function getVisitorLoginPreview(eventId: string): Promise<LoginPrev
return (body as { data: LoginPreviewResponse }).data; return (body as { data: LoginPreviewResponse }).data;
} }
/** Public visitor theme for an event — GET /api/public/visitor-theme/:eventId */
export type PublicVisitorThemeResponse = {
eventId: string;
theme: EventThemeConfig;
};
export async function getPublicVisitorTheme(eventId: string): Promise<PublicVisitorThemeResponse> {
const base = getBaseUrl();
const url = `${base}/api/public/visitor-theme/${encodeURIComponent(eventId)}`;
const res = await fetch(url);
const body = await parseJson(res);
if (!res.ok) {
throw new Error(messageFromBody(body));
}
if (!body || typeof body !== "object" || !("data" in body)) {
throw new Error("Invalid response");
}
const data = (body as { data: PublicVisitorThemeResponse }).data;
if (!data?.eventId || !data.theme) {
throw new Error("Invalid response");
}
return data;
}
export type VisitorEventTrackListItem = { export type VisitorEventTrackListItem = {
_id: string; _id: string;
event: string; event: string;

View File

@ -45,8 +45,8 @@ import {
function WhiteCard({ children }: { children: ReactNode }) { function WhiteCard({ children }: { children: ReactNode }) {
return ( return (
<div className="flex w-full max-w-[354px] flex-col items-center"> <div className="flex w-full max-w-[354px] flex-col items-center">
<div className="w-full rounded-xl shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]"> <div className="w-full rounded-xl shadow-md">
<div className="rounded-xl bg-white px-6 py-6">{children}</div> <div className="rounded-xl bg-whit card-bg px-6 py-6">{children}</div>
</div> </div>
</div> </div>
); );
@ -410,24 +410,24 @@ export function AiAvatarPage() {
</div> </div>
) : null} ) : null}
<div className="flex flex-col items-center py-4 text-center"> <div className="flex flex-col items-center py-4 text-center">
<div className="mb-5 flex h-16 w-16 items-center justify-center rounded-full bg-[#fdecea]"> <div className="mb-5 flex h-16 w-16 items-center justify-center rounded-full bg-[var(--theme-button-color)]/10">
<Loader2 className="h-8 w-8 animate-spin text-[#D32F2F]" aria-hidden /> <Loader2 className="h-8 w-8 animate-spin text-[var(--theme-button-color)]" aria-hidden />
</div> </div>
<h1 className="mb-2 text-[22px] font-semibold text-[#1a1a1a]">Creating your avatar</h1> <h1 className="mb-2 text-[22px] font-semibold text-[var(--theme-card-text-color)]">Creating your avatar</h1>
<p <p
key={pipelineMsgIndex} key={pipelineMsgIndex}
className="mb-5 min-h-[2.5rem] text-sm text-[#666] transition-opacity duration-300" className="mb-5 min-h-[2.5rem] text-sm text-[var(--theme-card-text-color)]/80 transition-opacity duration-300"
aria-live="polite" aria-live="polite"
> >
{GENERATING_PIPELINE_MESSAGES[pipelineMsgIndex]} {GENERATING_PIPELINE_MESSAGES[pipelineMsgIndex]}
</p> </p>
<div className="mb-2 h-1.5 w-full overflow-hidden rounded-full bg-[#eee]"> <div className="mb-2 h-1.5 w-full overflow-hidden rounded-full bg-[var(--theme-card-text-color)]/5">
<div <div
className="h-full rounded-full bg-[#D32F2F] transition-[width] duration-700 ease-out" className="h-full rounded-full bg-[var(--theme-button-color)] transition-[width] duration-700 ease-out"
style={{ width: `${progressPct}%` }} style={{ width: `${progressPct}%` }}
/> />
</div> </div>
<p className="text-xs text-[#999]">This can take up to a minute — hang tight</p> <p className="text-xs text-[var(--theme-card-text-color)]/60">This can take up to a minute — hang tight</p>
</div> </div>
</WhiteCard> </WhiteCard>
</AuthShell> </AuthShell>
@ -447,12 +447,12 @@ export function AiAvatarPage() {
/> />
</div> </div>
) : null} ) : null}
<h1 className="mb-2 text-center text-[22px] font-semibold text-[#1a1a1a]">Your avatar</h1> <h1 className="mb-2 text-center text-[22px] font-semibold">Your avatar</h1>
<p className="mb-4 text-center text-sm text-[#666]"> <p className="mb-4 text-center text-sm text-[var(--theme-card-text-color)]/80">
Here’s your AI avatar. You can download or share it. Here's your AI avatar. You can download or share it.
</p> </p>
{(done.fileUrl || done.previewUrl) ? ( {(done.fileUrl || done.previewUrl) ? (
<div className="mb-4 overflow-hidden rounded-lg border border-[#e5e5e5] bg-[#f8f8f8]"> <div className="mb-4 overflow-hidden rounded-lg">
<img <img
ref={resultImgRef} ref={resultImgRef}
crossOrigin="anonymous" crossOrigin="anonymous"
@ -477,7 +477,7 @@ export function AiAvatarPage() {
<button <button
type="button" type="button"
onClick={resetForNew} onClick={resetForNew}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-[#D32F2F] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c]" className="inline-flex h-11 w-full items-center justify-center rounded-lg theme-btn-bg px-4 text-sm font-semibold text-white"
> >
Create another Create another
</button> </button>
@ -485,7 +485,7 @@ export function AiAvatarPage() {
<button <button
type="button" type="button"
onClick={() => void onDownload()} onClick={() => void onDownload()}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-[#ddd] bg-white px-4 text-sm font-semibold text-[#333] hover:bg-[#fafafa]" className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-zinc-300 bg-zinc-100 px-4 text-sm font-semibold text-gray-600"
> >
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />
Download photo Download photo
@ -494,14 +494,14 @@ export function AiAvatarPage() {
</div> </div>
{showShare ? ( {showShare ? (
<div className="mt-4"> <div className="mt-4">
<p className="mb-2 text-center text-xs font-semibold uppercase tracking-wide text-[#888]"> <p className="mb-2 text-center text-xs font-semibold uppercase tracking-wide text-[var(--theme-card-text-color)]/60">
Share Share
</p> </p>
{useNativeShare ? ( {useNativeShare ? (
<button <button
type="button" type="button"
onClick={() => void onNativeShare()} onClick={() => void onNativeShare()}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-[#ddd] bg-white px-4 text-sm font-semibold text-[#333] hover:bg-[#fafafa]" className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-zinc-300 bg-zinc-100 px-4 text-sm font-semibold text-gray-600"
> >
<Share2 className="h-4 w-4" /> <Share2 className="h-4 w-4" />
Share Share
@ -513,7 +513,7 @@ export function AiAvatarPage() {
key={item.key} key={item.key}
type="button" type="button"
onClick={() => void onShare(item.key)} onClick={() => void onShare(item.key)}
className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-[#ddd] bg-white px-3 text-xs font-semibold text-[#333] hover:bg-[#fafafa]" className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-zinc-300 bg-zinc-100 px-3 text-xs font-semibold text-gray-600"
> >
<ShareIcon platform={item.key} /> <ShareIcon platform={item.key} />
{item.label} {item.label}
@ -544,22 +544,22 @@ export function AiAvatarPage() {
{meta?.accessMode === "login" && !loggedIn ? ( {meta?.accessMode === "login" && !loggedIn ? (
<div className="space-y-3"> <div className="space-y-3">
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">AI Avatar</h1> <h1 className="mb-1 text-center text-[22px] font-semibold text-[var(--theme-card-text-color)]">AI Avatar</h1>
<p className="text-center text-sm text-[#666]">Sign in to create your AI avatar.</p> <p className="text-center text-sm text-[var(--theme-card-text-color)]/80">Sign in to create your AI avatar.</p>
<button <button
type="button" type="button"
onClick={() => void goLogin()} onClick={() => void goLogin()}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-[#D32F2F] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c]" className="inline-flex h-11 w-full items-center justify-center rounded-lg theme-btn-bg px-4 text-sm font-semibold text-white"
> >
Sign in to continue Sign in to continue
</button> </button>
</div> </div>
) : !selectedAvatar ? ( ) : !selectedAvatar ? (
<div className="space-y-4"> <div className="space-y-4">
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Choose an avatar</h1> <h1 className="mb-1 text-center text-[22px] font-semibold text-[var(--theme-card-text-color)]">Choose an avatar</h1>
<p className="text-center text-sm text-[#666]">Pick an avatar, then upload your selfie.</p> <p className="text-center text-sm text-[var(--theme-card-text-color)]/80">Pick an avatar, then upload your selfie.</p>
{!meta?.avatars?.length ? ( {!meta?.avatars?.length ? (
<p className="text-center text-sm text-[#666]">No avatars available yet.</p> <p className="text-center text-sm text-[var(--theme-card-text-color)]/80">No avatars available yet.</p>
) : ( ) : (
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{meta.avatars.map((avatar) => ( {meta.avatars.map((avatar) => (
@ -567,9 +567,9 @@ export function AiAvatarPage() {
key={avatar._id} key={avatar._id}
type="button" type="button"
onClick={() => setSelectedAvatar(avatar)} onClick={() => setSelectedAvatar(avatar)}
className="overflow-hidden rounded-lg border border-[#e5e5e5] bg-[#f8f8f8] text-left hover:border-[#D32F2F]" className="overflow-hidden rounded-lg border border-[var(--theme-card-text-color)]/15 text-left hover:border-[var(--theme-card-text-color)]/50"
> >
<div className="aspect-square w-full bg-white"> <div className="aspect-square w-full">
{avatar.imageUrl ? ( {avatar.imageUrl ? (
<img <img
src={avatar.imageUrl} src={avatar.imageUrl}
@ -577,12 +577,12 @@ export function AiAvatarPage() {
className="h-full w-full object-contain" className="h-full w-full object-contain"
/> />
) : ( ) : (
<div className="flex h-full items-center justify-center text-xs text-[#999]"> <div className="flex h-full items-center justify-center text-xs text-[var(--theme-card-text-color)]/60">
No preview No preview
</div> </div>
)} )}
</div> </div>
<p className="truncate px-2 py-2 text-center text-xs font-semibold text-[#333]"> <p className="truncate px-2 py-2 text-center text-xs font-semibold">
{avatar.name} {avatar.name}
</p> </p>
</button> </button>
@ -592,7 +592,7 @@ export function AiAvatarPage() {
{err ? <p className="text-center text-sm text-red-600">{err}</p> : null} {err ? <p className="text-center text-sm text-red-600">{err}</p> : null}
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4 text-center">
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
@ -600,13 +600,13 @@ export function AiAvatarPage() {
clearSelectedPhoto(); clearSelectedPhoto();
setErr(null); setErr(null);
}} }}
className="text-sm font-semibold text-[#D32F2F]" className="text-sm font-semibold theme-btn-link"
> >
← Change avatar ← Change avatar
</button> </button>
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Upload selfie</h1> <h1 className="mb-1 text-center text-[22px] font-semibold text-[var(--theme-card-text-color)]">Upload selfie</h1>
<p className="text-center text-sm text-[#666]"> <p className="text-center text-sm text-[var(--theme-card-text-color)]/80">
Avatar: <span className="font-semibold text-[#333]">{selectedAvatar.name}</span> Avatar: <span className="font-semibold">{selectedAvatar.name}</span>
</p> </p>
<input <input
@ -618,7 +618,7 @@ export function AiAvatarPage() {
onChange={(e) => void handleFileChange(e.target.files?.[0] || null)} onChange={(e) => void handleFileChange(e.target.files?.[0] || null)}
/> />
{previewUrl ? ( {previewUrl ? (
<div className="overflow-hidden rounded-lg border border-[#e5e5e5] bg-[#f8f8f8]"> <div className="overflow-hidden rounded-lg">
<img src={previewUrl} alt="Your selfie" className="mx-auto max-h-[240px] w-full object-contain" /> <img src={previewUrl} alt="Your selfie" className="mx-auto max-h-[240px] w-full object-contain" />
</div> </div>
) : null} ) : null}
@ -626,7 +626,7 @@ export function AiAvatarPage() {
type="button" type="button"
disabled={picking} disabled={picking}
onClick={() => fileRef.current?.click()} onClick={() => fileRef.current?.click()}
className="inline-flex h-11 w-full items-center justify-center rounded-lg border border-[#ddd] bg-white px-4 text-sm font-semibold text-[#333] hover:bg-[#fafafa] disabled:opacity-55" className="inline-flex h-11 w-full items-center justify-center rounded-lg border border-zinc-300 bg-zinc-100 px-4 text-sm font-semibold text-gray-600 disabled:opacity-55"
> >
{picking ? "Processing…" : file ? "Change selfie" : "Choose selfie"} {picking ? "Processing…" : file ? "Change selfie" : "Choose selfie"}
</button> </button>
@ -634,7 +634,7 @@ export function AiAvatarPage() {
type="button" type="button"
disabled={!file || submitting} disabled={!file || submitting}
onClick={() => void onSubmit()} onClick={() => void onSubmit()}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-[#D32F2F] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c] disabled:opacity-55" className="inline-flex h-11 w-full items-center justify-center rounded-lg theme-btn-bg px-4 text-sm font-semibold text-white hover:bg-[#b71c1c] disabled:opacity-55"
> >
{submitting ? "Creating avatar…" : "Create avatar"} {submitting ? "Creating avatar…" : "Create avatar"}
</button> </button>

View File

@ -121,9 +121,9 @@ export function EventLandingPage() {
if (!eventId) { if (!eventId) {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile showRewardsAndLogout={false} showBack={false} /> <HeaderMobile showRewardsAndLogout={false} showBack={false} />
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 flex-col items-center justify-center px-4 py-10">
<p className="text-center text-sm text-[#24282f]">Open this page from your event link or sign in again.</p> <p className="text-center text-sm text-[#24282f]">Open this page from your event link or sign in again.</p>
<Link to="/" className="mt-4 text-sm font-semibold text-[#1e72ef] underline"> <Link to="/" className="mt-4 text-sm font-semibold text-[#1e72ef] underline">
Home Home
@ -135,9 +135,9 @@ export function EventLandingPage() {
if (!event) { if (!event) {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile logoUrl={ctx?.event?.logoUrl} eventId={eventId || null} showBack={false} /> <HeaderMobile logoUrl={ctx?.event?.logoUrl} eventId={eventId || null} showBack={false} />
<main className="flex flex-1 items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 items-center justify-center px-4 py-10">
<p className="text-sm text-[#24282f]">Loading event…</p> <p className="text-sm text-[#24282f]">Loading event…</p>
</main> </main>
</MobileShell> </MobileShell>
@ -145,40 +145,40 @@ export function EventLandingPage() {
} }
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile logoUrl={event.logoUrl} eventId={eventId || null} showBack={false} /> <HeaderMobile logoUrl={event.logoUrl} eventId={eventId || null} showBack={false} />
<EventBanner imageUrl={bannerSrc} alt={event.name} /> <EventBanner imageUrl={bannerSrc} alt={event.name} className="rounded-b-lg shadow-sm" />
<main className="relative z-0 flex w-full max-w-none flex-1 flex-col bg-[#f2f2f7] pb-8 pt-6"> <main className="relative z-0 flex w-full max-w-none flex-1 flex-col pb-8 pt-6">
<div className="flex w-full max-w-none flex-col gap-6"> <div className="flex w-full max-w-none flex-col gap-6 px-4">
<section className="w-full"> <section className="w-full">
<div className="w-full rounded-lg border border-[rgba(116,112,112,0.2)] bg-white p-4 shadow-sm"> <div className="w-full rounded-lg card-bg p-4 shadow-md">
<h1 className="text-lg font-semibold text-[#24282f]">{event.name}</h1> <h1 className="text-lg font-semibold">{event.name}</h1>
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs"> <div className="mt-2 flex flex-wrap items-center gap-2 text-xs">
<EventRatingBadge ratings={event.ratings} /> <EventRatingBadge ratings={event.ratings} />
{reg.enableRatings ? ( {reg.enableRatings ? (
<Link to={ratingsHref} className="font-inter font-semibold uppercase tracking-wide text-[#1e72ef]"> <Link to={ratingsHref} className="font-semibold uppercase tracking-wide theme-btn-link">
rate now rate now
</Link> </Link>
) : ( ) : (
<span className="font-inter text-[#8e8e93]">Ratings off</span> <span className="text-[var(--theme-card-text-color)]/50">Ratings off</span>
)} )}
</div> </div>
<p className="mt-3 text-base leading-5 text-[rgba(36,40,47,0.87)]"> <p className="mt-3 text-sm leading-5 text-[var(--theme-card-text-color)]">
{event.description?.trim() || event.subtitle?.trim() || "Welcome to this event."} {event.description?.trim() || event.subtitle?.trim() || "Welcome to this event."}
</p> </p>
<button type="button" className="mt-2 font-inter text-xs font-semibold uppercase tracking-wide text-[#1e72ef]"> {/* <button type="button" className="mt-2 text-xs font-semibold uppercase tracking-wide text-[#1e72e theme-btn-link">
Read More Read More
</button> </button> */}
<div className="mt-4 flex flex-col gap-2 border-t border-[#f2f2f7] pt-4 text-xs text-[#24282f]"> <div className="mt-4 flex flex-col gap-2 border-t border-[var(--theme-card-text-color)]/5 pt-4 text-xs text-[var(--theme-card-text-color)]/95">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calendar className="size-4 shrink-0 text-[#8e8e93]" strokeWidth={1.75} /> <Calendar className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="font-inter">{schedule || "—"}</span> <span className="">{schedule || "—"}</span>
</div> </div>
{event.location?.trim() ? ( {event.location?.trim() ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<MapPin className="size-4 shrink-0 text-[#8e8e93]" strokeWidth={1.75} /> <MapPin className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="font-inter">{event.location.trim()}</span> <span className="">{event.location.trim()}</span>
</div> </div>
) : null} ) : null}
</div> </div>
@ -186,12 +186,12 @@ export function EventLandingPage() {
</section> </section>
{bookmarkedIds.size > 0 ? ( {bookmarkedIds.size > 0 ? (
<section className="w-full"> <section className="w-full card-bg rounded-lg shadow-md p-4">
<div className="w-full px-4">
<h2 className="font-inter text-base font-semibold text-[#24282f]">My Sessions</h2> <h2 className="text-md font-semibold">My Sessions</h2>
</div>
{agendaLoading && !agendaSessions.length ? ( {agendaLoading && !agendaSessions.length ? (
<p className="mt-3 px-4 text-sm text-[rgba(36,40,47,0.55)]">Loading…</p> <p className="mt-3 px-4 text-sm text-[var(--theme-card-text-color)]/80">Loading…</p>
) : ( ) : (
<LandingMySessionsList <LandingMySessionsList
sessions={bookmarkedSessionsSorted} sessions={bookmarkedSessionsSorted}
@ -203,17 +203,17 @@ export function EventLandingPage() {
</section> </section>
) : null} ) : null}
<section className="w-full"> <section className="w-full card-bg rounded-lg shadow-md p-4">
<div className="w-full px-4">
<h2 className="font-inter text-base font-semibold text-[#24282f]">Agenda</h2> <h2 className="text-base font-semibold">Agenda</h2>
</div>
{agendaErr ? ( {agendaErr ? (
<p className="mt-3 px-4 text-sm text-red-600">{agendaErr}</p> <p className="mt-3 px-4 text-sm text-red-600">{agendaErr}</p>
) : null} ) : null}
{agendaLoading && !agendaSessions.length ? ( {agendaLoading && !agendaSessions.length ? (
<p className="mt-3 px-4 text-sm text-[rgba(36,40,47,0.55)]">Loading agenda…</p> <p className="mt-3 px-4 text-sm text-[var(--theme-card-text-color)]/80">Loading agenda…</p>
) : agendaDays.length === 0 ? ( ) : agendaDays.length === 0 ? (
<p className="mt-3 px-4 text-sm text-[rgba(36,40,47,0.55)]">No sessions scheduled for this event yet.</p> <p className="mt-3 px-4 text-sm text-[var(--theme-card-text-color)]/80">No sessions scheduled for this event yet.</p>
) : ( ) : (
<LandingAgendaCards <LandingAgendaCards
days={agendaDays} days={agendaDays}

View File

@ -109,11 +109,11 @@ export function EventTrackPage() {
if (!eventId || !trackId) { if (!eventId || !trackId) {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile eventId={eventId || null} /> <HeaderMobile eventId={eventId || null} />
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 flex-col items-center justify-center px-4 py-10">
<p className="text-center text-sm text-[#24282f]">{err}</p> <p className="text-center text-sm text-[#24282f]">{err}</p>
<Link to="/" className="mt-4 text-sm font-semibold text-[#1e72ef] underline"> <Link to="/" className="mt-4 text-sm font-semibold theme-btn-link underline">
Home Home
</Link> </Link>
</main> </main>
@ -123,10 +123,10 @@ export function EventTrackPage() {
if (loading) { if (loading) {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile eventId={eventId || null} /> <HeaderMobile eventId={eventId || null} />
<main className="flex flex-1 items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 items-center justify-center px-4 py-10">
<p className="text-sm text-[#24282f]">Loading track…</p> <p className="text-sm text-[var(--theme-card-text-color)]">Loading track…</p>
</main> </main>
</MobileShell> </MobileShell>
); );
@ -134,9 +134,9 @@ export function EventTrackPage() {
if (err || !track) { if (err || !track) {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile eventId={eventId || null} /> <HeaderMobile eventId={eventId || null} />
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 flex-col items-center justify-center px-4 py-10">
<p className="text-center text-sm text-red-600">{err ?? "Track not found."}</p> <p className="text-center text-sm text-red-600">{err ?? "Track not found."}</p>
</main> </main>
</MobileShell> </MobileShell>
@ -160,42 +160,44 @@ export function EventTrackPage() {
(ctx?.eventId === eventId && ctx.event?.logoUrl?.trim() ? ctx.event.logoUrl.trim() : undefined); (ctx?.eventId === eventId && ctx.event?.logoUrl?.trim() ? ctx.event.logoUrl.trim() : undefined);
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile logoUrl={headerLogo} eventId={eventId || null} /> <HeaderMobile logoUrl={headerLogo} eventId={eventId || null} />
<EventBanner /> <EventBanner className="rounded-b-lg border-none" />
<main className="relative z-0 flex flex-1 flex-col bg-[#f2f2f7] px-4 pb-28 pt-6"> <main className="relative z-0 flex flex-1 flex-col px-4 pb-28 pt-6">
<div className="mx-auto w-full max-w-[370px]"> <div className="mx-auto w-full max-w-[370px]">
<div className="mb-6 flex flex-col gap-4"> <div className="mb-6 flex flex-col gap-4 card-bg p-4 rounded-lg">
<div className="inline-flex w-fit rounded bg-[#e5e5ea] px-2 py-0.5"> <div className="inline-flex w-fit rounded bg-zinc-200 px-2 py-0.5">
<span className="text-[10px] tracking-wide text-[#24282f]">{eventName.toUpperCase()}</span> <span className="text-[10px] tracking-wide">{eventName.toUpperCase()}</span>
</div> </div>
<h1 className="text-lg font-semibold tracking-wide text-[#24282f]">{track.name}</h1> <h1 className="text-lg font-semibold tracking-wide">{track.name}</h1>
{track.hall?.trim() ? ( {track.hall?.trim() ? (
<p className="text-xs text-[rgba(36,40,47,0.7)]">{track.hall.trim()}</p> <p className="text-xs text-[var(--theme-card-text-color)]/80">{track.hall.trim()}</p>
) : null} ) : null}
<p className="text-[10px] uppercase tracking-wide text-[#8e8e93]">{trackTypeLabel}</p> <p className="text-xs uppercase font-semibold tracking-wide text-[var(--theme-card-text-color)]/70">{trackTypeLabel}</p>
<div className="flex flex-col gap-1 text-xs text-[#24282f]"> <div className="flex flex-col gap-2 text-xs text-[var(--theme-card-text-color)]/80">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calendar className="size-4 shrink-0" strokeWidth={1.75} /> <Calendar className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="font-inter tracking-wide">{schedule || "—"}</span> <span className="font-inter tracking-wide">{schedule || "—"}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clock className="size-4 shrink-0" strokeWidth={1.75} /> <Clock className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="font-inter tracking-wide">See sessions below</span> <span className="font-inter tracking-wide">See sessions below</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<User className="size-4 shrink-0" strokeWidth={1.75} /> <User className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="font-inter tracking-wide">Track agenda (sample)</span> <span className="font-inter tracking-wide">Track agenda (sample)</span>
</div> </div>
</div> </div>
</div> </div>
<div className="mb-4 flex w-full max-w-[370px] flex-col gap-2"> <div className="mb-4 flex w-full max-w-[370px] flex-col gap-2">
<h2 className="font-inter text-base font-semibold text-[#24282f]">Sessions</h2> <div className="card-bg p-4 rounded-lg">
<h2 className="font-inter text-base font-semibold mb-2">Sessions</h2>
<button <button
type="button" type="button"
className="flex h-[43px] w-full items-center justify-between rounded-lg border border-[rgba(116,112,112,0.3)] bg-white px-4 py-3" className="flex h-[43px] w-full items-center justify-between rounded-lg border border-[var(--theme-button-color)]/10 bg-[var(--theme-button-color)]/9 px-4 py-3"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calendar className="size-4 shrink-0 text-[#24282f]" strokeWidth={1.75} /> <Calendar className="size-4 shrink-0 text-[#24282f]" strokeWidth={1.75} />
@ -204,8 +206,9 @@ export function EventTrackPage() {
<ChevronDown className="size-4 shrink-0 text-[#1d66ef]" strokeWidth={2} /> <ChevronDown className="size-4 shrink-0 text-[#1d66ef]" strokeWidth={2} />
</button> </button>
</div> </div>
</div>
<div className="relative rounded-md bg-white p-4 shadow-[0_1px_4px_rgba(12,12,13,0.05)]"> <div className="relative rounded-lg card-bg p-4 shadow-md">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{agendaRows.map((row) => ( {agendaRows.map((row) => (
<AgendaRow key={row.time + row.sessionType} row={row} /> <AgendaRow key={row.time + row.sessionType} row={row} />
@ -213,9 +216,9 @@ export function EventTrackPage() {
</div> </div>
</div> </div>
<p className="mt-6 text-center text-xs text-[#24282f]/60"> <p className="mt-6 text-center text-sm">
<Link <Link
className="text-[#1e72ef] underline" className="text-[var(--theme-text-color)] underline"
to={`/questions?event_id=${encodeURIComponent(eventId)}`} to={`/questions?event_id=${encodeURIComponent(eventId)}`}
> >
Ask &amp; session questions Ask &amp; session questions

View File

@ -43,50 +43,50 @@ export function EventTrackV2Page() {
const [tab, setTab] = useState(0); const [tab, setTab] = useState(0);
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile <HeaderMobile
logoUrl={ctx?.eventId === eventId ? ctx.event?.logoUrl : undefined} logoUrl={ctx?.eventId === eventId ? ctx.event?.logoUrl : undefined}
eventId={eventId || null} eventId={eventId || null}
/> />
<EventBanner /> <EventBanner className="rounded-b-lg border-none" />
<main className="relative z-0 flex flex-1 flex-col bg-[#f2f2f7] px-4 pb-28 pt-6"> <main className="relative z-0 flex flex-1 flex-col px-4 pb-28 pt-6">
<div className="mx-auto w-full max-w-[370px]"> <div className="mx-auto w-full max-w-[370px]">
<div className="mb-4 flex flex-col gap-4"> <div className="mb-4 flex flex-col gap-4 card-bg p-4 rounded-lg">
<span className="inline-flex w-fit rounded bg-[#e5e5ea] px-2 py-0.5 text-[10px] text-[#24282f]"> <span className="inline-flex w-fit rounded bg-zinc-200 px-2 py-0.5 text-[10px] text-gray-600">
NAPCON JAIPUR 2026 NAPCON JAIPUR 2026
</span> </span>
<h1 className="text-lg font-semibold text-[#24282f]">This will be Track Name</h1> <h1 className="text-lg font-semibold">This will be Track Name</h1>
<div className="flex items-center gap-2 text-xs text-[#24282f]"> <div className="flex items-center gap-2 text-xs">
<MapPin className="size-4 shrink-0" strokeWidth={1.75} /> <MapPin className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="font-inter">Health and Wellness Expo</span> <span className="font-inter">Health and Wellness Expo</span>
</div> </div>
<div className="flex flex-col gap-1 text-xs text-[#24282f]"> <div className="flex flex-col gap-2 text-xs">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calendar className="size-4" strokeWidth={1.75} /> <Calendar className="size-4 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="font-inter">November 22, 2025 - November 23, 2025</span> <span className="font-inter">November 22, 2025 - November 23, 2025</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clock className="size-4" strokeWidth={1.75} /> <Clock className="size-4 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="font-inter">6:00 PM - 10:00 PM</span> <span className="font-inter">6:00 PM - 10:00 PM</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<User className="size-4" strokeWidth={1.75} /> <User className="size-4 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="font-inter">400 Participants (Capacity)</span> <span className="font-inter">400 Participants (Capacity)</span>
</div> </div>
</div> </div>
</div> </div>
<div className="mb-4 flex w-full border-b-2 border-[#c7c7cc]"> <div className="mb-4 flex w-full card-bg p-4 py-2 rounded-lg">
{dates.map((d, i) => ( {dates.map((d, i) => (
<button <button
key={d} key={d}
type="button" type="button"
onClick={() => setTab(i)} onClick={() => setTab(i)}
className={`flex-1 pb-2 pt-1 text-center text-base ${ className={`flex-1 pb-2 pt-1 text-center text-xs ${
tab === i tab === i
? "border-b-2 border-[#1e72ef] font-semibold text-[#24282f]" ? "border-b-2 border-[var(--theme-button-color)] font-semibold"
: "font-normal text-[#24282f]" : "font-normal"
}`} }`}
style={{ marginBottom: -2 }} style={{ marginBottom: -2 }}
> >
@ -95,7 +95,7 @@ export function EventTrackV2Page() {
))} ))}
</div> </div>
<div className="relative rounded-md bg-white p-4 shadow-[0_1px_4px_rgba(12,12,13,0.05)]"> <div className="relative rounded-md card-bg p-4 shadow-md">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{rowsV2.map((row) => ( {rowsV2.map((row) => (
<AgendaRowTrackV2 key={row.time + row.sessionType} row={row} /> <AgendaRowTrackV2 key={row.time + row.sessionType} row={row} />
@ -103,8 +103,8 @@ export function EventTrackV2Page() {
</div> </div>
</div> </div>
<p className="mt-4 text-center text-xs"> <p className="mt-4 text-center text-sm">
<Link className="text-[#1e72ef] underline" to="/event"> <Link className="text-[var(--theme-text-color)] underline" to="/event">
Back to event Back to event
</Link> </Link>
</p> </p>

View File

@ -98,14 +98,16 @@ export function FeedbackPage() {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative">
<HeaderMobile logoUrl={ctx?.event?.logoUrl} eventId={null} /> <HeaderMobile logoUrl={ctx?.event?.logoUrl} eventId={null} />
<main className="flex flex-1 flex-col bg-[#f2f2f7] px-4 pb-10 pt-6"> <main className="flex flex-1 flex-col px-4 pb-10 pt-6">
<div className="mx-auto w-full max-w-[370px]"> <div className="mx-auto w-full max-w-[370px]">
<h1 className="font-inter text-xl font-semibold text-[#24282f]">Feedback</h1> <div className="card-bg rounded-lg shadow-md p-4">
<p className="mt-3 text-sm text-[rgba(36,40,47,0.67)]">Open feedback from your event or session link.</p> <h1 className="font-inter text-xl font-semibold text-[var(--theme-card-text-color)]">Feedback</h1>
<Link to="/" className="mt-4 inline-block text-sm font-semibold text-[#1e72ef] underline"> <p className="mt-3 text-sm text-[var(--theme-card-text-color)]/80">Open feedback from your event or session link.</p>
<Link to="/" className="mt-4 inline-block text-sm font-semibold theme-btn-link">
Home Home
</Link> </Link>
</div> </div>
</div>
</main> </main>
</MobileShell> </MobileShell>
); );
@ -115,14 +117,16 @@ export function FeedbackPage() {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative">
<HeaderMobile logoUrl={eventSnapshot?.logoUrl} eventId={eventId || null} /> <HeaderMobile logoUrl={eventSnapshot?.logoUrl} eventId={eventId || null} />
<main className="flex flex-1 flex-col bg-[#f2f2f7] px-4 pb-10 pt-6"> <main className="flex flex-1 flex-col px-4 pb-10 pt-6">
<div className="mx-auto w-full max-w-[370px]"> <div className="mx-auto w-full max-w-[370px]">
<h1 className="font-inter text-xl font-semibold text-[#24282f]">Feedback</h1> <div className="card-bg rounded-lg shadow-md p-4">
<p className="mt-3 text-sm text-[rgba(36,40,47,0.67)]">Feedback is disabled for this event.</p> <h1 className="text-xl font-semibold text-[var(--theme-card-text-color)]">Feedback</h1>
<Link to="/event" className="mt-4 inline-block text-sm font-semibold text-[#1e72ef] underline"> <p className="mt-3 text-sm text-[var(--theme-card-text-color)]/80">Feedback is disabled for this event.</p>
<Link to="/event" className="mt-4 inline-block text-sm font-semibold theme-btn-link">
Back to event Back to event
</Link> </Link>
</div> </div>
</div>
</main> </main>
</MobileShell> </MobileShell>
); );
@ -131,27 +135,29 @@ export function FeedbackPage() {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative">
<HeaderMobile logoUrl={eventSnapshot?.logoUrl} eventId={eventId || null} /> <HeaderMobile logoUrl={eventSnapshot?.logoUrl} eventId={eventId || null} />
<main className="flex flex-1 flex-col bg-[#f2f2f7] px-4 pb-10 pt-6"> <main className="flex flex-1 flex-col px-4 pb-10 pt-6">
<div className="mx-auto w-full max-w-[370px]"> <div className="mx-auto w-full max-w-[370px]">
<h1 className="font-inter text-xl font-semibold text-[#24282f]">{schema?.name || "Feedback"}</h1> <div className="card-bg rounded-lg shadow-md p-4">
<h1 className="text-xl font-semibold text-[var(--theme-card-text-color)]">{schema?.name || "Feedback"}</h1>
{schemaLoading ? ( {schemaLoading ? (
<p className="mt-4 text-sm text-[rgba(36,40,47,0.67)]">Loading form…</p> <p className="mt-4 text-sm text-[var(--theme-card-text-color)]/80">Loading form…</p>
) : schemaErr ? ( ) : schemaErr ? (
<div className="mt-4"> <div className="mt-4">
<p className="text-sm text-red-600">{schemaErr}</p> <p className="text-sm text-red-600">{schemaErr}</p>
<p className="mt-2 text-sm text-[rgba(36,40,47,0.67)]"> <p className="mt-2 text-sm text-[var(--theme-card-text-color)]/80">
Ask the organizer to publish an active feedback form for this event. Ask the organizer to publish an active feedback form for this event.
</p> </p>
<Link to="/event" className="mt-4 inline-block text-sm font-semibold text-[#1e72ef] underline"> <Link to="/event" className="mt-4 inline-block text-sm font-semibold theme-btn-link">
Back to event Back to event
</Link> </Link>
</div> </div>
) : schema ? ( ) : schema ? (
<div className="mt-6"> <div className="mt-2">
<DynamicFeedbackForm eventId={eventId} schema={schema} variant="light" /> <DynamicFeedbackForm eventId={eventId} schema={schema} variant="light" className="px-0" />
</div> </div>
) : null} ) : null}
</div> </div>
</div>
</main> </main>
</MobileShell> </MobileShell>
); );

View File

@ -9,25 +9,22 @@ export function Home() {
return ( return (
<AuthShell> <AuthShell>
<div className="flex w-full max-w-[354px] flex-col items-center"> <div className="flex w-full max-w-[354px] flex-col items-center">
<div className="w-full overflow-hidden rounded-2xl bg-white shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]"> <div className="w-full overflow-hidden rounded-2xl bg-whit card-bg shadow-md">
<div className="h-1.5 w-full bg-gradient-to-r from-[#34A7F2] via-[#1D66EF] to-[#ef8d1d]" aria-hidden /> {/* <div className="h-1.5 w-full bg-gradient-to-r from-[#34A7F2] via-[#1D66EF] to-[#ef8d1d]" aria-hidden /> */}
<form onSubmit={handleJoin} className="flex flex-col gap-6 px-6 pb-8 pt-7"> <form onSubmit={handleJoin} className="flex flex-col gap-6 px-6 pb-8 pt-7">
<div className="flex flex-col items-center gap-3 text-center"> <div className="flex flex-col items-center gap-3 text-center">
<div <div
className="flex size-12 items-center justify-center rounded-full" className="flex size-12 items-center justify-center rounded-full bg-[var(--theme-button-color)]/10"
style={{
background: "linear-gradient(135deg, rgba(52,167,242,0.12) 0%, rgba(29,102,239,0.16) 100%)",
}}
aria-hidden aria-hidden
> >
<Ticket className="size-6 text-[#1D66EF]" strokeWidth={1.75} /> <Ticket className="size-6 text-[var(--theme-button-color)]" strokeWidth={1.75} />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<h1 className="font-inter text-[22px] font-bold leading-tight text-[#24282f]"> <h1 className="font-inter text-[22px] font-bold leading-tight text-[var(--theme-card-text-color)]">
Join with your code Join with your code
</h1> </h1>
<p className="font-inter text-sm leading-relaxed text-[rgba(36,40,47,0.72)]"> <p className="text-sm leading-relaxed text-[var(--theme-card-text-color)]/80">
Enter the code provided by the event organiser — you’ll usually find it in your Enter the code provided by the event organiser — you’ll usually find it in your
invite, on the event screen, or on a poster at the venue. invite, on the event screen, or on a poster at the venue.
</p> </p>
@ -35,7 +32,7 @@ export function Home() {
</div> </div>
<label className="flex flex-col gap-2"> <label className="flex flex-col gap-2">
<span className="font-inter text-sm font-semibold text-[#24282f]">Code</span> <span className="font-inter text-sm font-semibold text-[var(--theme-card-text-color)]">Code</span>
<input <input
id="join-event-code" id="join-event-code"
type="text" type="text"
@ -46,7 +43,7 @@ export function Home() {
autoComplete="off" autoComplete="off"
autoCapitalize="characters" autoCapitalize="characters"
spellCheck={false} spellCheck={false}
className="h-[52px] w-full rounded-xl border border-[rgba(116,112,112,0.3)] bg-[rgba(157,157,172,0.07)] px-4 text-center font-inter text-base font-semibold uppercase tracking-[0.18em] text-[#24282f] outline-none transition-shadow focus:border-[#1D66EF]/55 focus:bg-white focus:shadow-[0_0_0_3px_rgba(29,102,239,0.12)] placeholder:font-normal placeholder:tracking-normal placeholder:text-[rgba(36,40,47,0.45)]" className="h-[52px] w-full rounded-xl border border-[var(--theme-card-text-color)]/10 bg-[var(--theme-card-color)]/40 px-4 text-center text-base font-semibold uppercase tracking-[0.18em] text-[var(--theme-card-text-color)] outline-none transition-all duration-300 ease-in-out focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[var(--theme-button-color)]/30 disabled:opacity-60 placeholder:font-normal placeholder:tracking-normal placeholder:text-[var(--theme-card-text-color)]/60"
/> />
</label> </label>
@ -59,7 +56,7 @@ export function Home() {
<button <button
type="submit" type="submit"
disabled={submitting || !code.trim()} disabled={submitting || !code.trim()}
className="w-full rounded-full border border-[rgba(214,200,200,0.6)] bg-[#ef8d1d] py-3.5 text-xs font-semibold uppercase tracking-wide text-white shadow-[0_4px_4px_rgba(0,0,0,0.15)] transition-opacity hover:opacity-95 disabled:opacity-60" className="w-full rounded-lg theme-btn-bg py-3.5 text-sm font-semibold uppercase tracking-wide text-white shadow-sm transition-opacity hover:opacity-95 disabled:opacity-60"
> >
{submitting ? "Joining…" : "Join"} {submitting ? "Joining…" : "Join"}
</button> </button>

View File

@ -77,19 +77,42 @@ export function LeaderboardPage() {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative">
<HeaderMobile logoUrl={ctx?.event?.logoUrl} eventId={eventId || null} /> <HeaderMobile logoUrl={ctx?.event?.logoUrl} eventId={eventId || null} />
<div className="eb-gradient-auth flex flex-1 flex-col items-center px-6 pb-10 pt-6"> <div className="eb-gradient-auth flex flex-1 flex-col items-center px-6 pb-10 pt-2">
<Award className="size-[68px] text-amber-300" strokeWidth={1.25} /> <div className="mt-6 w-full max-w-[354px] rounded-xl card-bg shadow-md">
<h1 className="mt-1 font-inter text-xl font-semibold text-white">Leaderboard</h1>
<div className="mt-6 w-full max-w-[354px] rounded-xl bg-white shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]"> <div className="flex gap-2 justify-between w-full p-4">
<div className="p-4"> <div className="flex gap-2 justify-between w-full">
<div className="eb-rank-highlight flex flex-col gap-2 rounded-3xl border-4 border-white px-4 py-4 text-white shadow-[0_9px_16px_rgba(0,0,0,0.14)]"> <div className="flex items-center gap-2 shrink-0 w-full">
<div className="bg-[var(--theme-button-color)]/12 rounded-full p-3">
<Award className="size-10 text-[var(--theme-button-color)]" strokeWidth={1.25} />
</div>
{/* <div className="bg-[var(--theme-button-color)]/12 rounded-lg p-2 w-[80px] h-[70px] flex items-center justify-center text-center font-bold">
<Award className="size-8 text-[var(--theme-button-color)]" strokeWidth={1.25} />
<span className="text-3xl">{!signedIn ? "—" : loading ? "…" : yourRank != null ? yourRank : "—"}</span>
</div> */}
<div className="flex flex-col gap-1">
<h1 className="mt-1 text-lg font-semibold">Your Rank</h1>
<span className="rounded-full bg-green-100 text-green-700 px-3 py-1 text-sm inline-block w-fit">
{!signedIn ? "Sign in" : loading ? "…" : yourTotal != null ? `${yourTotal} points` : "0 points"}
</span>
</div>
</div>
</div>
<div className="eb-rank-highligh flex shrink-0 grow-1 max-w-[150px] font-bold items-center justify-end">
<div className="bg-[var(--theme-button-color)] rounded-full w-[65px] h-[65px] text-3xl text-[var(--theme-button-color) text-white flex items-center justify-center">
{!signedIn ? "—" : loading ? "…" : yourRank != null ? yourRank : "—"}
</div>
</div>
</div>
<div className="p-4 pt-2 border-t border-[var(--theme-card-text-color)]/5">
{/* <div className="eb-rank-highlight flex flex-col gap-2 rounded-3xl border-4 border-white px-4 py-4 text-white shadow-[0_9px_16px_rgba(0,0,0,0.14)]">
<p className="text-center font-inter text-base font-semibold uppercase tracking-wide">Your rank</p> <p className="text-center font-inter text-base font-semibold uppercase tracking-wide">Your rank</p>
<div className="flex items-center justify-center gap-4"> <div className="flex items-center justify-center gap-4">
<span className="font-inter text-5xl font-bold drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)]"> <span className="font-inter text-5xl font-bold drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)]">
{!signedIn ? "—" : loading ? "…" : yourRank != null ? yourRank : "—"} {!signedIn ? "—" : loading ? "…" : yourRank != null ? yourRank : "—"}
</span> </span>
<span className="rounded-xl bg-[#cff7d3] px-4 py-3 text-xs font-medium text-[#009951] shadow-sm"> <span className="rounded-xl bg-[#cff7d3] px-4 py-2 text-sm font-medium text-[#009951] shadow-sm">
{!signedIn {!signedIn
? "Sign in" ? "Sign in"
: loading : loading
@ -99,14 +122,14 @@ export function LeaderboardPage() {
: "0 points"} : "0 points"}
</span> </span>
</div> </div>
</div> </div> */}
<h2 className="mt-6 font-inter text-base font-semibold text-[#24282f]">Top participants</h2> <h2 className="mt-6 font-inter text-base font-semibold">Leaderboard</h2>
{err ? ( {err ? (
<p className="mt-3 text-xs text-red-600">{err}</p> <p className="mt-3 text-xs text-red-600">{err}</p>
) : loading && rows.length === 0 ? ( ) : loading && rows.length === 0 ? (
<p className="mt-3 text-xs text-[rgba(36,40,47,0.67)]">Loading…</p> <p className="mt-3 text-xs text-[var(--theme-text-color)]/80">Loading…</p>
) : top5.length === 0 ? ( ) : top5.length === 0 ? (
<p className="mt-3 text-xs text-[rgba(36,40,47,0.67)]"> <p className="mt-3 text-xs text-[var(--theme-text-color)]/80">
No scores yet. Complete activities and earn points when organizers enable rewards. No scores yet. Complete activities and earn points when organizers enable rewards.
</p> </p>
) : ( ) : (
@ -116,12 +139,12 @@ export function LeaderboardPage() {
return ( return (
<li <li
key={row.participantId} key={row.participantId}
className="flex items-center gap-3 border-b border-[#ededed] pb-3 last:border-0" className="flex items-center gap-3 border-b border-[var(--theme-card-text-color)]/5 pb-3 last:border-0"
> >
<span className="shrink-0 rounded bg-[#ebffee] px-3 py-2 text-xs font-medium text-[#009951] shadow-sm"> <span className="shrink-0 rounded bg-green-100 text-green-700 px-3 py-2 text-xs font-medium shadow-sm">
#{row.rank} · {row.total} pts #{row.rank} · {row.total} pts
</span> </span>
<span className="min-w-0 flex-1 text-sm font-semibold text-[#24282f]">{row.displayName}</span> <span className="min-w-0 flex-1 text-sm">{row.displayName}</span>
{medal ? <Medal tier={medal} /> : null} {medal ? <Medal tier={medal} /> : null}
</li> </li>
); );
@ -133,7 +156,7 @@ export function LeaderboardPage() {
<Link <Link
to={eventId ? `/rewards?event_id=${encodeURIComponent(eventId)}` : "/rewards"} to={eventId ? `/rewards?event_id=${encodeURIComponent(eventId)}` : "/rewards"}
className="mt-6 font-inter font-semibold text-white underline" className="mt-6 font-inter font-semibold text-[var(--theme-text-color)] underline"
> >
Rewards Rewards
</Link> </Link>

View File

@ -240,9 +240,9 @@ export function LoginPage() {
<AuthShell> <AuthShell>
<div className="flex w-full max-w-[354px] flex-col items-center"> <div className="flex w-full max-w-[354px] flex-col items-center">
<div className="w-full rounded-xl shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]"> <div className="w-full rounded-xl shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]">
<div className="rounded-xl bg-white"> <div className="rounded-xl card-bg">
<div className="px-6 pb-2 pt-6"> <div className="px-6 pb-2 pt-6">
<div className="flex h-[103px] items-center justify-center rounded-xl border border-[#f2f2f7] px-4"> <div className="flex h-[103px] items-center justify-center rounded-xl border border-[var(--theme-card-color)]/25 px-4">
{preview?.logoUrl ? ( {preview?.logoUrl ? (
<img <img
src={preview.logoUrl} src={preview.logoUrl}
@ -256,13 +256,13 @@ export function LoginPage() {
</div> </div>
<form onSubmit={handleSubmit} className="flex flex-col gap-8 px-6 pb-8 pt-2"> <form onSubmit={handleSubmit} className="flex flex-col gap-8 px-6 pb-8 pt-2">
<div className="flex flex-col items-center gap-4 text-center"> <div className="flex flex-col items-center gap-4 text-center">
<p className="font-inter text-base font-semibold text-[#24282f]">Log in to</p> <p className="text-base font-semibold text-[var(--theme-card-text-color)]">Log in to</p>
<div className="min-w-0 space-y-1"> <div className="min-w-0 space-y-1">
<p className="font-inter text-[28px] font-bold leading-tight text-[#24282f] sm:text-[32px]"> <p className="text-[28px] font-bold leading-tight text-[var(--theme-card-text-color)] sm:text-[32px]">
{headline} {headline}
</p> </p>
{preview?.subtitle ? ( {preview?.subtitle ? (
<p className="font-inter text-sm font-medium text-[#24282f]/80">{preview.subtitle}</p> <p className="text-sm font-medium text-[var(--theme-card-text-color)]/80">{preview.subtitle}</p>
) : null} ) : null}
</div> </div>
{previewError ? ( {previewError ? (
@ -276,11 +276,11 @@ export function LoginPage() {
</p> </p>
) : null} ) : null}
{showMeta ? ( {showMeta ? (
<div className="flex flex-wrap items-center justify-center gap-2 text-xs text-[#24282f]"> <div className="flex flex-wrap items-center justify-center gap-2 text-xs text-[var(--theme-card-text-color)]">
{schedule ? ( {schedule ? (
<> <>
<Calendar className="size-4 shrink-0" strokeWidth={1.75} /> <Calendar className="size-4 shrink-0" strokeWidth={1.75} />
<span className="font-inter">{schedule}</span> <span className="">{schedule}</span>
</> </>
) : null} ) : null}
{schedule && preview?.location?.trim() ? ( {schedule && preview?.location?.trim() ? (
@ -289,7 +289,7 @@ export function LoginPage() {
{preview?.location?.trim() ? ( {preview?.location?.trim() ? (
<> <>
<MapPin className="size-4 shrink-0" strokeWidth={1.75} /> <MapPin className="size-4 shrink-0" strokeWidth={1.75} />
<span className="font-inter">{preview.location.trim()}</span> <span className="">{preview.location.trim()}</span>
</> </>
) : null} ) : null}
</div> </div>
@ -298,25 +298,25 @@ export function LoginPage() {
{showInviteField ? ( {showInviteField ? (
<label className="flex flex-col gap-2"> <label className="flex flex-col gap-2">
<span className="font-inter text-sm font-semibold text-[#24282f]">Invite code</span> <span className="text-sm font-semibold text-[var(--theme-card-text-color)]">Invite code</span>
<input <input
value={inviteInput} value={inviteInput}
onChange={(e) => setInviteInput(e.target.value)} onChange={(e) => setInviteInput(e.target.value)}
placeholder="Enter your invite code" placeholder="Enter your invite code"
className="h-[44px] w-full rounded-lg border border-[rgba(116,112,112,0.3)] bg-[rgba(157,157,172,0.07)] px-4 font-inter text-sm text-[#24282f]" className="h-[44px] w-full rounded-lg border border-[rgba(116,112,112,0.3)] bg-[rgba(157,157,172,0.07)] px-4 text-sm text-[var(--theme-card-text-color)] transition-colors duration-300 ease-in-out focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[var(--theme-button-color)]/30"
/> />
</label> </label>
) : null} ) : null}
{showEmail ? ( {showEmail ? (
<label className="flex flex-col gap-2"> <label className="flex flex-col gap-2">
<span className="font-inter text-base font-semibold text-[#24282f]">Email address</span> <span className="text-base font-semibold text-[var(--theme-card-text-color)]">Email address</span>
<input <input
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email address" placeholder="Enter your email address"
className="h-[51px] w-full rounded-lg border border-[rgba(116,112,112,0.3)] bg-[rgba(157,157,172,0.07)] px-4 font-inter text-sm tracking-wide text-[#24282f] placeholder:text-[rgba(36,40,47,0.6)]" className="h-[51px] w-full rounded-lg border border-[var(--theme-card-text-color)]/10 bg-[var(--theme-card-color)]/40 px-4 text-sm tracking-wide text-[var(--theme-card-text-color)] placeholder:text-[var(--theme-card-text-color)]/60 transition-all duration-300 ease-in-out focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[var(--theme-button-color)]/30 disabled:opacity-60"
/> />
</label> </label>
) : null} ) : null}
@ -335,7 +335,7 @@ export function LoginPage() {
<button <button
type="submit" type="submit"
disabled={loading || loadingPreview} disabled={loading || loadingPreview}
className="w-full rounded-full border border-[rgba(214,200,200,0.6)] bg-[#ef8d1d] py-3 text-xs font-semibold uppercase tracking-wide text-white shadow-[0_4px_4px_rgba(0,0,0,0.15)] disabled:opacity-60" className="w-full rounded-lg theme-btn-bg py-3 text-sm font-semibold uppercase tracking-wide text-white shadow-sm transition-colors duration-300 ease-in-out disabled:opacity-60"
> >
{loading ? "Please wait…" : reg.primaryCtaLabel} {loading ? "Please wait…" : reg.primaryCtaLabel}
</button> </button>

View File

@ -44,7 +44,7 @@ function WhiteCard({ children, wide }: { children: ReactNode; wide?: boolean })
return ( return (
<div className={`flex w-full ${wide ? "max-w-[480px]" : "max-w-[354px]"} flex-col items-center`}> <div className={`flex w-full ${wide ? "max-w-[480px]" : "max-w-[354px]"} flex-col items-center`}>
<div className="w-full rounded-xl shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]"> <div className="w-full rounded-xl shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]">
<div className="rounded-xl bg-white px-6 py-6">{children}</div> <div className="rounded-xl card-bg text-[var(--theme-card-text-color)] shadow-md px-6 py-6">{children}</div>
</div> </div>
</div> </div>
); );
@ -432,18 +432,18 @@ export function PhotoSearchPage() {
</div> </div>
) : null} ) : null}
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Find Photos</h1> <h1 className="mb-1 text-center text-[22px] font-semibold text-[var(--theme-card-text-color)]">Find Photos</h1>
<p className="mb-5 text-center text-sm text-[#666]"> <p className="mb-5 text-center text-sm text-[var(--theme-card-text-color)]/80">
Upload a selfie to find photos of you from {meta?.eventName || "the event"}. Upload a selfie to find photos of you from {meta?.eventName || "the event"}.
</p> </p>
{meta?.accessMode === "login" && !loggedIn ? ( {meta?.accessMode === "login" && !loggedIn ? (
<div className="space-y-3"> <div className="space-y-3">
<p className="text-center text-sm text-[#666]">Sign in to search for your photos.</p> <p className="text-center text-sm text-[var(--theme-card-text-color)]/80">Sign in to search for your photos.</p>
<button <button
type="button" type="button"
onClick={() => void goLogin()} onClick={() => void goLogin()}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-[#D32F2F] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c]" className="inline-flex h-11 w-full items-center justify-center rounded-lg theme-btn-bg px-4 text-sm font-semibold text-white"
> >
Sign in to continue Sign in to continue
</button> </button>
@ -460,14 +460,14 @@ export function PhotoSearchPage() {
onChange={(e) => onPick(e.target.files?.[0] || null)} onChange={(e) => onPick(e.target.files?.[0] || null)}
/> />
{previewUrl ? ( {previewUrl ? (
<div className="mb-3 overflow-hidden rounded-lg border border-[#e5e5e5] bg-[#f8f8f8]"> <div className="mb-3 overflow-hidden rounded-lg">
<img src={previewUrl} alt="Your selfie" className="mx-auto max-h-[240px] w-full object-contain" /> <img src={previewUrl} alt="Your selfie" className="mx-auto max-h-[240px] w-full object-contain" />
</div> </div>
) : null} ) : null}
<button <button
type="button" type="button"
onClick={() => fileRef.current?.click()} onClick={() => fileRef.current?.click()}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-[#ddd] bg-white px-4 text-sm font-semibold text-[#333] hover:bg-[#fafafa]" className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-zinc-300 bg-zinc-100 px-4 text-sm font-semibold text-[var(--theme-card-text-color)]"
> >
{file ? "Change photo" : "Choose photo"} {file ? "Change photo" : "Choose photo"}
</button> </button>
@ -477,7 +477,7 @@ export function PhotoSearchPage() {
type="button" type="button"
disabled={!file || searching} disabled={!file || searching}
onClick={() => void onSearch()} onClick={() => void onSearch()}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-[#D32F2F] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c] disabled:opacity-55" className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-[#D32F2F theme-btn-bg px-4 text-sm font-semibold text-white hover:bg-[var(--theme-button-color)]/85 disabled:opacity-55"
> >
<Search className="h-4 w-4" /> <Search className="h-4 w-4" />
{searching ? "Searching…" : "Find photos"} {searching ? "Searching…" : "Find photos"}

View File

@ -59,9 +59,9 @@ export function PhotoWallPresentPage() {
return ( return (
<div className="eb-gradient-auth min-h-dvh text-white"> <div className="eb-gradient-auth min-h-dvh text-white">
<div className="px-4 pb-3 pt-4 text-center"> <div className="px-4 pb-3 pt-4 text-center card-bg rounded-lg shadow-sm max-w-[400px] mx-auto mb-3">
<p className="text-xs uppercase tracking-[0.2em] text-white/70">Photo Wall</p> <p className="text-xs uppercase tracking-[0.2em] text-[var(--theme-card-text-color)]/70">Photo Wall</p>
<h1 className="font-inter text-xl font-semibold sm:text-2xl">{data.eventName}</h1> <h1 className="text-xl font-semibold sm:text-2xl text-[var(--theme-card-text-color)]">{data.eventName}</h1>
</div> </div>
{data.photos.length === 0 ? ( {data.photos.length === 0 ? (
@ -98,7 +98,7 @@ export function PhotoWallPresentPage() {
/> />
{showNames && p.displayName ? ( {showNames && p.displayName ? (
<p <p
className="mt-1.5 w-full truncate text-center font-inter text-xs font-medium text-white/90 sm:text-sm" className="mt- w-full truncate text-center text-xs font-medium text-[var(--theme-card-text-color)]/90 bg-[var(--theme-card-color)] rounded-md p-1 sm:text-sm"
title={p.displayName} title={p.displayName}
> >
{p.displayName} {p.displayName}

View File

@ -51,7 +51,7 @@ function WhiteCard({ children }: { children: ReactNode }) {
return ( return (
<div className="flex w-full max-w-[354px] flex-col items-center"> <div className="flex w-full max-w-[354px] flex-col items-center">
<div className="w-full rounded-xl shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]"> <div className="w-full rounded-xl shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]">
<div className="rounded-xl bg-white px-6 py-6">{children}</div> <div className="rounded-xl card-bg text-[var(--theme-card-text-color)] shadow-md px-6 py-6">{children}</div>
</div> </div>
</div> </div>
); );
@ -371,7 +371,7 @@ export function PhotoWallUploadPage() {
return ( return (
<AuthShell showLogo={false}> <AuthShell showLogo={false}>
<WhiteCard> <WhiteCard>
<p className="text-center text-sm text-[#666]">Loading…</p> <p className="text-center text-sm text-[var(--theme-card-text-color)]/70">Loading…</p>
</WhiteCard> </WhiteCard>
</AuthShell> </AuthShell>
); );
@ -398,10 +398,10 @@ export function PhotoWallUploadPage() {
<PhotoWallEventHeader event={meta.event} display={meta.eventHeader || null} /> <PhotoWallEventHeader event={meta.event} display={meta.eventHeader || null} />
</div> </div>
) : null} ) : null}
<h1 className="mb-2 text-center text-[22px] font-semibold text-[#1a1a1a]"> <h1 className="mb-2 text-center text-[22px] font-semibold text-[var(--theme-card-text-color)]">
{done.framed ? "Your framed photo" : "Photo uploaded"} {done.framed ? "Your framed photo" : "Photo uploaded"}
</h1> </h1>
<p className="mb-4 text-center text-sm text-[#666]"> <p className="mb-4 text-center text-sm text-[var(--theme-card-text-color)]/80">
{done.awaitingApproval {done.awaitingApproval
? "Thanks! Your photo is waiting for approval." ? "Thanks! Your photo is waiting for approval."
: done.framed : done.framed
@ -435,7 +435,7 @@ export function PhotoWallUploadPage() {
<button <button
type="button" type="button"
onClick={resetForNewUpload} onClick={resetForNewUpload}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-[#D32F2F] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c]" className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-[#D32F2F theme-btn-bg px-4 text-sm font-semibold text-white hover:bg-[#b71c1c]"
> >
<ImagePlus className="h-4 w-4" /> <ImagePlus className="h-4 w-4" />
Edit / Upload new photo Edit / Upload new photo
@ -453,7 +453,7 @@ export function PhotoWallUploadPage() {
</div> </div>
{showShare ? ( {showShare ? (
<div className="mt-4"> <div className="mt-4">
<p className="mb-2 text-center text-xs font-semibold uppercase tracking-wide text-[#888]"> <p className="mb-2 text-center text-xs font-semibold uppercase tracking-wide text-[var(--theme-card-text-color)]/80">
Share Share
</p> </p>
{useNativeShare ? ( {useNativeShare ? (
@ -496,18 +496,18 @@ export function PhotoWallUploadPage() {
<PhotoWallEventHeader event={meta.event} display={meta.eventHeader || null} /> <PhotoWallEventHeader event={meta.event} display={meta.eventHeader || null} />
</div> </div>
) : null} ) : null}
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Upload your photo</h1> <h1 className="mb-1 text-center text-[22px] font-semibold text-[var(--theme-card-text-color)]">Upload your photo</h1>
<p className="mb-5 text-center text-sm text-[#666]"> <p className="mb-5 text-center text-sm text-[var(--theme-card-text-color)]/80">
{meta?.form?.description || "Add a selfie to the event photo wall."} {meta?.form?.description || "Add a selfie to the event photo wall."}
</p> </p>
{meta?.accessMode === "login" && !loggedIn ? ( {meta?.accessMode === "login" && !loggedIn ? (
<div className="space-y-3"> <div className="space-y-3">
<p className="text-center text-sm text-[#666]">Sign in to upload your photo for this event.</p> <p className="text-center text-sm text-[var(--theme-card-text-color)]/80">Sign in to upload your photo for this event.</p>
<button <button
type="button" type="button"
onClick={() => void goLogin()} onClick={() => void goLogin()}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-[#D32F2F] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c]" className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-[var(--theme-button-color)] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c]"
> >
Sign in to continue Sign in to continue
</button> </button>
@ -539,20 +539,20 @@ export function PhotoWallUploadPage() {
)} )}
</button> </button>
{file ? ( {file ? (
<p className="mt-2 truncate text-center text-xs text-[#888]">{file.name}</p> <p className="mt-2 truncate text-center text-xs text-[var(--theme-card-text-color)]/80">{file.name}</p>
) : null} ) : null}
</div> </div>
{meta?.askUploaderName ? ( {meta?.askUploaderName ? (
<label className="block text-sm"> <label className="block text-sm">
<span className="mb-1 block font-medium text-[#333]"> <span className="mb-1 block font-medium text-[var(--theme-card-text-color)]">
Your name <span className="text-red-500">*</span> Your name <span className="text-red-500">*</span>
</span> </span>
<input <input
type="text" type="text"
value={uploaderName} value={uploaderName}
onChange={(e) => setUploaderName(e.target.value)} onChange={(e) => setUploaderName(e.target.value)}
className="h-10 w-full rounded-lg border border-[#ddd] px-3 text-sm outline-none focus:border-[#D32F2F]" className="h-10 w-full rounded-lg border border-[rgba(116,112,112,0.3)] px-3 text-sm transition-colors duration-300 ease-in-out focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[var(--theme-button-color)]/30"
placeholder="Name shown on the wall" placeholder="Name shown on the wall"
required required
/> />
@ -566,7 +566,7 @@ export function PhotoWallUploadPage() {
if (f.type === "textarea") { if (f.type === "textarea") {
return ( return (
<label key={key} className="block text-sm"> <label key={key} className="block text-sm">
<span className="mb-1 block font-medium text-[#333]"> <span className="mb-1 block font-medium text-[var(--theme-card-text-color)]">
{f.label} {f.label}
{required ? <span className="text-red-500"> *</span> : null} {required ? <span className="text-red-500"> *</span> : null}
</span> </span>
@ -575,14 +575,14 @@ export function PhotoWallUploadPage() {
onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))} onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))}
required={required} required={required}
rows={3} rows={3}
className="w-full rounded-lg border border-[#ddd] px-3 py-2 text-sm outline-none focus:border-[#D32F2F]" className="w-full rounded-lg border border-[rgba(116,112,112,0.3)] px-3 py-2 text-sm transition-colors duration-300 ease-in-out focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[var(--theme-button-color)]/30"
/> />
</label> </label>
); );
} }
return ( return (
<label key={key} className="block text-sm"> <label key={key} className="block text-sm">
<span className="mb-1 block font-medium text-[#333]"> <span className="mb-1 block font-medium text-[var(--theme-card-text-color)]">
{f.label} {f.label}
{required ? <span className="text-red-500"> *</span> : null} {required ? <span className="text-red-500"> *</span> : null}
</span> </span>
@ -591,7 +591,7 @@ export function PhotoWallUploadPage() {
value={String(values[key] ?? "")} value={String(values[key] ?? "")}
onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))} onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))}
required={required} required={required}
className="h-10 w-full rounded-lg border border-[#ddd] px-3 text-sm outline-none focus:border-[#D32F2F]" className="h-10 w-full rounded-lg border border-[rgba(116,112,112,0.3)] px-3 text-sm transition-colors duration-300 ease-in-out focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[var(--theme-button-color)]/30"
/> />
</label> </label>
); );
@ -602,7 +602,7 @@ export function PhotoWallUploadPage() {
<button <button
type="submit" type="submit"
disabled={!file || submitting || picking} disabled={!file || submitting || picking}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-[#D32F2F] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c] disabled:opacity-50" className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-[#D32F2F theme-btn-bg px-4 text-sm font-semibold text-white hover:bg-[#b71c1c] disabled:opacity-50"
> >
{submitting ? "Uploading…" : "Upload"} {submitting ? "Uploading…" : "Upload"}
</button> </button>

View File

@ -103,15 +103,18 @@ export function Questions() {
if (!eventId) { if (!eventId) {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile eventId={eventId || null} /> <HeaderMobile eventId={eventId || null} />
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 flex-col items-center justify-center px-4 py-10">
<div className="card-bg p-4 rounded-lg">
<p className="text-center text-sm text-[#24282f]"> <p className="text-center text-sm text-[#24282f]">
Add <span className="font-mono">event_id</span> to the URL to open questions. Add <span className="font-mono">event_id</span> to the URL to open questions.
</p> </p>
<Link to="/" className="mt-4 text-sm font-semibold text-[#1e72ef] underline"> <Link to="/" className="mt-4 text-sm font-semibold theme-btn-link">
Home Home
</Link> </Link>
</div>
</main> </main>
</MobileShell> </MobileShell>
); );
@ -119,21 +122,24 @@ export function Questions() {
if (!sessionId) { if (!sessionId) {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile logoUrl={headerLogoUrl} eventId={eventId || null} /> <HeaderMobile logoUrl={headerLogoUrl} eventId={eventId || null} />
<main className="relative z-0 flex flex-1 flex-col bg-[#f2f2f7] px-4 pb-28 pt-6"> <main className="relative z-0 flex flex-1 flex-col px-4 pb-28 pt-6">
<div className="mx-auto w-full max-w-[370px]"> <div className="mx-auto w-full max-w-[370px]">
<h1 className="text-lg font-semibold text-[#24282f]">{ev?.name ?? "Event"}</h1> <div className="card-bg p-4 rounded-lg">
<p className="mt-3 text-sm leading-relaxed text-[rgba(36,40,47,0.75)]">
<h1 className="text-lg font-semibold">{ev?.name ?? "Event"}</h1>
<p className="mt-3 text-sm leading-relaxed text-[var(--theme-card-text-color)]/80">
Open a session from the agenda to view all audience questions and ask your own. Open a session from the agenda to view all audience questions and ask your own.
</p> </p>
<Link <Link
to={trackHref} to={trackHref}
className="mt-6 inline-flex items-center gap-1 text-sm font-semibold text-[#1e72ef] underline" className="mt-6 inline-flex items-center gap-1 text-sm font-semibold theme-btn-link"
> >
Browse sessions Browse sessions
</Link> </Link>
</div> </div>
</div>
</main> </main>
<HyperNavigationFAB eventId={eventId} sessionId={sessionId || undefined} /> <HyperNavigationFAB eventId={eventId} sessionId={sessionId || undefined} />
</MobileShell> </MobileShell>
@ -141,31 +147,34 @@ export function Questions() {
} }
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile logoUrl={headerLogoUrl} eventId={eventId || null} /> <HeaderMobile logoUrl={headerLogoUrl} eventId={eventId || null} />
<main className="relative z-0 flex flex-1 flex-col bg-[#f2f2f7] pb-28 pt-6"> <main className="relative z-0 flex flex-1 flex-col pb-28 pt-6">
<div className="mx-auto w-full max-w-[370px] px-4"> <div className="mx-auto w-full max-w-[370px] px-3">
<div className="mb-1 inline-flex w-fit rounded bg-[#e5e5ea] px-2 py-0.5"> <div className="flex flex-col gap-2 card-bg p-4 rounded-lg">
<span className="text-[10px] tracking-wide text-[#24282f]">Audience questions</span>
<div className="mb-1 inline-flex w-fit rounded bg-zinc-200 px-2 py-0.5">
<span className="text-[10px] tracking-wide text-gray-600">Audience questions</span>
</div> </div>
<h1 className="text-lg font-semibold text-[#24282f]"> <h1 className="text-lg font-semibold">
{sessionTitle ?? "Session questions"} {sessionTitle ?? "Session questions"}
</h1> </h1>
{ev?.name ? ( {ev?.name ? (
<p className="mt-1 text-xs text-[rgba(36,40,47,0.65)]">{ev.name}</p> <p className="mt-1 text-xs text-[var(--theme-card-text-color)]/80">{ev.name}</p>
) : null} ) : null}
<button <button
type="button" type="button"
onClick={() => setAskOpen(true)} onClick={() => setAskOpen(true)}
className="mt-4 flex h-11 w-full items-center justify-between rounded-full bg-[#ef8d1d] px-[18px] text-left shadow-sm transition hover:bg-[#e0800c]" className="mt-4 flex h-11 w-full items-center justify-between rounded-lg theme-btn-bg px-[18px] text-left shadow-sm transition-colors duration-300 ease-in-out hover:bg-[var(--theme-button-color)]/90"
> >
<span className="text-xs font-semibold uppercase tracking-wide text-white"> <span className="text-sm font-semibold uppercase tracking-wide text-white">
Ask a question Ask a question
</span> </span>
<ChevronRight className="size-4 text-white" strokeWidth={2.5} /> <ChevronRight className="size-4 text-white" strokeWidth={2.5} />
</button> </button>
</div>
<div className="mt-6 flex flex-col gap-3"> <div className="mt-6 flex flex-col gap-3">
{listLoading ? ( {listLoading ? (
@ -186,8 +195,8 @@ export function Questions() {
</div> </div>
{sessionHref ? ( {sessionHref ? (
<p className="mt-6 text-center text-xs"> <p className="mt-6 text-center text-md">
<Link className="text-[#1e72ef] underline" to={sessionHref}> <Link className="text-[var(--theme-text-color)] underline" to={sessionHref}>
Back to session Back to session
</Link> </Link>
</p> </p>

View File

@ -636,11 +636,13 @@ export function QuizPollTakePage() {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative">
<HeaderMobile eventId={null} /> <HeaderMobile eventId={null} />
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 flex-col items-center justify-center px-4 py-10">
<p className="text-center text-sm text-[#24282f]">Invalid quiz link.</p> <div className="mx-auto w-full max-w-[370px] rounded-lg card-bg p-4 text-center shadow-sm">
<Link to="/" className="mt-4 text-sm font-semibold text-[#1e72ef] underline"> <p className="text-center text-sm">Invalid quiz link.</p>
<Link to="/" className="mt-4 text-sm font-semibold text-[var(--theme-button-color)] underline">
Home Home
</Link> </Link>
</div>
</main> </main>
</MobileShell> </MobileShell>
); );
@ -650,8 +652,10 @@ export function QuizPollTakePage() {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} /> <HeaderMobile logoUrl={headerLogo} eventId={eventId} />
<main className="flex flex-1 items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 items-center justify-center px-4 py-10">
<p className="text-sm text-[#24282f]">Loading…</p> <div className="mx-auto w-full max-w-[370px] rounded-lg card-bg p-4 text-center shadow-sm">
<p className="text-sm">Loading…</p>
</div>
</main> </main>
</MobileShell> </MobileShell>
); );
@ -661,11 +665,13 @@ export function QuizPollTakePage() {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} /> <HeaderMobile logoUrl={headerLogo} eventId={eventId} />
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 flex-col items-center justify-center px-4 py-10">
<div className="mx-auto w-full max-w-[370px] rounded-lg card-bg p-4 text-center shadow-sm">
<p className="text-center text-sm text-red-600">{err ?? "Quiz not found."}</p> <p className="text-center text-sm text-red-600">{err ?? "Quiz not found."}</p>
<Link to={sessionBackHref} className="mt-4 text-sm font-semibold text-[#1e72ef] underline"> <Link to={sessionBackHref} className="mt-4 text-sm font-semibold text-[var(--theme-button-color)] underline">
Back to session Back to session
</Link> </Link>
</div>
</main> </main>
</MobileShell> </MobileShell>
); );
@ -676,8 +682,10 @@ export function QuizPollTakePage() {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} /> <HeaderMobile logoUrl={headerLogo} eventId={eventId} />
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 flex-col items-center justify-center px-4 py-10">
<p className="text-center text-sm text-[#24282f]">Returning to session…</p> <div className="mx-auto w-full max-w-[370px] rounded-lg card-bg p-4 text-center shadow-sm">
<p className="text-center text-sm text-[var(--theme-card-text-color)]/80">Returning to session…</p>
</div>
</main> </main>
</MobileShell> </MobileShell>
); );
@ -687,15 +695,17 @@ export function QuizPollTakePage() {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} /> <HeaderMobile logoUrl={headerLogo} eventId={eventId} />
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 flex-col items-center justify-center px-4 py-10">
<p className="font-inter text-lg font-semibold text-[#24282f]">Thank you</p> <div className="mx-auto w-full max-w-[370px] rounded-lg card-bg p-4 text-center shadow-sm">
<p className="mt-2 text-center text-sm text-[#24282f]">Your response was submitted.</p> <p className="font-inter text-lg font-semibold">Thank you</p>
<p className="mt-2 text-center text-sm text-[var(--theme-card-text-color)]/80">Your response was submitted.</p>
<Link <Link
to={sessionBackHref} to={sessionBackHref}
className="mt-6 text-sm font-semibold text-[#1e72ef] underline" className="mt-6 text-sm font-semibold text[var(--theme-button-color)] underline"
> >
Back to session Back to session
</Link> </Link>
</div>
</main> </main>
</MobileShell> </MobileShell>
); );
@ -712,20 +722,20 @@ export function QuizPollTakePage() {
<div className="flex shrink-0 flex-col"> <div className="flex shrink-0 flex-col">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} /> <HeaderMobile logoUrl={headerLogo} eventId={eventId} />
</div> </div>
<main className="min-h-0 flex-1 overflow-y-auto overscroll-contain bg-[#f2f2f7] px-4 pb-8 pt-6"> <main className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-8 pt-6">
<div className="mx-auto w-full max-w-[370px]"> <div className="mx-auto w-full max-w-[370px]">
<div className="rounded-lg border border-[rgba(116,112,112,0.2)] bg-white p-5 text-center shadow-sm"> <div className="rounded-lg card-bg p-5 text-center shadow-md">
<p className="font-inter text-lg font-semibold text-[#24282f]">Thank you</p> <p className="font-inter text-lg font-semibold text-[var(--theme-card-text-color)]]">Thank you</p>
<p className="mt-1 text-sm text-[#24282f]">Your response was submitted.</p> <p className="mt-1 text-sm text-[var(--theme-card-text-color)]/80">Your response was submitted.</p>
{showRes && isQuiz && typeof scoreSubmitted === "number" ? ( {showRes && isQuiz && typeof scoreSubmitted === "number" ? (
<p className="mt-3 text-sm font-semibold text-[#1e72ef]">Your score: {scoreSubmitted}</p> <p className="mt-3 text-sm font-semibold text-[var(--theme-button-color)]">Your score: {scoreSubmitted}</p>
) : null} ) : null}
</div> </div>
{showRes && isQuiz ? ( {showRes && isQuiz ? (
<div className="mt-6"> <div className="mt-6">
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#1e72ef]">Results</p> <p className="text-[10px] font-semibold uppercase tracking-wide text-[var(--theme-button-color)]">Results</p>
<p className="mt-0.5 text-xs text-[rgba(36,40,47,0.55)]">Questions with correct answers</p> <p className="mt-0.5 text-xs text-[var(--theme-card-text-color)]/80">Questions with correct answers</p>
<ul className="mt-4 flex flex-col gap-4"> <ul className="mt-4 flex flex-col gap-4">
{quiz.questions.map((q, qi) => { {quiz.questions.map((q, qi) => {
const raw = answers[q._id]; const raw = answers[q._id];
@ -734,13 +744,13 @@ export function QuizPollTakePage() {
return ( return (
<li <li
key={q._id} key={q._id}
className="rounded-lg border border-[rgba(116,112,112,0.2)] bg-white p-4 shadow-sm" className="rounded-lg border border-[var(--theme-card-text-color)]/15 card-bg p-4 shadow-sm"
> >
<p className="text-sm font-semibold text-[#24282f]"> <p className="text-sm font-semibold text-[var(--theme-card-text-color)]">
<span className="text-[#8e8e93]">{qi + 1}. </span> <span className="text-[var(--theme-card-text-color)]">{qi + 1}. </span>
{q.question} {q.question}
</p> </p>
<p className="mt-2 text-[10px] font-medium uppercase tracking-wide text-[#8e8e93]"> <p className="mt-2 text-xs font-medium uppercase tracking-wide text-[var(--theme-card-text-color)]/80">
Your answer Your answer
</p> </p>
<p <p
@ -749,7 +759,7 @@ export function QuizPollTakePage() {
? "text-green-700" ? "text-green-700"
: graded === false : graded === false
? "text-red-600" ? "text-red-600"
: "text-[#24282f]" : "text-[var(--theme-card-text-color)]"
}`} }`}
> >
{formatUserAnswerLine(q, raw)} {formatUserAnswerLine(q, raw)}
@ -757,17 +767,17 @@ export function QuizPollTakePage() {
</p> </p>
{corr ? ( {corr ? (
<> <>
<p className="mt-2 text-[10px] font-medium uppercase tracking-wide text-[#8e8e93]"> <p className="mt-2 text-[10px] font-medium uppercase tracking-wide text-[var(--theme-card-text-color)]/80">
Correct answer Correct answer
</p> </p>
<p className="text-sm font-semibold text-green-800">{corr}</p> <p className="text-sm font-semibold text-green-800">{corr}</p>
</> </>
) : q.type === "text" ? ( ) : q.type === "text" ? (
<p className="mt-2 text-xs text-[rgba(36,40,47,0.55)]"> <p className="mt-2 text-xs text-[var(--theme-card-text-color)]/60">
Open-ended — no single correct answer. Open-ended — no single correct answer.
</p> </p>
) : q.type === "rating" ? ( ) : q.type === "rating" ? (
<p className="mt-2 text-xs text-[rgba(36,40,47,0.55)]"> <p className="mt-2 text-xs text-[var(--theme-card-text-color)]/60">
Rating scale — no correct answer key. Rating scale — no correct answer key.
</p> </p>
) : null} ) : null}
@ -780,16 +790,16 @@ export function QuizPollTakePage() {
{showRes && isPollOrSurvey ? ( {showRes && isPollOrSurvey ? (
<div className="mt-6"> <div className="mt-6">
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#1e72ef]">Results</p> <p className="text-[10px] font-semibold uppercase tracking-wide text-[var(--theme-button-color)]">Results</p>
<p className="mt-0.5 text-xs text-[rgba(36,40,47,0.55)]">Share of responses per option</p> <p className="mt-0.5 text-xs text-[var(--theme-card-text-color)]">Share of responses per option</p>
{pollResultsLoading ? ( {pollResultsLoading ? (
<p className="mt-6 text-center text-sm text-[#8e8e93]">Loading results…</p> <p className="mt-6 text-center text-sm text-[var(--theme-card-text-color)]/80">Loading results…</p>
) : null} ) : null}
{pollResultsErr ? ( {pollResultsErr ? (
<p className="mt-4 text-center text-sm text-red-600">{pollResultsErr}</p> <p className="mt-4 text-center text-sm text-red-600">{pollResultsErr}</p>
) : null} ) : null}
{pollResults && !pollResultsLoading ? ( {pollResults && !pollResultsLoading ? (
<p className="mt-3 text-xs text-[rgba(36,40,47,0.65)]"> <p className="mt-3 text-xs text-[var(--theme-card-text-color)]/60">
Based on {pollResults.totalSubmissions} submission Based on {pollResults.totalSubmissions} submission
{pollResults.totalSubmissions === 1 ? "" : "s"} {pollResults.totalSubmissions === 1 ? "" : "s"}
</p> </p>
@ -799,11 +809,11 @@ export function QuizPollTakePage() {
{pollResults.questions.map((pq) => ( {pollResults.questions.map((pq) => (
<li <li
key={`${pq.questionIndex}-${pq.questionId}`} key={`${pq.questionIndex}-${pq.questionId}`}
className="rounded-lg border border-[rgba(116,112,112,0.2)] bg-white p-4 shadow-sm" className="rounded-lg border border-[var(--theme-card-text-color)]/15 card-bg p-4 shadow-sm"
> >
<p className="text-sm font-semibold text-[#24282f]">{pq.question}</p> <p className="text-sm font-semibold text-[var(--theme-card-text-color)]">{pq.question}</p>
{pq.kind === "text" ? ( {pq.kind === "text" ? (
<p className="mt-2 text-sm text-[rgba(36,40,47,0.75)]"> <p className="mt-2 text-sm text-[var(--theme-card-text-color)]/80">
{(pq.answeredCount ?? 0) === 0 {(pq.answeredCount ?? 0) === 0
? "No text answers yet." ? "No text answers yet."
: `${pq.answeredCount} of ${pq.totalResponses} answered (${pq.percentAnswered ?? 0}%)`} : `${pq.answeredCount} of ${pq.totalResponses} answered (${pq.percentAnswered ?? 0}%)`}
@ -812,15 +822,15 @@ export function QuizPollTakePage() {
<div className="mt-3 flex flex-col gap-3"> <div className="mt-3 flex flex-col gap-3">
{(pq.options || []).map((opt) => ( {(pq.options || []).map((opt) => (
<div key={opt.optionId}> <div key={opt.optionId}>
<div className="mb-1 flex justify-between gap-2 text-xs text-[#24282f]"> <div className="mb-1 flex justify-between gap-2 text-xs text-[var(--theme-card-text-color)]">
<span className="min-w-0 flex-1 leading-snug">{opt.text || "—"}</span> <span className="min-w-0 flex-1 leading-snug">{opt.text || "—"}</span>
<span className="shrink-0 tabular-nums font-semibold text-[#1e72ef]"> <span className="shrink-0 tabular-nums font-semibold text-[var(--theme-button-color)]">
{opt.percent}% {opt.percent}%
</span> </span>
</div> </div>
<div className="h-2 overflow-hidden rounded-full bg-[#e5e5ea]"> <div className="h-2 overflow-hidden rounded-full bg-[var(--theme-button-color)]/10">
<div <div
className="h-full rounded-full bg-[#1e72ef]" className="h-full rounded-full bg-[var(--theme-button-color)]"
style={{ width: `${Math.min(100, Math.max(0, opt.percent))}%` }} style={{ width: `${Math.min(100, Math.max(0, opt.percent))}%` }}
/> />
</div> </div>
@ -837,7 +847,7 @@ export function QuizPollTakePage() {
<Link <Link
to={sessionBackHref} to={sessionBackHref}
className="mt-8 block text-center text-sm font-semibold text-[#1e72ef] underline" className="mt-8 block text-center text-sm font-semibold text-[var(--theme-text-color)] underline"
> >
Back to session Back to session
</Link> </Link>
@ -853,8 +863,10 @@ export function QuizPollTakePage() {
<div className="flex shrink-0 flex-col"> <div className="flex shrink-0 flex-col">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} /> <HeaderMobile logoUrl={headerLogo} eventId={eventId} />
</div> </div>
<main className="flex flex-1 items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 items-center justify-center px-4 py-10">
<p className="text-sm text-[#24282f]">Loading…</p> <div className="mx-auto w-full max-w-[370px] rounded-lg card-bg p-4 text-center shadow-sm">
<p className="text-sm">Loading…</p>
</div>
</main> </main>
</MobileShell> </MobileShell>
); );
@ -868,14 +880,16 @@ export function QuizPollTakePage() {
<div className="flex shrink-0 flex-col"> <div className="flex shrink-0 flex-col">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} /> <HeaderMobile logoUrl={headerLogo} eventId={eventId} />
</div> </div>
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 flex-col items-center justify-center px-4 py-10">
<div className="mx-auto w-full max-w-[370px] rounded-lg border border-[rgba(116,112,112,0.2)] bg-white p-5 text-center shadow-sm"> <div className="mx-auto w-full max-w-[370px] rounded-lg border text-center shadow-sm">
<p className="font-inter text-lg font-semibold text-[#24282f]">Answer submitted</p> <div className="card-bg rounded-lg p-4">
<p className="mt-2 text-sm text-[rgba(36,40,47,0.75)]"> <p className="font-inter text-lg font-semibold">Answer submitted</p>
<p className="mt-2 text-sm text-[var(--theme-card-text-color)]/80">
You have already submitted this question. Please wait for the host to show the next one. You have already submitted this question. Please wait for the host to show the next one.
</p> </p>
</div> </div>
<Link to={sessionBackHref} className="mt-6 text-sm font-semibold text-[#1e72ef] underline"> </div>
<Link to={sessionBackHref} className="mt-6 text-sm font-semibold text-[var(--theme-button-color)] underline">
Back to session Back to session
</Link> </Link>
</main> </main>
@ -889,14 +903,14 @@ export function QuizPollTakePage() {
<div className="flex shrink-0 flex-col"> <div className="flex shrink-0 flex-col">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} /> <HeaderMobile logoUrl={headerLogo} eventId={eventId} />
</div> </div>
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 flex-col items-center justify-center px-4 py-10">
<div className="mx-auto w-full max-w-[370px] rounded-lg border border-[rgba(116,112,112,0.2)] bg-white p-5 text-center shadow-sm"> <div className="mx-auto w-full max-w-[370px] rounded-lg card-bg p-5 text-center shadow-sm">
<p className="font-inter text-lg font-semibold text-[#24282f]">{quiz.title}</p> <p className="font-inter text-lg font-semibold">{quiz.title}</p>
<p className="mt-2 text-sm text-[rgba(36,40,47,0.75)]"> <p className="mt-2 text-sm text-[var(--theme-card-text-color)]/80">
The host will start the next question soon. The host will start the next question soon.
</p> </p>
</div> </div>
<Link to={sessionBackHref} className="mt-6 text-sm font-semibold text-[#1e72ef] underline"> <Link to={sessionBackHref} className="mt-6 text-sm font-semibold text-[var(--theme-button-color)] underline">
Back to session Back to session
</Link> </Link>
</main> </main>
@ -906,7 +920,7 @@ export function QuizPollTakePage() {
const footerBtnClass = const footerBtnClass =
"rounded-lg px-3 py-1.5 text-center text-xs font-semibold shadow-sm disabled:opacity-50 disabled:hover:bg-inherit"; "rounded-lg px-3 py-1.5 text-center text-xs font-semibold shadow-sm disabled:opacity-50 disabled:hover:bg-inherit";
const footerSecondaryClass = `${footerBtnClass} flex-1 border border-[#e5e5ea] bg-white text-[#24282f] hover:bg-[#f2f2f7]`; const footerSecondaryClass = `${footerBtnClass} flex-1 border border-zinc-300 bg-zinc-100 text-gray-700`;
return ( return (
<MobileShell className="relative h-dvh min-h-0 flex-col"> <MobileShell className="relative h-dvh min-h-0 flex-col">
@ -915,33 +929,36 @@ export function QuizPollTakePage() {
</div> </div>
<main <main
ref={mainScrollRef} ref={mainScrollRef}
className="relative z-0 min-h-0 flex-1 overflow-y-auto overscroll-contain bg-[#f2f2f7] px-4 pt-6" className="relative z-0 min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-6"
> >
<div className="mx-auto w-full max-w-[370px] pb-4"> <div className="mx-auto w-full max-w-[370px] pb-4">
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#1e72ef]">{typeHeading(quiz.type)}</p> <div className="flex flex-col gap-1 card-bg p-4 rounded-lg shadow-sm">
<h1 className="mt-1 font-inter text-xl font-semibold text-[#24282f]">{quiz.title}</h1> <p className="text-[10px] font-semibold uppercase tracking-wide text-[var(--theme-button-color)]">{typeHeading(quiz.type)}</p>
<h1 className="mt-1 font-inte text-xl font-semibold">{quiz.title}</h1>
{isQuizMasterMode && liveQuestion ? ( {isQuizMasterMode && liveQuestion ? (
<p className="mt-2 text-xs font-medium tabular-nums text-[rgba(36,40,47,0.55)]"> <p className="mt-2 text-xs font-medium tabular-nums text-[var(--theme-card-text-color)]/80">
Question {liveQuestionIndex + 1} of {totalQuestions} Question {liveQuestionIndex + 1} of {totalQuestions}
</p> </p>
) : multiQuestion ? ( ) : multiQuestion ? (
<p className="mt-2 text-xs font-medium tabular-nums text-[rgba(36,40,47,0.55)]"> <p className="mt-2 text-xs font-medium tabular-nums text-[var(--theme-card-text-color)]/80">
Question {stepSafe + 1} of {totalQuestions} Question {stepSafe + 1} of {totalQuestions}
</p> </p>
) : null} ) : null}
</div>
{timerActive && secondsLeft != null ? ( {timerActive && secondsLeft != null ? (
<div <div
className={`mt-4 flex items-center justify-between rounded-lg border px-3 py-2.5 ${ className={`mt-4 flex items-center justify-between rounded-lg border px-3 py-2.5 ${
secondsLeft <= 30 ? "border-amber-400 bg-amber-50" : "border-[#e5e5ea] bg-white" secondsLeft <= 30 ? "border-amber-400 bg-amber-50" : "border-zinc-300 bg-zinc-100"
}`} }`}
> >
<span className="text-xs font-medium text-[#24282f]"> <span className="text-xs font-medium text-[var(--theme-card-text-color)]">
{perQuestionTimer && isQuizMasterMode ? "Time left for this question" : "Time left"} {perQuestionTimer && isQuizMasterMode ? "Time left for this question" : "Time left"}
</span> </span>
<span <span
className={`font-mono text-lg font-bold tabular-nums ${ className={`font-mono text-lg font-bold tabular-nums ${
secondsLeft <= 30 ? "text-amber-800" : "text-[#24282f]" secondsLeft <= 30 ? "text-amber-800" : "text-[var(--theme-card-text-color)]"
}`} }`}
> >
{formatCountdown(secondsLeft)} {formatCountdown(secondsLeft)}
@ -958,9 +975,9 @@ export function QuizPollTakePage() {
).map((q, offset) => { ).map((q, offset) => {
const qi = isQuizMasterMode ? liveQuestionIndex : multiQuestion ? stepSafe : offset; const qi = isQuizMasterMode ? liveQuestionIndex : multiQuestion ? stepSafe : offset;
return ( return (
<li key={q._id} className="rounded-lg border border-[rgba(116,112,112,0.2)] bg-white p-4 shadow-sm"> <li key={q._id} className="rounded-lg card-bg p-4 shadow-sm">
<p className="text-sm font-semibold text-[#24282f]"> <p className="text-sm font-semibold text-[var(--theme-card-text-color)]">
<span className="text-[#8e8e93]">{qi + 1}. </span> <span className="text-[var(--theme-card-text-color)]">{qi + 1}. </span>
{q.question} {q.question}
</p> </p>
<QuestionMedia url={q.imageUrl} alt="" /> <QuestionMedia url={q.imageUrl} alt="" />
@ -972,7 +989,7 @@ export function QuizPollTakePage() {
<label <label
key={opt._id} key={opt._id}
className={`flex cursor-pointer items-start gap-2 rounded-md border px-3 py-2 text-sm ${ className={`flex cursor-pointer items-start gap-2 rounded-md border px-3 py-2 text-sm ${
selected ? "border-[#1e72ef] bg-[#1e72ef]/5" : "border-[#e5e5ea]" selected ? "border-[var(--theme-button-color)] bg-[var(--theme-card-color)]/10 text-[var(--theme-button-color)]" : "bg-[var(--theme-card-color)]/30 border-[var(--theme-card-color)]/15 text-[var(--theme-card-text-color)]"
}`} }`}
> >
<input <input
@ -994,7 +1011,7 @@ export function QuizPollTakePage() {
value={typeof answers[q._id] === "string" ? answers[q._id] : ""} value={typeof answers[q._id] === "string" ? answers[q._id] : ""}
onChange={(e) => setText(q, e.target.value)} onChange={(e) => setText(q, e.target.value)}
rows={3} rows={3}
className="mt-3 w-full rounded-md border border-[#e5e5ea] px-3 py-2 text-sm" className="mt-3 w-full rounded-md border border-[#e5e5ea] px-3 py-2 text-sm transition-all duration-300 ease-in-out focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[var(--theme-button-color)]/30 disabled:opacity-60"
placeholder="Your answer" placeholder="Your answer"
/> />
) : q.type === "rating" ? ( ) : q.type === "rating" ? (
@ -1006,7 +1023,7 @@ export function QuizPollTakePage() {
onClick={() => setSingle(q, String(n))} onClick={() => setSingle(q, String(n))}
className={`size-10 rounded-full border text-sm font-semibold ${ className={`size-10 rounded-full border text-sm font-semibold ${
answers[q._id] === String(n) answers[q._id] === String(n)
? "border-[#1e72ef] bg-[#1e72ef] text-white" ? "border-[var(--theme-button-color)] bg-[var(--theme-button-color] text-white"
: "border-[#e5e5ea] bg-white text-[#24282f]" : "border-[#e5e5ea] bg-white text-[#24282f]"
}`} }`}
> >
@ -1020,7 +1037,7 @@ export function QuizPollTakePage() {
<label <label
key={opt._id} key={opt._id}
className={`flex cursor-pointer items-start gap-2 rounded-md border px-3 py-2 text-sm ${ className={`flex cursor-pointer items-start gap-2 rounded-md border px-3 py-2 text-sm ${
answers[q._id] === opt._id ? "border-[#1e72ef] bg-[#1e72ef]/5" : "border-[#e5e5ea]" answers[q._id] === opt._id ? "border-[var(--theme-button-color)] bg-[var(--theme-card-color)]/10 text-[var(--theme-button-color)]" : "bg-[var(--theme-card-color)]/30 border-[var(--theme-card-color)]/15 text-[var(--theme-card-text-color)]"
}`} }`}
> >
<input <input
@ -1044,20 +1061,20 @@ export function QuizPollTakePage() {
</ul> </ul>
{!signedIn ? ( {!signedIn ? (
<p className="mt-8 text-center text-xs text-[rgba(36,40,47,0.65)]"> <p className="mt-8 text-center text-xs text-[var(--theme-card-text-color)]/80">
{multiQuestion ? ( {multiQuestion ? (
<> <>
Use <span className="font-semibold text-[#24282f]">Next</span> or{" "} Use <span className="font-semibold">Next</span> or{" "}
<span className="font-semibold text-[#24282f]">Submit</span> below after{" "} <span className="font-semibold">Submit</span> below after{" "}
<Link className="font-semibold text-[#1e72ef] underline" to={visitorEventUrl(eventId)}> <Link className="font-semibold theme-btn-link" to={visitorEventUrl(eventId)}>
signing in signing in
</Link> </Link>
. .
</> </>
) : ( ) : (
<> <>
Tap <span className="font-semibold text-[#24282f]">Submit</span> below after{" "} Tap <span className="font-semibold">Submit</span> below after{" "}
<Link className="font-semibold text-[#1e72ef] underline" to={visitorEventUrl(eventId)}> <Link className="font-semibold theme-btn-link" to={visitorEventUrl(eventId)}>
signing in signing in
</Link> </Link>
. .
@ -1068,8 +1085,8 @@ export function QuizPollTakePage() {
{submitError ? <p className="mt-4 text-center text-sm text-red-600">{submitError}</p> : null} {submitError ? <p className="mt-4 text-center text-sm text-red-600">{submitError}</p> : null}
<p className="mt-6 text-center text-xs"> <p className="mt-6 text-center text-md">
<Link className="text-[#1e72ef] underline" to={sessionBackHref}> <Link className="text-[var(--theme-text-color)] underline" to={sessionBackHref}>
Back to session Back to session
</Link> </Link>
</p> </p>
@ -1077,7 +1094,7 @@ export function QuizPollTakePage() {
</main> </main>
<footer <footer
className="z-40 shrink-0 border-t border-[#e5e5ea] bg-white shadow-[0_-4px_12px_rgba(0,0,0,0.06)]" className="z-40 shrink-0 border-t border-[var(--theme-card-color)]/25 card-bg shadow-md"
style={{ paddingBottom: "max(12px, env(safe-area-inset-bottom))", paddingTop: 12, paddingLeft: 16, paddingRight: 16 }} style={{ paddingBottom: "max(12px, env(safe-area-inset-bottom))", paddingTop: 12, paddingLeft: 16, paddingRight: 16 }}
> >
<div className="mx-auto flex w-full max-w-[370px] gap-2"> <div className="mx-auto flex w-full max-w-[370px] gap-2">
@ -1096,7 +1113,7 @@ export function QuizPollTakePage() {
type="button" type="button"
disabled={submitting} disabled={submitting}
onClick={() => void runSubmit()} onClick={() => void runSubmit()}
className={`${footerBtnClass} flex-1 bg-orange-500 text-white hover:bg-orange-600 disabled:hover:bg-orange-500`} className={`${footerBtnClass} flex-1 theme-btn-bg text-white disabled:hover:bg-[var(--theme-button-color)]/50`}
> >
{submitLabel} {submitLabel}
</button> </button>
@ -1104,7 +1121,7 @@ export function QuizPollTakePage() {
<button <button
type="button" type="button"
onClick={() => setQuestionStep((s) => Math.min(totalQuestions - 1, s + 1))} onClick={() => setQuestionStep((s) => Math.min(totalQuestions - 1, s + 1))}
className={`${footerBtnClass} flex-1 bg-orange-500 text-white hover:bg-orange-600`} className={`${footerBtnClass} flex-1 theme-btn-bg text-white disabled:hover:bg-[var(--theme-button-color)]/50`}
> >
Next Next
</button> </button>
@ -1115,7 +1132,7 @@ export function QuizPollTakePage() {
type="button" type="button"
disabled={submitting} disabled={submitting}
onClick={() => void runSubmit()} onClick={() => void runSubmit()}
className="w-full rounded-lg bg-orange-500 px-3 py-1.5 text-center text-xs font-semibold text-white shadow-sm hover:bg-orange-600 disabled:opacity-60 disabled:hover:bg-orange-500" className="w-full rounded-lg theme-btn-bg px-3 py-1.5 text-center text-xs font-semibold text-white shadow-sm disabled:opacity-60 disabled:hover:bg-[var(--theme-button-color)]/50"
> >
{submitLabel} {submitLabel}
</button> </button>

View File

@ -98,14 +98,16 @@ export function RatingsPage() {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative">
<HeaderMobile logoUrl={ctx?.event?.logoUrl} eventId={null} /> <HeaderMobile logoUrl={ctx?.event?.logoUrl} eventId={null} />
<main className="flex flex-1 flex-col bg-[#f2f2f7] px-4 pb-10 pt-6"> <main className="flex flex-1 flex-col px-4 pb-10 pt-6">
<div className="mx-auto w-full max-w-[370px]"> <div className="mx-auto w-full max-w-[370px]">
<h1 className="font-inter text-xl font-semibold text-[#24282f]">Ratings</h1> <div className="card-bg rounded-lg shadow-md p-4">
<p className="mt-3 text-sm text-[rgba(36,40,47,0.67)]">Open ratings from your event or session link.</p> <h1 className="text-xl font-semibold text-[var(--theme-card-text-color)]">Ratings</h1>
<Link to="/" className="mt-4 inline-block text-sm font-semibold text-[#1e72ef] underline"> <p className="mt-3 text-sm text-[var(--theme-card-text-color)]/80">Open ratings from your event or session link.</p>
<Link to="/" className="mt-4 inline-block text-sm font-semibold theme-btn-link">
Home Home
</Link> </Link>
</div> </div>
</div>
</main> </main>
</MobileShell> </MobileShell>
); );
@ -115,14 +117,16 @@ export function RatingsPage() {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative">
<HeaderMobile logoUrl={eventSnapshot?.logoUrl} eventId={eventId || null} /> <HeaderMobile logoUrl={eventSnapshot?.logoUrl} eventId={eventId || null} />
<main className="flex flex-1 flex-col bg-[#f2f2f7] px-4 pb-10 pt-6"> <main className="flex flex-1 flex-col px-4 pb-10 pt-6">
<div className="mx-auto w-full max-w-[370px]"> <div className="mx-auto w-full max-w-[370px]">
<h1 className="font-inter text-xl font-semibold text-[#24282f]">Ratings</h1> <div className="card-bg rounded-lg shadow-md p-4">
<p className="mt-3 text-sm text-[rgba(36,40,47,0.67)]">Ratings are disabled for this event.</p> <h1 className="text-xl font-semibold text-[var(--theme-card-text-color)]">Ratings</h1>
<Link to="/event" className="mt-4 inline-block text-sm font-semibold text-[#1e72ef] underline"> <p className="mt-3 text-sm text-[var(--theme-card-text-color)]/80">Ratings are disabled for this event.</p>
<Link to="/event" className="mt-4 inline-block text-sm font-semibold theme-btn-link">
Back to event Back to event
</Link> </Link>
</div> </div>
</div>
</main> </main>
</MobileShell> </MobileShell>
); );
@ -131,27 +135,29 @@ export function RatingsPage() {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative">
<HeaderMobile logoUrl={eventSnapshot?.logoUrl} eventId={eventId || null} /> <HeaderMobile logoUrl={eventSnapshot?.logoUrl} eventId={eventId || null} />
<main className="flex flex-1 flex-col bg-[#f2f2f7] px-4 pb-10 pt-6"> <main className="flex flex-1 flex-col px-4 pb-10 pt-6">
<div className="mx-auto w-full max-w-[370px]"> <div className="mx-auto w-full max-w-[370px]">
<h1 className="font-inter text-xl font-semibold text-[#24282f]">{schema?.name || "Ratings"}</h1> <div className="card-bg rounded-lg shadow-md p-4">
<h1 className="text-xl font-semibold text-[var(--theme-card-text-color)]">{schema?.name || "Ratings"}</h1>
{schemaLoading ? ( {schemaLoading ? (
<p className="mt-4 text-sm text-[rgba(36,40,47,0.67)]">Loading form…</p> <p className="mt-4 text-sm text-[var(--theme-card-text-color)]/80">Loading form…</p>
) : schemaErr ? ( ) : schemaErr ? (
<div className="mt-4"> <div className="mt-4">
<p className="text-sm text-red-600">{schemaErr}</p> <p className="text-sm text-red-600">{schemaErr}</p>
<p className="mt-2 text-sm text-[rgba(36,40,47,0.67)]"> <p className="mt-2 text-sm text-[var(--theme-card-text-color)]/80">
Ask the organizer to publish an active ratings form for this event. Ask the organizer to publish an active ratings form for this event.
</p> </p>
<Link to="/event" className="mt-4 inline-block text-sm font-semibold text-[#1e72ef] underline"> <Link to="/event" className="mt-4 inline-block text-sm font-semibold theme-btn-link">
Back to event Back to event
</Link> </Link>
</div> </div>
) : schema ? ( ) : schema ? (
<div className="mt-6"> <div className="mt-2">
<DynamicFeedbackForm eventId={eventId} schema={schema} variant="light" /> <DynamicFeedbackForm eventId={eventId} schema={schema} variant="light" className="px-0" />
</div> </div>
) : null} ) : null}
</div> </div>
</div>
</main> </main>
</MobileShell> </MobileShell>
); );

View File

@ -70,31 +70,56 @@ export function RewardsPage() {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative">
<HeaderMobile logoUrl={ctx?.event?.logoUrl} eventId={eventId || null} /> <HeaderMobile logoUrl={ctx?.event?.logoUrl} eventId={eventId || null} />
<div className="eb-gradient-auth flex flex-1 flex-col items-center px-6 pb-10 pt-6"> <div className="eb-gradient-auth flex flex-1 flex-col items-center px-6 pb-10 pt-2">
{/* <div className="flex flex-col w-full max-w-[354px] items-center card-bg p-4 rounded-md">
<Trophy className="size-[68px] text-amber-300" strokeWidth={1.25} /> <Trophy className="size-[68px] text-amber-300" strokeWidth={1.25} />
<h1 className="mt-1 font-inter text-xl font-semibold text-white">Your Rewards</h1> <h1 className="mt-1 font-inter text-xl font-semibold text-white">Your Rewards</h1>
<Link <Link
to={eventId ? `/leaderboard?event_id=${encodeURIComponent(eventId)}` : "/leaderboard"} to={eventId ? `/leaderboard?event_id=${encodeURIComponent(eventId)}` : "/leaderboard"}
className="mt-6 w-full max-w-[354px] rounded-full border border-[rgba(214,200,200,0.6)] bg-[#ef8d1d] py-3 text-center text-xs font-semibold uppercase tracking-wide text-white shadow-[0_4px_4px_rgba(0,0,0,0.15)]" className="mt-6 w-full max-w-[354px] rounded-full border border-[rgba(214,200,200,0.6)] bg-[var(--theme-button-color)] transition-colors duration-300 ease-in-out hover:bg-[var(--theme-button-color)]/90 py-3 text-center text-xs font-semibold uppercase tracking-wide text-white shadow-[0_4px_4px_rgba(0,0,0,0.15)]"
> >
Leaderboard Leaderboard
</Link> </Link>
</div> */}
<div className="mt-6 w-full max-w-[354px] rounded-xl bg-white shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]"> <div className="mt-6 w-full max-w-[354px] rounded-xl bg-whit card-bg shadow-md">
<div className="p-4"> <div className="flex gap-2 justify-between w-full p-4">
<div className="eb-rank-highlight flex flex-col items-center justify-center gap-2 rounded-3xl border-4 border-white py-4 text-white shadow-[0_9px_16px_rgba(0,0,0,0.14)]"> <div className="flex items-center gap-2 shrink-0">
<p className="font-inter text-base font-semibold uppercase tracking-wide">Total Points</p> <div className="bg-[var(--theme-button-color)]/12 rounded-full p-3">
<p className="font-inter text-5xl font-bold tabular-nums drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)]"> <Trophy className="size-10 text-[var(--theme-button-color)]" strokeWidth={1.25} />
{displayTotal}
</p>
</div> </div>
<h2 className="mt-6 font-inter text-base font-semibold text-[#24282f]">Points history</h2> <h1 className="mt-1 text-lg font-semibold text-[var(--theme-card-text-color)]">Your Points</h1>
</div>
<div className="eb-rank-highligh flex shrink-0 grow-1 max-w-[150px] font-bold items-center justify-end">
<div className="bg-[var(--theme-button-color)] rounded-full w-[65px] h-[65px] text-3xl text-[var(--theme-button-color) text-white flex items-center justify-center">
{displayTotal}
</div>
</div>
</div>
<div className="flex justify-center px-4">
{/* <Link
to={eventId ? `/leaderboard?event_id=${encodeURIComponent(eventId)}` : "/leaderboard"}
className="w-full rounded-ful rounded-lg border border-[rgba(214,200,200,0.6)] bg-[var(--theme-button-color)] transition-colors duration-300 ease-in-out hover:bg-[var(--theme-button-color)]/90 py-3 text-center text-xs font-semibold uppercase tracking-wide text-white shadow-[0_4px_4px_rgba(0,0,0,0.15)]"
>
Leaderboard
</Link> */}
<Link
to={eventId ? `/leaderboard?event_id=${encodeURIComponent(eventId)}` : "/leaderboard"}
className="w-full rounded-ful rounded-lg border border-[var(--theme-button-color)]/25 bg-[var(--theme-button-color)]/20 text-[var(--theme-button-color)] py-3 text-center text-sm font-semibold uppercase tracking-wide shadow-sm"
>
Leaderboard
</Link>
</div>
<div className="p-4">
<h2 className="mt-2 font-inter text-base font-semibold text-[var(--theme-card-text-color)]/80">Points history</h2>
<ul className="mt-3 flex flex-col gap-3"> <ul className="mt-3 flex flex-col gap-3">
{historyLoading && ( {historyLoading && (
<li className="text-xs text-[rgba(36,40,47,0.67)]">Loading…</li> <li className="text-xs text-[var(--theme-card-text-color)]/70">Loading…</li>
)} )}
{!historyLoading && historyItems.length === 0 && ( {!historyLoading && historyItems.length === 0 && (
<li className="text-xs text-[rgba(36,40,47,0.67)]">No transactions yet.</li> <li className="text-xs text-[var(--theme-card-text-color)]/70">No transactions yet.</li>
)} )}
{!historyLoading && {!historyLoading &&
historyItems.map((row) => { historyItems.map((row) => {
@ -108,13 +133,13 @@ export function RewardsPage() {
return ( return (
<li <li
key={row._id} key={row._id}
className="flex gap-4 border-b border-[#ededed] pb-3 last:border-0" className="flex items-center gap-4 border-b border-[var(--theme-card-text-color)]/5 pb-3 last:border-0"
> >
<span className={`shrink-0 rounded px-3 py-2 text-xs font-medium shadow-sm ${cls}`}> <span className={`shrink-0 rounded px-3 py-2 text-xs font-medium shadow-sm ${cls}`}>
{signed} {signed}
{row.points} pts {row.points} pts
</span> </span>
<span className="text-xs leading-snug text-[rgba(36,40,47,0.67)]"> <span className="text-sm leading-snug text-[var(--theme-card-text-color)]/70">
{historyLabel(row.source, row.description)} {historyLabel(row.source, row.description)}
</span> </span>
</li> </li>

View File

@ -363,13 +363,13 @@ export function SingleEventPage() {
if (!eventId || !sessionId) { if (!eventId || !sessionId) {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile <HeaderMobile
eventName={headerEventNameForEvent(eventId, null, ctx)} eventName={headerEventNameForEvent(eventId, null, ctx)}
eventId={eventId || null} eventId={eventId || null}
showBack={false} showBack={false}
/> />
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 flex-col items-center justify-center px-4 py-10">
<p className="text-center text-sm text-[#24282f]">{err}</p> <p className="text-center text-sm text-[#24282f]">{err}</p>
<Link to="/" className="mt-4 text-sm font-semibold text-[#1e72ef] underline"> <Link to="/" className="mt-4 text-sm font-semibold text-[#1e72ef] underline">
Home Home
@ -381,14 +381,14 @@ export function SingleEventPage() {
if (loading) { if (loading) {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile <HeaderMobile
logoUrl={headerLogoForEvent(eventId, null, ctx)} logoUrl={headerLogoForEvent(eventId, null, ctx)}
eventName={headerEventNameForEvent(eventId, null, ctx)} eventName={headerEventNameForEvent(eventId, null, ctx)}
eventId={eventId || null} eventId={eventId || null}
showBack={false} showBack={false}
/> />
<main className="flex flex-1 items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 items-center justify-center px-4 py-10">
<p className="text-sm text-[#24282f]">Loading session…</p> <p className="text-sm text-[#24282f]">Loading session…</p>
</main> </main>
</MobileShell> </MobileShell>
@ -397,14 +397,14 @@ export function SingleEventPage() {
if (err || !session) { if (err || !session) {
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile <HeaderMobile
logoUrl={headerLogoForEvent(eventId, eventInfo, ctx)} logoUrl={headerLogoForEvent(eventId, eventInfo, ctx)}
eventName={headerEventNameForEvent(eventId, eventInfo, ctx)} eventName={headerEventNameForEvent(eventId, eventInfo, ctx)}
eventId={eventId || null} eventId={eventId || null}
showBack={false} showBack={false}
/> />
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10"> <main className="flex flex-1 flex-col items-center justify-center px-4 py-10">
<p className="text-center text-sm text-red-600">{err ?? "Session not found."}</p> <p className="text-center text-sm text-red-600">{err ?? "Session not found."}</p>
</main> </main>
</MobileShell> </MobileShell>
@ -451,7 +451,7 @@ export function SingleEventPage() {
Boolean(sessionNotes || sessionLocation || trackHall || hasCapacity); Boolean(sessionNotes || sessionLocation || trackHall || hasCapacity);
return ( return (
<MobileShell className="relative"> <MobileShell className="relative bg-transparent">
<HeaderMobile <HeaderMobile
logoUrl={headerLogoUrl} logoUrl={headerLogoUrl}
eventName={headerEventName} eventName={headerEventName}
@ -469,7 +469,7 @@ export function SingleEventPage() {
</div> </div>
) : null} ) : null}
<div className="eb-gradient-banner relative flex flex-col items-center justify-center gap-1.5 px-6 pb-4 pt-3 text-center text-white"> <div className="card-bg relative flex flex-col justify-center rounded-b-lg shadow-md gap-1.5 px-4 pb-4 pt-3 text-[var(--theme-card-text-color)]">
<div className="absolute right-3 top-2.5 z-10 flex max-w-[calc(100%-4.5rem)] flex-col items-end gap-1.5"> <div className="absolute right-3 top-2.5 z-10 flex max-w-[calc(100%-4.5rem)] flex-col items-end gap-1.5">
{live ? ( {live ? (
<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-[#14ae5c] px-2 py-0.5 text-[10px] font-semibold text-white"> <span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-[#14ae5c] px-2 py-0.5 text-[10px] font-semibold text-white">
@ -480,37 +480,40 @@ export function SingleEventPage() {
<button <button
type="button" type="button"
onClick={() => toggleBookmark(sessionId)} onClick={() => toggleBookmark(sessionId)}
className="shrink-0 rounded-md p-1 text-white hover:bg-white/15" className="shrink-0 rounded-md p-1 text-[var(--theme-button-color)]"
aria-label={sessionBookmarked ? "Remove bookmark" : "Bookmark session"} aria-label={sessionBookmarked ? "Remove bookmark" : "Bookmark session"}
aria-pressed={sessionBookmarked} aria-pressed={sessionBookmarked}
> >
<Bookmark className={`size-6 ${sessionBookmarked ? "fill-current" : ""}`} strokeWidth={1.75} /> <Bookmark className={`size-6 ${sessionBookmarked ? "fill-current" : ""}`} strokeWidth={1.75} />
</button> </button>
</div> </div>
<div className="flex max-w-full flex-wrap items-center justify-center gap-x-2 gap-y-1 px-2 text-[10px] text-white/90"> <div className="flex max-w-full flex-wrap items-center gap-x-2 gap-y-1 pe-2 text-xs">
<span className="rounded bg-white/20 px-2 py-0.5">{typeLabel}</span> <span className="rounded bg-[var(--theme-button-color)]/20 px-2 py-0.5">{typeLabel}</span>
{timeLine ? <span>{timeLine}</span> : null} {timeLine ? <span>{timeLine}</span> : null}
{trackTitle !== typeLabel ? <span className="opacity-90">{trackTitle}</span> : null} {trackTitle !== typeLabel ? <span className="opacity-90">{trackTitle}</span> : null}
</div> </div>
<h1 className="font-inter text-2xl font-bold tracking-tight">{session.title}</h1> <h1 className="text-xl font-semibold tracking-tight">{session.title}</h1>
<div className="flex w-full max-w-full flex-wrap items-center justify-end gap-x-3 gap-y-1 pr-1 text-sm text-white/95"> <div className="flex w-full max-w-full flex-wrap items-center justify-between gap-x-3 gap-y-1 pr-1 text-sm text-[var(--theme-card-text-color)]/70">
<div className="flex items-center gap-1.5 text-sm text-[var(--theme-card-text-color)]/80">
{duration ? ( {duration ? (
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<Clock className="size-4 shrink-0" strokeWidth={1.75} /> <Clock className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
{startT} ({duration}) {startT} ({duration})
</span> </span>
) : startT ? ( ) : startT ? (
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<Clock className="size-4 shrink-0" strokeWidth={1.75} /> <Clock className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
{startT} {startT}
</span> </span>
) : null} ) : null}
{sessionDate ? ( {sessionDate ? (
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<Calendar className="size-4 shrink-0" strokeWidth={1.75} /> <Calendar className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
{sessionDate} {sessionDate}
</span> </span>
) : null} ) : null}
</div>
<div className="flex items-center justify-end gap-1.5">
{hasExpandableSessionMeta ? ( {hasExpandableSessionMeta ? (
<button <button
type="button" type="button"
@ -519,7 +522,7 @@ export function SingleEventPage() {
aria-label={ aria-label={
sessionMetaOpen ? "Hide session notes and location" : "Show session notes and location" sessionMetaOpen ? "Hide session notes and location" : "Show session notes and location"
} }
className="inline-flex shrink-0 items-center rounded-md p-0.5 text-white hover:bg-white/15" className="inline-flex shrink-0 items-center rounded-md p-0.5 bg-[var(--theme-button-color)]/5 text-[var(--theme-card-text-color)] hover:bg-[var(--theme-button-color)]/15"
> >
<ChevronDown <ChevronDown
className={`size-5 transition-transform ${sessionMetaOpen ? "rotate-180" : ""}`} className={`size-5 transition-transform ${sessionMetaOpen ? "rotate-180" : ""}`}
@ -530,27 +533,29 @@ export function SingleEventPage() {
) : null} ) : null}
</div> </div>
</div>
{sessionMetaOpen && hasExpandableSessionMeta ? ( {sessionMetaOpen && hasExpandableSessionMeta ? (
<div className="mt-2 w-full max-w-full border-t border-white/25 px-1 pt-3 text-left text-sm text-white/95"> <div className="mt-2 w-full max-w-full border-t border-[var(--theme-card-text-color)]/5 px-1 pt-3 text-left text-sm text-[var(--theme-card-text-color)]/95">
{sessionNotes ? ( {sessionNotes ? (
<div className="mb-3"> <div className="mb-3">
<p className="text-[10px] font-semibold uppercase tracking-wide text-white/70">Notes</p> <p className="text-xs font-semibold uppercase tracking-wide text-[var(--theme-card-text-color)]/80">Notes</p>
<p className="mt-1.5 whitespace-pre-wrap leading-snug">{sessionNotes}</p> <p className="mt-1.5 whitespace-pre-wrap leading-snug">{sessionNotes}</p>
</div> </div>
) : null} ) : null}
{sessionLocation || trackHall ? ( {sessionLocation || trackHall ? (
<div> <div>
<p className="text-[10px] font-semibold uppercase tracking-wide text-white/70">Location</p> <p className="text-xs font-semibold uppercase tracking-wide text-[var(--theme-card-text-color)]/80">Location</p>
<ul className="mt-1.5 flex flex-col gap-1.5"> <ul className="mt-1.5 flex flex-col gap-1.5">
{sessionLocation ? ( {sessionLocation ? (
<li className="flex items-start gap-2 leading-snug"> <li className="flex items-start gap-2 leading-snug">
<MapPin className="mt-0.5 size-4 shrink-0 text-white/90" strokeWidth={1.75} /> <MapPin className="mt-0.5 size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span>{sessionLocation}</span> <span>{sessionLocation}</span>
</li> </li>
) : null} ) : null}
{trackHall ? ( {trackHall ? (
<li className="flex items-start gap-2 leading-snug"> <li className="flex items-start gap-2 leading-snug">
<MapPin className="mt-0.5 size-4 shrink-0 text-white/90" strokeWidth={1.75} /> <MapPin className="mt-0.5 size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span>{trackHall}</span> <span>{trackHall}</span>
</li> </li>
) : null} ) : null}
@ -558,13 +563,13 @@ export function SingleEventPage() {
</div> </div>
) : null} ) : null}
{hasCapacity ? ( {hasCapacity ? (
<p className="mt-3 text-xs text-white/85">Capacity: {session.capacity}</p> <p className="mt-3 text-xs font-semibold uppercase text-[var(--theme-card-text-color)]/80">Capacity: {session.capacity}</p>
) : null} ) : null}
</div> </div>
) : null} ) : null}
</div> </div>
<main className="relative z-0 w-full max-w-none flex-1 bg-[#f2f2f7] pb-28 pt-4"> <main className="test-class relative z-0 w-full max-w-none flex-1 px-4 pb-28 pt-4">
<div className="flex w-full max-w-none flex-col gap-4"> <div className="flex w-full max-w-none flex-col gap-4">
{session && session.enableQnA !== false ? ( {session && session.enableQnA !== false ? (
<SessionQuestionsCard <SessionQuestionsCard
@ -578,33 +583,33 @@ export function SingleEventPage() {
<SessionQuizPollsSection eventId={eventId} sessionId={sessionId} /> <SessionQuizPollsSection eventId={eventId} sessionId={sessionId} />
{showSessionFeedback || showSessionRatings ? ( {showSessionFeedback || showSessionRatings ? (
<div className="flex w-full max-w-none flex-col gap-2"> <div className="flex w-full max-w-none flex-col card-bg rounded-lg shadow-md gap-2 p-3">
<p className="px-1 text-[10px] font-semibold uppercase tracking-wide text-[#8e8e93]"> <p className="px-1 text-md font-semibold uppercas tracking-wide">
Feedback & ratings Feedback & ratings
</p> </p>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{showSessionFeedback ? ( {showSessionFeedback ? (
<Link <Link
to={sessionFeedbackHref} to={sessionFeedbackHref}
className="flex items-center justify-between gap-3 rounded-lg border border-[rgba(116,112,112,0.2)] bg-white px-3 py-3 text-sm font-medium text-[#24282f] shadow-[0_1px_4px_rgba(12,12,13,0.05)] active:bg-[#f2f2f7]" className="flex items-center justify-between gap-3 rounded-lg border border-[var(--theme-button-color)]/9 bg-[var(--theme-button-color)]/10 px-3 py-4 text-sm font-medium text-[var(--theme-card-text-color)] active:bg-[var(--theme-button-color)]/15"
> >
<span className="flex min-w-0 items-center gap-2.5"> <span className="flex min-w-0 items-center gap-2.5">
<MessageCircle className="size-[18px] shrink-0 text-[#1e72ef]" strokeWidth={1.75} /> <MessageCircle className="size-[18px] shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="truncate">Session feedback</span> <span className="truncate">Session feedback</span>
</span> </span>
<ChevronRight className="size-4 shrink-0 text-[#c7c7cc]" strokeWidth={2} aria-hidden /> <ChevronRight className="size-4 shrink-0 text-[var(--theme-card-text-color)]/70" strokeWidth={2} aria-hidden />
</Link> </Link>
) : null} ) : null}
{showSessionRatings ? ( {showSessionRatings ? (
<Link <Link
to={sessionRatingsHref} to={sessionRatingsHref}
className="flex items-center justify-between gap-3 rounded-lg border border-[rgba(116,112,112,0.2)] bg-white px-3 py-3 text-sm font-medium text-[#24282f] shadow-[0_1px_4px_rgba(12,12,13,0.05)] active:bg-[#f2f2f7]" className="flex items-center justify-between gap-3 rounded-lg border border-[var(--theme-button-color)]/9 bg-[var(--theme-button-color)]/10 px-3 py-4 text-sm font-medium text-[var(--theme-card-text-color)] active:bg-[var(--theme-button-color)]/15"
> >
<span className="flex min-w-0 items-center gap-2.5"> <span className="flex min-w-0 items-center gap-2.5">
<Star className="size-[18px] shrink-0 text-[#1e72ef]" strokeWidth={1.75} /> <Star className="size-[18px] shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="truncate">Rate this session</span> <span className="truncate">Rate this session</span>
</span> </span>
<ChevronRight className="size-4 shrink-0 text-[#c7c7cc]" strokeWidth={2} aria-hidden /> <ChevronRight className="size-4 shrink-0 text-[var(--theme-card-text-color)]/70" strokeWidth={2} aria-hidden />
</Link> </Link>
) : null} ) : null}
</div> </div>

View File

@ -44,8 +44,8 @@ import {
function WhiteCard({ children }: { children: ReactNode }) { function WhiteCard({ children }: { children: ReactNode }) {
return ( return (
<div className="flex w-full max-w-[354px] flex-col items-center"> <div className="flex w-full max-w-[354px] flex-col items-center">
<div className="w-full rounded-xl shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]"> <div className="w-full rounded-xl shadow-md">
<div className="rounded-xl bg-white px-6 py-6">{children}</div> <div className="rounded-xl card-bg px-6 py-6">{children}</div>
</div> </div>
</div> </div>
); );
@ -387,12 +387,12 @@ export function SocialSharePage() {
/> />
</div> </div>
) : null} ) : null}
<h1 className="mb-2 text-center text-[22px] font-semibold text-[#1a1a1a]">Your photo</h1> <h1 className="mb-2 text-center text-[22px] font-semibold">Your photo</h1>
<p className="mb-4 text-center text-sm text-[#666]"> <p className="mb-4 text-center text-sm text-[var(--theme-card-text-color)]/80">
Here’s your framed photo. You can download or share it. Here's your framed photo. You can download or share it.
</p> </p>
{(done.fileUrl || done.previewUrl) ? ( {(done.fileUrl || done.previewUrl) ? (
<div className="mb-4 overflow-hidden rounded-lg border border-[#e5e5e5] bg-[#f8f8f8]"> <div className="mb-4 overflow-hidden rounded-lg">
<img <img
ref={resultImgRef} ref={resultImgRef}
crossOrigin="anonymous" crossOrigin="anonymous"
@ -417,7 +417,7 @@ export function SocialSharePage() {
<button <button
type="button" type="button"
onClick={resetForNew} onClick={resetForNew}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-[#D32F2F] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c]" className="inline-flex h-11 w-full items-center justify-center rounded-lg theme-btn-bg px-4 text-sm font-semibold text-white"
> >
Create another Create another
</button> </button>
@ -425,7 +425,7 @@ export function SocialSharePage() {
<button <button
type="button" type="button"
onClick={() => void onDownload()} onClick={() => void onDownload()}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-[#ddd] bg-white px-4 text-sm font-semibold text-[#333] hover:bg-[#fafafa]" className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-zinc-300 bg-zinc-100 px-4 text-sm font-semibold text-gray-600"
> >
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />
Download photo Download photo
@ -434,14 +434,14 @@ export function SocialSharePage() {
</div> </div>
{showShare ? ( {showShare ? (
<div className="mt-4"> <div className="mt-4">
<p className="mb-2 text-center text-xs font-semibold uppercase tracking-wide text-[#888]"> <p className="mb-2 text-center text-xs font-semibold uppercase tracking-wide text-[var(--theme-card-text-color)]/60">
Share Share
</p> </p>
{useNativeShare ? ( {useNativeShare ? (
<button <button
type="button" type="button"
onClick={() => void onNativeShare()} onClick={() => void onNativeShare()}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-[#ddd] bg-white px-4 text-sm font-semibold text-[#333] hover:bg-[#fafafa]" className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-zinc-300 bg-zinc-100 px-4 text-sm font-semibold text-gray-600"
> >
<Share2 className="h-4 w-4" /> <Share2 className="h-4 w-4" />
Share Share
@ -453,7 +453,7 @@ export function SocialSharePage() {
key={item.key} key={item.key}
type="button" type="button"
onClick={() => void onShare(item.key)} onClick={() => void onShare(item.key)}
className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-[#ddd] bg-white px-3 text-xs font-semibold text-[#333] hover:bg-[#fafafa]" className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-zinc-300 bg-zinc-100 px-3 text-xs font-semibold text-gray-600"
> >
<ShareIcon platform={item.key} /> <ShareIcon platform={item.key} />
{item.label} {item.label}
@ -484,22 +484,22 @@ export function SocialSharePage() {
{meta?.accessMode === "login" && !loggedIn ? ( {meta?.accessMode === "login" && !loggedIn ? (
<div className="space-y-3"> <div className="space-y-3">
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Social Share</h1> <h1 className="mb-1 text-center text-[22px] font-semibold">Social Share</h1>
<p className="text-center text-sm text-[#666]">Sign in to create and share your photo.</p> <p className="text-center text-sm text-[var(--theme-card-text-color)]/80">Sign in to create and share your photo.</p>
<button <button
type="button" type="button"
onClick={() => void goLogin()} onClick={() => void goLogin()}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-[#D32F2F] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c]" className="inline-flex h-11 w-full items-center justify-center rounded-lg theme-btn-bg px-4 text-sm font-semibold text-white hover:bg-[#b71c1c]"
> >
Sign in to continue Sign in to continue
</button> </button>
</div> </div>
) : !selectedFrame ? ( ) : !selectedFrame ? (
<div className="space-y-4"> <div className="space-y-4">
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Choose a frame</h1> <h1 className="mb-1 text-center text-[22px] font-semibold">Choose a frame</h1>
<p className="text-center text-sm text-[#666]">Pick a frame, then upload your photo.</p> <p className="text-center text-sm text-[var(--theme-card-text-color)]/80">Pick a frame, then upload your photo.</p>
{!meta?.frames?.length ? ( {!meta?.frames?.length ? (
<p className="text-center text-sm text-[#666]">No frames available yet.</p> <p className="text-center text-sm text-[var(--theme-card-text-color)]/80">No frames available yet.</p>
) : ( ) : (
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{meta.frames.map((frame) => ( {meta.frames.map((frame) => (
@ -507,9 +507,9 @@ export function SocialSharePage() {
key={frame._id} key={frame._id}
type="button" type="button"
onClick={() => setSelectedFrame(frame)} onClick={() => setSelectedFrame(frame)}
className="overflow-hidden rounded-lg border border-[#e5e5e5] bg-[#f8f8f8] text-left hover:border-[#D32F2F]" className="overflow-hidden rounded-lg border border-[var(--theme-card-text-color)]/15 text-left hover:border-[var(--theme-card-text-color)]/25"
> >
<div className="aspect-square w-full bg-white"> <div className="aspect-square w-full">
{frame.frameImageUrl ? ( {frame.frameImageUrl ? (
<img <img
src={frame.frameImageUrl} src={frame.frameImageUrl}
@ -517,12 +517,12 @@ export function SocialSharePage() {
className="h-full w-full object-contain" className="h-full w-full object-contain"
/> />
) : ( ) : (
<div className="flex h-full items-center justify-center text-xs text-[#999]"> <div className="flex h-full items-center justify-center text-xs text-[var(--theme-card-text-color)]/60">
No preview No preview
</div> </div>
)} )}
</div> </div>
<p className="truncate px-2 py-2 text-center text-xs font-semibold text-[#333]"> <p className="truncate px-2 py-2 text-center text-xs font-semibold text-[var(--theme-card-text-color)]">
{frame.name} {frame.name}
</p> </p>
</button> </button>
@ -532,7 +532,7 @@ export function SocialSharePage() {
{err ? <p className="text-center text-sm text-red-600">{err}</p> : null} {err ? <p className="text-center text-sm text-red-600">{err}</p> : null}
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4 text-center">
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
@ -540,13 +540,13 @@ export function SocialSharePage() {
clearSelectedPhoto(); clearSelectedPhoto();
setErr(null); setErr(null);
}} }}
className="text-sm font-semibold text-[#D32F2F]" className="text-sm font-semibold theme-btn-link"
> >
← Change frame ← Change frame
</button> </button>
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Upload photo</h1> <h1 className="mb-1 text-center text-[22px] font-semibold text-[var(--theme-card-text-color)]">Upload photo</h1>
<p className="text-center text-sm text-[#666]"> <p className="text-center text-sm text-[var(--theme-card-text-color)]/80">
Frame: <span className="font-semibold text-[#333]">{selectedFrame.name}</span> Frame: <span className="font-semibold">{selectedFrame.name}</span>
</p> </p>
<input <input
@ -558,7 +558,7 @@ export function SocialSharePage() {
onChange={(e) => void handleFileChange(e.target.files?.[0] || null)} onChange={(e) => void handleFileChange(e.target.files?.[0] || null)}
/> />
{previewUrl ? ( {previewUrl ? (
<div className="overflow-hidden rounded-lg border border-[#e5e5e5] bg-[#f8f8f8]"> <div className="overflow-hidden rounded-lg">
<img src={previewUrl} alt="Your photo" className="mx-auto max-h-[240px] w-full object-contain" /> <img src={previewUrl} alt="Your photo" className="mx-auto max-h-[240px] w-full object-contain" />
</div> </div>
) : null} ) : null}
@ -566,7 +566,7 @@ export function SocialSharePage() {
type="button" type="button"
disabled={picking} disabled={picking}
onClick={() => fileRef.current?.click()} onClick={() => fileRef.current?.click()}
className="inline-flex h-11 w-full items-center justify-center rounded-lg border border-[#ddd] bg-white px-4 text-sm font-semibold text-[#333] hover:bg-[#fafafa] disabled:opacity-55" className="inline-flex h-11 w-full items-center justify-center rounded-lg border border-zinc-300 bg-zinc-100 px-4 text-sm font-semibold text-gray-600 disabled:opacity-55"
> >
{picking ? "Processing…" : file ? "Change photo" : "Choose photo"} {picking ? "Processing…" : file ? "Change photo" : "Choose photo"}
</button> </button>
@ -574,7 +574,7 @@ export function SocialSharePage() {
type="button" type="button"
disabled={!file || submitting} disabled={!file || submitting}
onClick={() => void onSubmit()} onClick={() => void onSubmit()}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-[#D32F2F] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c] disabled:opacity-55" className="inline-flex h-11 w-full items-center justify-center rounded-lg theme-btn-bg px-4 text-sm font-semibold text-white disabled:opacity-55"
> >
{submitting ? "Creating…" : "Create photo"} {submitting ? "Creating…" : "Create photo"}
</button> </button>

229
src/theme/eventTheme.ts Normal file
View File

@ -0,0 +1,229 @@
import type { EventThemeConfig } from "../types/visitorContext";
const DEFAULT_THEME_FONT = "Open Sans";
type FontFaceSpec = { href: string; heading: string; body: string };
/** Single-family fonts for theme.font (heading + body share the same family). */
export const THEME_FONT_SPECS: Record<string, FontFaceSpec> = {
"Open Sans": {
href: "https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;700;800&display=swap",
heading: '"Open Sans", system-ui, sans-serif',
body: '"Open Sans", system-ui, sans-serif',
},
Montserrat: {
href: "https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;600;700;800&display=swap",
heading: '"Montserrat", system-ui, sans-serif',
body: '"Montserrat", system-ui, sans-serif',
},
Roboto: {
href: "https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap",
heading: '"Roboto", system-ui, sans-serif',
body: '"Roboto", system-ui, sans-serif',
},
Poppins: {
href: "https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700;800&display=swap",
heading: '"Poppins", system-ui, sans-serif',
body: '"Poppins", system-ui, sans-serif',
},
Raleway: {
href: "https://fonts.googleapis.com/css2?family=Raleway:wght@300;400;600;700;800&display=swap",
heading: '"Raleway", system-ui, sans-serif',
body: '"Raleway", system-ui, sans-serif',
},
};
const THEME_FONT_ALIASES: Record<string, string> = Object.fromEntries(
Object.keys(THEME_FONT_SPECS).map((name) => [name.toLowerCase(), name]),
);
const THEME_VAR_KEYS = [
"--theme-heading-font",
"--theme-body-font",
"--theme-primary-color",
"--theme-secondary-color",
"--theme-header-color",
"--theme-button-color",
"--theme-text-color",
"--theme-text-highlight-color",
"--theme-card-color",
"--theme-card-text-color",
"--theme-bg-color",
"--theme-bg-image",
"--theme-bg-size",
"--theme-bg-repeat",
"--theme-bg-position",
] as const;
function normalizeHex(input?: string | null): string | null {
if (input == null) return null;
let s = String(input).trim();
if (!s) return null;
if (!s.startsWith("#")) s = `#${s}`;
if (/^#([0-9a-fA-F]{3})$/.test(s)) {
const x = s.slice(1);
s = `#${x[0]}${x[0]}${x[1]}${x[1]}${x[2]}${x[2]}`;
}
if (!/^#([0-9a-fA-F]{6})$/.test(s)) return null;
return s;
}
/** Accepts hex / rgb() / rgba() / named colors as CSS color strings. */
function normalizeCssColor(input?: string | null): string | null {
if (input == null) return null;
const s = String(input).trim();
if (!s) return null;
const hex = normalizeHex(s);
if (hex) return hex;
if (/^(rgb|rgba|hsl|hsla)\(/i.test(s) || /^[a-z]+$/i.test(s)) return s;
return s;
}
function normalizeBgRepeat(repeat?: string | null): string {
const raw = String(repeat ?? "").trim().toLowerCase();
if (!raw || raw === "no" || raw === "no-repeat" || raw === "false") return "no-repeat";
if (raw === "yes" || raw === "true" || raw === "repeat") return "repeat";
if (raw === "repeat-x" || raw === "repeat-y" || raw === "space" || raw === "round") return raw;
return "no-repeat";
}
/**
* Resolves theme.font → font face. Defaults to Open Sans.
* Unknown values fall back to Open Sans.
*/
export function resolveThemeFont(config?: EventThemeConfig | null): FontFaceSpec {
const raw = config?.font != null ? String(config.font).trim() : "";
const key = raw ? THEME_FONT_ALIASES[raw.toLowerCase()] : DEFAULT_THEME_FONT;
return THEME_FONT_SPECS[key] || THEME_FONT_SPECS[DEFAULT_THEME_FONT];
}
function themeBackgroundVars(theme?: EventThemeConfig | null): Record<string, string> {
if (!theme) return {};
const mode = String(theme.theme || "").trim().toLowerCase();
const vars: Record<string, string> = {
"--theme-bg-color": "transparent",
"--theme-bg-image": "none",
"--theme-bg-size": "auto",
"--theme-bg-repeat": "no-repeat",
"--theme-bg-position": "center center",
};
if (mode === "background") {
const url = theme.background?.url != null ? String(theme.background.url).trim() : "";
if (url) {
vars["--theme-bg-image"] = `url("${url.replace(/"/g, '\\"')}")`;
vars["--theme-bg-size"] = String(theme.background?.size || "cover").trim() || "cover";
vars["--theme-bg-repeat"] = normalizeBgRepeat(theme.background?.repeat);
vars["--theme-bg-position"] = "center center";
}
return vars;
}
if (mode === "gradient") {
const c1 =
normalizeCssColor(theme.gradient?.color1) ||
normalizeCssColor(theme.primaryColor) ||
"#1e79f0";
const c2 =
normalizeCssColor(theme.gradient?.color2) ||
normalizeCssColor(theme.secondaryColor) ||
"#bd35e6";
vars["--theme-bg-image"] = `linear-gradient(135deg, ${c1} 0%, ${c2} 100%)`;
vars["--theme-bg-size"] = "cover";
vars["--theme-bg-repeat"] = "no-repeat";
return vars;
}
if (mode === "simple") {
const color =
normalizeCssColor(theme.color) ||
normalizeCssColor(theme.primaryColor) ||
"#1e79f0";
vars["--theme-bg-color"] = color;
vars["--theme-bg-image"] = "none";
return vars;
}
return vars;
}
function themePaletteVars(theme?: EventThemeConfig | null): Record<string, string> {
if (!theme) return {};
const vars: Record<string, string> = {};
const primary = normalizeCssColor(theme.primaryColor);
const secondary = normalizeCssColor(theme.secondaryColor);
const header = normalizeCssColor(theme.headerColor);
const button = normalizeCssColor(theme.buttonColor);
const text = normalizeCssColor(theme.textColor);
const textHighlight = normalizeCssColor(theme.textHighlightColor);
const card = normalizeCssColor(theme.cardColor);
const cardText = normalizeCssColor(theme.cardTextColor);
if (primary) vars["--theme-primary-color"] = primary;
if (secondary) vars["--theme-secondary-color"] = secondary;
if (header) vars["--theme-header-color"] = header;
if (button) vars["--theme-button-color"] = button;
if (text) vars["--theme-text-color"] = text;
if (textHighlight) vars["--theme-text-highlight-color"] = textHighlight;
if (card) vars["--theme-card-color"] = card;
if (cardText) vars["--theme-card-text-color"] = cardText;
return vars;
}
/** Build CSS custom properties for an event theme (fonts + palette + page background). */
export function eventThemeCssVars(theme?: EventThemeConfig | null): Record<string, string> {
const spec = resolveThemeFont(theme);
return {
"--theme-heading-font": spec.heading,
"--theme-body-font": spec.body,
...themePaletteVars(theme),
...themeBackgroundVars(theme),
};
}
export function loadEventThemeFont(theme?: EventThemeConfig | null): void {
if (typeof document === "undefined") return;
const spec = resolveThemeFont(theme);
const id = "eb-event-theme-fonts";
let link = document.getElementById(id) as HTMLLinkElement | null;
if (!link) {
link = document.createElement("link");
link.id = id;
link.rel = "stylesheet";
document.head.appendChild(link);
}
if (link.href !== spec.href) {
link.href = spec.href;
}
}
function applyCssVars(el: HTMLElement, vars: Record<string, string>) {
for (const [key, value] of Object.entries(vars)) {
el.style.setProperty(key, value);
}
}
function clearCssVars(el: HTMLElement, keys: readonly string[]) {
for (const key of keys) {
el.style.removeProperty(key);
}
}
/** Apply theme CSS variables + font to `<html>`. Pass null/undefined to clear. */
export function applyEventTheme(theme?: EventThemeConfig | null): void {
if (typeof document === "undefined") return;
const html = document.documentElement;
clearCssVars(html, THEME_VAR_KEYS);
if (!theme) return;
loadEventThemeFont(theme);
applyCssVars(html, eventThemeCssVars(theme));
html.dataset.ebTheme = String(theme.theme || "").trim().toLowerCase() || "";
}
export function clearEventTheme(): void {
if (typeof document === "undefined") return;
const html = document.documentElement;
clearCssVars(html, THEME_VAR_KEYS);
delete html.dataset.ebTheme;
}

View File

@ -15,6 +15,42 @@ export interface ResolveCodeResponse {
trackId?: string; trackId?: string;
} }
/** Page background preset from public visitor-theme API. */
export type EventThemeMode = "simple" | "gradient" | "background" | "pattern" | string;
/** Single-family fonts driven by theme.font (same set as Eventbuzz_Web). */
export type EventThemeFont = "Open Sans" | "Montserrat" | "Roboto" | "Poppins" | "Raleway";
export type EventThemeBackground = {
url?: string | null;
size?: string | null;
/** `"no"` / `"no-repeat"` → no-repeat; `"yes"` / `"repeat"` → repeat */
repeat?: string | null;
};
export type EventThemeGradient = {
color1?: string | null;
color2?: string | null;
};
/** Dynamic styling config from GET /api/public/visitor-theme/:eventId → CSS vars on `<html>`. */
export interface EventThemeConfig {
theme: EventThemeMode;
color?: string | null;
pattern?: string | null;
background?: EventThemeBackground | null;
gradient?: EventThemeGradient | null;
font?: EventThemeFont | string | null;
primaryColor?: string | null;
secondaryColor?: string | null;
headerColor?: string | null;
buttonColor?: string | null;
textColor?: string | null;
textHighlightColor?: string | null;
cardColor?: string | null;
cardTextColor?: string | null;
}
/** Visitor login-preview: full event (no website / createdAt / updatedAt) + computed logoUrl. */ /** Visitor login-preview: full event (no website / createdAt / updatedAt) + computed logoUrl. */
export interface LoginPreviewResponse { export interface LoginPreviewResponse {
_id: string; _id: string;