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

View File

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

View File

@ -35,39 +35,39 @@ export type AgendaRowTrackV2Data = {
export function AgendaRowTrackV2({ row }: { row: AgendaRowTrackV2Data }) {
const [detailsOpen, setDetailsOpen] = useState(false);
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 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 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 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 (
<div className="relative w-full pb-4">
<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-wrap items-center gap-x-2 gap-y-1">
<span
className="inline-flex shrink-0 rounded px-2 py-0.5 text-[10px] text-[#24282f]"
style={{ backgroundColor: "rgba(94, 147, 239, 0.14)" }}
className="inline-flex shrink-0 rounded px-2 py-0.5 text-[10px] bg-zinc-200 text-gray-600"
>
{row.sessionType}
</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 ? (
<>
<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}
<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>
</div>
</div>
<h3 className="text-sm font-semibold leading-[22px] text-[#24282f]">
<h3 className="text-sm font-semibold leading-[22px]">
{row.trackTitle}
</h3>
{row.trackLocation?.trim() ? (
<div className="flex items-start gap-1.5 text-xs text-[rgba(36,40,47,0.75)]">
<MapPin className="mt-0.5 size-3.5 shrink-0 text-[#1e72ef]" strokeWidth={1.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-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="leading-snug">{row.trackLocation.trim()}</span>
</div>
) : null}
@ -93,7 +93,7 @@ export function AgendaRowTrackV2({ row }: { row: AgendaRowTrackV2Data }) {
e.stopPropagation();
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-pressed={row.bookmark.isBookmarked}
>
@ -111,7 +111,7 @@ export function AgendaRowTrackV2({ row }: { row: AgendaRowTrackV2Data }) {
e.stopPropagation();
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-label={detailsOpen ? "Hide session details" : "Show session details"}
>
@ -123,15 +123,15 @@ export function AgendaRowTrackV2({ row }: { row: AgendaRowTrackV2Data }) {
</div>
</div>
{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() ? (
<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}
<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
</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(", ")}
</p>
</div>

View File

@ -79,20 +79,20 @@ export function AskQuestionModal({
role="dialog"
aria-modal="true"
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="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
</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.
</p>
</div>
<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"
onClick={close}
>
@ -110,13 +110,13 @@ export function AskQuestionModal({
placeholder="Type your question here..."
rows={5}
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}
<button
type="submit"
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"}
</button>

View File

@ -53,7 +53,7 @@ export function AudienceQuestionCard({
const shell =
variant === "embedded"
? "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(
async (direction: "up" | "down") => {
@ -86,7 +86,7 @@ export function AudienceQuestionCard({
type="button"
disabled={!canVote || voting}
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"
? "border-[#34c759] bg-[#34c759]/10 text-[#34c759]"
: "border-[#34c759] bg-white text-[#34c759] hover:bg-[#34c759]/5"
@ -94,7 +94,7 @@ export function AudienceQuestionCard({
aria-label={canVote ? "Upvote" : undefined}
>
<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>
);
@ -103,14 +103,14 @@ export function AudienceQuestionCard({
type="button"
disabled={!canVote || voting}
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"
? "border-[#ff3b30] bg-[#ff3b30]/10 text-[#ff3b30]"
: "border-[#ff3b30] bg-white text-[#ff3b30] hover:bg-[#ff3b30]/5"
} disabled:cursor-not-allowed disabled:opacity-45`}
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} />
</button>
);
@ -118,12 +118,12 @@ export function AudienceQuestionCard({
return (
<div className={shell}>
<div className="flex items-center gap-2">
<div className="flex size-8 items-center justify-center rounded-full bg-[#f2f2f7]">
<User className="size-4 text-[#24282f]/70" strokeWidth={2} />
<div className="flex size-10 items-center justify-center rounded-full bg-[var(--theme-button-color)]/10">
<User className="size-5 text-[var(theme-card-text-color)]/70" strokeWidth={2} />
</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>
<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 justify-end gap-2">
{canVote ? (

View File

@ -23,10 +23,12 @@ export function DynamicFeedbackForm({
eventId,
schema,
variant = "light",
className = '',
}: {
eventId: string;
schema: VisitorPublicFeedbackForm;
variant?: "light" | "gradient";
className?: string | '';
}) {
const [values, setValues] = useState<Record<string, unknown>>({});
const [submitting, setSubmitting] = useState(false);
@ -86,11 +88,11 @@ export function DynamicFeedbackForm({
<div
className={
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 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"
: "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]">
{schema.thankYouMessage?.trim() || "Your response has been recorded."}
</p>
@ -100,13 +102,13 @@ export function DynamicFeedbackForm({
const fieldShell =
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 bg-white p-4 shadow-[0_1px_4px_rgba(12,12,13,0.05)]";
? `rounded-md p-4 pb-2 ${className}`
: `rounded-md p-4 pb-2 ${className}`;
return (
<form onSubmit={onSubmit} className={fieldShell}>
{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}
<div className="flex flex-col gap-4">
{fields.map((f) => (
@ -120,7 +122,7 @@ export function DynamicFeedbackForm({
<button
type="submit"
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"}
</button>
@ -154,12 +156,12 @@ function FieldInput({
const t = (f.type || "text").toLowerCase();
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") {
return (
<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>
</p>
);
@ -170,11 +172,11 @@ function FieldInput({
const selected = Number.isFinite(n) ? n : 0;
return (
<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}
{f.required ? <span className="text-red-600"> *</span> : null}
</legend>
<div className="flex gap-2">
<div className="flex gap-2 mt-1">
{Array.from({ length: 5 }, (_, i) => {
const star = i + 1;
return (
@ -182,11 +184,11 @@ function FieldInput({
key={star}
type="button"
onClick={() => onChange(label, star)}
className="text-[#ef8d1d]"
className="text-[var(--theme-button-color)]"
aria-label={`${star} star${star === 1 ? "" : "s"}`}
>
<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}
/>
</button>
@ -223,13 +225,13 @@ function FieldInput({
if (t === "radio") {
return (
<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}
{f.required ? <span className="text-red-600"> *</span> : null}
</legend>
<div className="flex flex-col gap-2">
{(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)} />
{o}
</label>
@ -247,13 +249,13 @@ function FieldInput({
};
return (
<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}
{f.required ? <span className="text-red-600"> *</span> : null}
</legend>
<div className="flex flex-col gap-2">
{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)} />
{o}
</label>
@ -265,7 +267,7 @@ function FieldInput({
if (t === "checkbox") {
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
type="checkbox"
checked={value === true || value === "yes" || value === "Yes"}
@ -282,7 +284,7 @@ function FieldInput({
if (t === "email" || t === "phone") {
return (
<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}
{f.required ? <span className="text-red-600"> *</span> : null}
</span>
@ -300,7 +302,7 @@ function FieldInput({
if (t === "textarea") {
return (
<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}
{f.required ? <span className="text-red-600"> *</span> : null}
</span>
@ -324,7 +326,7 @@ function FieldInput({
if (t === "text" && longText) {
return (
<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}
{f.required ? <span className="text-red-600"> *</span> : null}
</span>
@ -341,7 +343,7 @@ function FieldInput({
return (
<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}
{f.required ? <span className="text-red-600"> *</span> : null}
</span>

View File

@ -4,13 +4,14 @@ export type EventBannerProps = {
/** Event banner URL from login-preview (`bannerImage` or `image`) */
imageUrl?: string | null;
alt?: string;
className?: string | '';
};
export function EventBanner({ imageUrl, alt = "" }: EventBannerProps) {
export function EventBanner({ imageUrl, alt = "", className = "" }: EventBannerProps) {
const src = imageUrl?.trim();
if (src) {
return (
<div className="w-full shrink-0 overflow-hidden bg-[#e5e5ea]">
<div className={`w-full shrink-0 overflow-hidden bg-[#e5e5ea] ${className}`}>
<img
src={src}
alt={alt}
@ -22,8 +23,8 @@ export function EventBanner({ imageUrl, alt = "" }: EventBannerProps) {
return (
<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"
data-name="Event Logo"
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 ${className}`}
>
<EventBuzzLogo
variant="white"

View File

@ -172,7 +172,7 @@ export function LandingAgendaCards({
return (
<div
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
type="button"
@ -181,34 +181,34 @@ export function LandingAgendaCards({
aria-expanded={isOpen}
>
<div className="flex min-w-0 items-center gap-2">
<Calendar className="size-4 shrink-0 text-[#1e72ef]" strokeWidth={1.75} />
<span className="font-inter text-sm font-semibold text-[#24282f]">{day.dateLabel}</span>
<Calendar className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="text-sm font-semibold">{day.dateLabel}</span>
</div>
<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}
aria-hidden
/>
</button>
{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) => (
<div key={tg.trackId} className="w-full">
{tgIdx > 0 ? (
<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"
aria-hidden
/>
) : null}
<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}
</p>
{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
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}
/>
<span className="font-inter leading-snug">{tg.trackHall}</span>
@ -216,7 +216,7 @@ export function LandingAgendaCards({
) : null}
</div>
<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">
{tg.sessions.map((s) => (
<AgendaRowTrackV2
@ -257,7 +257,7 @@ export function LandingMySessionsList({
return (
<div className="mt-3 w-full max-w-none">
<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">
{sessions.map((s) => (
<AgendaRowTrackV2

View File

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

View File

@ -131,7 +131,7 @@ export function HeaderMobile({
return (
<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"
>
<div className="flex w-full items-center justify-between gap-2">
@ -140,13 +140,13 @@ export function HeaderMobile({
<button
type="button"
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"
>
<ChevronLeft className="size-6" strokeWidth={2} />
</button>
) : 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 ? (
<img
src={logoSrc}
@ -169,17 +169,17 @@ export function HeaderMobile({
<div className="flex shrink-0 items-center justify-end gap-2 sm:gap-4">
<Link
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} />
<span className="min-w-[1.25rem] text-sm font-medium tabular-nums tracking-wide text-[#24282f]">
<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-[var(--theme-button-color)]">
{pointsLabel}
</span>
</Link>
<button
type="button"
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"
>
<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 items: MenuItem[] = [
{ label: "Event Home", to: eventHome, icon: <Home className="size-4 shrink-0 text-[#1e72ef]" strokeWidth={1.75} /> },
{ label: "Rewards", to: rewards, icon: <Gift className="size-4 shrink-0 text-[#1e72ef]" strokeWidth={1.75} /> },
{ label: "Leaderboard", to: leaderboard, icon: <Trophy 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-[var(--theme-button-color)]" strokeWidth={1.75} /> },
{ label: "Leaderboard", to: leaderboard, icon: <Trophy className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} /> },
...(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
? [{ 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",
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-haspopup="menu"
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
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) {
return (
<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>
{showPoweredByFooter ? <PoweredByFooter /> : null}

View File

@ -47,16 +47,16 @@ export function PhotoWallCropModal({
};
return (
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/55 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="border-b border-[#f2f2f7] px-4 py-3">
<p className="font-inter text-base font-semibold text-[#24282f]">Crop your photo</p>
<p className="mt-0.5 font-inter text-xs text-[rgba(36,40,47,0.65)]">
<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 card-bg shadow-xl">
<div className="border-b border-[var(--theme-card-border-color)]/5 px-4 py-3">
<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-[var(--theme-card-text-color)]/80">
Adjust to match the wall photo ratio before uploading.
</p>
</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
image={imageSrc}
crop={crop}
@ -70,7 +70,7 @@ export function PhotoWallCropModal({
<div className="flex flex-col gap-3 px-4 py-4">
<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
type="range"
min={1}
@ -78,7 +78,7 @@ export function PhotoWallCropModal({
step={0.05}
value={zoom}
onChange={(e) => setZoom(Number(e.target.value))}
className="w-full accent-[#ef8d1d]"
className="w-full accent-[var(--theme-button-color)]"
/>
</label>
@ -93,7 +93,7 @@ export function PhotoWallCropModal({
type="button"
onClick={onCancel}
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
</button>
@ -101,7 +101,7 @@ export function PhotoWallCropModal({
type="button"
onClick={() => void confirm()}
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"}
</button>

View File

@ -75,7 +75,7 @@ export function PhotoWallEventHeader({
if (d.showDateRange && dateLine) {
metaItems.push(
<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}
</span>,
);
@ -85,7 +85,7 @@ export function PhotoWallEventHeader({
if (showLoc) {
metaItems.push(
<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}
</span>,
);
@ -105,7 +105,7 @@ export function PhotoWallEventHeader({
return (
<header className="flex w-full flex-col items-center gap-3 text-center">
{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
src={event.logoUrl!}
alt={event.name}
@ -121,22 +121,22 @@ export function PhotoWallEventHeader({
{showTitle || showSubtitle || metaItems.length > 0 ? (
<div className="flex w-full flex-col items-center gap-2">
{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}
{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}
</h1>
) : null}
{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}
{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) => (
<span key={index} className="inline-flex items-center gap-1.5 font-inter">
{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>
) : null}
@ -147,7 +147,7 @@ export function PhotoWallEventHeader({
) : null}
</div>
) : 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}
</header>
);

View File

@ -93,16 +93,16 @@ export function SessionQuestionsCard({
return (
<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"
>
<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">
<h2 className="truncate text-lg font-semibold tracking-wide text-[#24282f]">
<h2 className="truncate text-md font-semibold tracking-wide">
{title}
</h2>
{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}
</span>
) : null}
@ -111,26 +111,26 @@ export function SessionQuestionsCard({
<div className="flex shrink-0 gap-1">
<button
type="button"
className="rounded-full bg-[#f2f2f7] p-1"
className="rounded-full bg-zinc-300 p-1"
aria-label="Previous question"
onClick={goPrev}
>
<ChevronLeft className="size-4 text-[#24282f]" />
<ChevronLeft className="size-4 text-zinc-700" />
</button>
<button
type="button"
className="rounded-full bg-[#f2f2f7] p-1"
className="rounded-full bg-zinc-300 p-1"
aria-label="Next question"
onClick={goNext}
>
<ChevronRight className="size-4 text-[#24282f]" />
<ChevronRight className="size-4 text-zinc-700" />
</button>
</div>
) : null}
</div>
<div className="border-t border-[#f2f2f7]">
<div className="border-t border-[var(--theme-card-text-color)]/5">
{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 ? (
<p className="px-4 py-6 text-center text-sm text-red-600">{error}</p>
) : current ? (
@ -143,19 +143,19 @@ export function SessionQuestionsCard({
/>
</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 className="flex items-center justify-between gap-3 px-4">
{viewAllTo ? (
<Link
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
</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
</span>
)}
@ -163,7 +163,7 @@ export function SessionQuestionsCard({
type="button"
onClick={() => live && setAskModalOpen(true)}
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
</button>

View File

@ -7,7 +7,7 @@ function typeBadgeClass(type: string): string {
const t = type.toLowerCase();
if (t === "poll") return "bg-[#eaddff]";
if (t === "survey") return "bg-[#e5e5ea]";
return "bg-[#ebffee]";
return "bg-green-100";
}
function typeLabel(type: string): string {
@ -44,7 +44,7 @@ export function SessionQuizPollsSection({ eventId, sessionId }: SessionQuizPolls
}).toString()}`;
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
type="button"
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}
>
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-[#24282f]">Quiz &amp; Polls</span>
<span className="rounded bg-[#ebffee] px-2 py-0.5 text-[10px] uppercase text-[#24282f]">
<span className="text-md font-semibold text-[var(--theme-card-text-color)]">Quiz &amp; Polls</span>
<span className="rounded bg-green-100 px-2 py-0.5 text-[10px] uppercase text-gray-800">
{loading ? "…" : `${items.length} Active`}
</span>
</div>
{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>
{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 ? (
<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 ? (
<p className="py-3 text-center text-sm text-red-600">{error}</p>
) : 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 ? (
<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">
{items.map((item) => (
<li
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">
<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()}
</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>
<Link
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)}
</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 {
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 {
margin: 0;
min-height: 100dvh;
font-family: var(--font-sans);
font-family: var(--theme-body-font, var(--font-sans));
color: var(--color-eb-text);
background-color: #f2f2f7;
background: transparent;
-webkit-font-smoothing: antialiased;
}
@ -31,35 +36,42 @@ body {
display: flex;
flex-direction: column;
min-height: 100dvh;
background-color: #f2f2f7;
background: transparent;
}
.eb-gradient-banner {
background: linear-gradient(
34.8deg,
rgb(29, 102, 239) 0%,
rgb(30, 121, 240) 45%,
rgb(189, 53, 230) 100%
var(--theme-primary-color, rgb(29, 102, 239)) 0%,
var(--theme-primary-color, rgb(30, 121, 240)) 45%,
var(--theme-secondary-color, rgb(189, 53, 230)) 100%
);
}
.eb-gradient-fab {
background: linear-gradient(
65deg,
rgb(29, 102, 239) 0%,
rgb(30, 121, 240) 40%,
rgb(189, 53, 230) 100%
var(--theme-primary-color, rgb(29, 102, 239)) 0%,
var(--theme-primary-color, rgb(30, 121, 240)) 40%,
var(--theme-secondary-color, rgb(189, 53, 230)) 100%
);
}
/* Auth / modal shells (Figma EventBuzz_Event_Mobile_V1_Register) */
.eb-gradient-auth {
background: linear-gradient(
background-color: var(--theme-bg-color, transparent);
background-image: var(
--theme-bg-image,
linear-gradient(
77.92deg,
rgb(29, 102, 239) 0%,
rgb(30, 121, 240) 45%,
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 */
@ -116,6 +128,36 @@ body {
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 {
0%,
100% {
@ -127,3 +169,14 @@ body {
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 { getAttendeeToken } from "./attendeeAuth";
import type { LoginPreviewResponse, ResolveCodeResponse, VisitorJoinType } from "../types/visitorContext";
import type {
EventThemeConfig,
LoginPreviewResponse,
ResolveCodeResponse,
VisitorJoinType,
} from "../types/visitorContext";
function getBaseUrl(): string {
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;
}
/** 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 = {
_id: string;
event: string;

View File

@ -45,8 +45,8 @@ import {
function WhiteCard({ children }: { children: ReactNode }) {
return (
<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="rounded-xl bg-white px-6 py-6">{children}</div>
<div className="w-full rounded-xl shadow-md">
<div className="rounded-xl bg-whit card-bg px-6 py-6">{children}</div>
</div>
</div>
);
@ -410,24 +410,24 @@ export function AiAvatarPage() {
</div>
) : null}
<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]">
<Loader2 className="h-8 w-8 animate-spin text-[#D32F2F]" aria-hidden />
<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-[var(--theme-button-color)]" aria-hidden />
</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
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"
>
{GENERATING_PIPELINE_MESSAGES[pipelineMsgIndex]}
</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
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}%` }}
/>
</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>
</WhiteCard>
</AuthShell>
@ -447,12 +447,12 @@ export function AiAvatarPage() {
/>
</div>
) : null}
<h1 className="mb-2 text-center text-[22px] font-semibold text-[#1a1a1a]">Your avatar</h1>
<p className="mb-4 text-center text-sm text-[#666]">
Here’s your AI avatar. You can download or share it.
<h1 className="mb-2 text-center text-[22px] font-semibold">Your avatar</h1>
<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.
</p>
{(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
ref={resultImgRef}
crossOrigin="anonymous"
@ -477,7 +477,7 @@ export function AiAvatarPage() {
<button
type="button"
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
</button>
@ -485,7 +485,7 @@ export function AiAvatarPage() {
<button
type="button"
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 photo
@ -494,14 +494,14 @@ export function AiAvatarPage() {
</div>
{showShare ? (
<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
</p>
{useNativeShare ? (
<button
type="button"
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" />
Share
@ -513,7 +513,7 @@ export function AiAvatarPage() {
key={item.key}
type="button"
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} />
{item.label}
@ -544,22 +544,22 @@ export function AiAvatarPage() {
{meta?.accessMode === "login" && !loggedIn ? (
<div className="space-y-3">
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">AI Avatar</h1>
<p className="text-center text-sm text-[#666]">Sign in to create your AI avatar.</p>
<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-[var(--theme-card-text-color)]/80">Sign in to create your AI avatar.</p>
<button
type="button"
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
</button>
</div>
) : !selectedAvatar ? (
<div className="space-y-4">
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Choose an avatar</h1>
<p className="text-center text-sm text-[#666]">Pick an avatar, then upload your selfie.</p>
<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-[var(--theme-card-text-color)]/80">Pick an avatar, then upload your selfie.</p>
{!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">
{meta.avatars.map((avatar) => (
@ -567,9 +567,9 @@ export function AiAvatarPage() {
key={avatar._id}
type="button"
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 ? (
<img
src={avatar.imageUrl}
@ -577,12 +577,12 @@ export function AiAvatarPage() {
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
</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}
</p>
</button>
@ -592,7 +592,7 @@ export function AiAvatarPage() {
{err ? <p className="text-center text-sm text-red-600">{err}</p> : null}
</div>
) : (
<div className="space-y-4">
<div className="space-y-4 text-center">
<button
type="button"
onClick={() => {
@ -600,13 +600,13 @@ export function AiAvatarPage() {
clearSelectedPhoto();
setErr(null);
}}
className="text-sm font-semibold text-[#D32F2F]"
className="text-sm font-semibold theme-btn-link"
>
← Change avatar
</button>
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Upload selfie</h1>
<p className="text-center text-sm text-[#666]">
Avatar: <span className="font-semibold text-[#333]">{selectedAvatar.name}</span>
<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-[var(--theme-card-text-color)]/80">
Avatar: <span className="font-semibold">{selectedAvatar.name}</span>
</p>
<input
@ -618,7 +618,7 @@ export function AiAvatarPage() {
onChange={(e) => void handleFileChange(e.target.files?.[0] || null)}
/>
{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" />
</div>
) : null}
@ -626,7 +626,7 @@ export function AiAvatarPage() {
type="button"
disabled={picking}
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"}
</button>
@ -634,7 +634,7 @@ export function AiAvatarPage() {
type="button"
disabled={!file || submitting}
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"}
</button>

View File

@ -121,9 +121,9 @@ export function EventLandingPage() {
if (!eventId) {
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<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>
<Link to="/" className="mt-4 text-sm font-semibold text-[#1e72ef] underline">
Home
@ -135,9 +135,9 @@ export function EventLandingPage() {
if (!event) {
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<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>
</main>
</MobileShell>
@ -145,40 +145,40 @@ export function EventLandingPage() {
}
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<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">
<div className="flex w-full max-w-none flex-col gap-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 px-4">
<section className="w-full">
<div className="w-full rounded-lg border border-[rgba(116,112,112,0.2)] bg-white p-4 shadow-sm">
<h1 className="text-lg font-semibold text-[#24282f]">{event.name}</h1>
<div className="w-full rounded-lg card-bg p-4 shadow-md">
<h1 className="text-lg font-semibold">{event.name}</h1>
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs">
<EventRatingBadge ratings={event.ratings} />
{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
</Link>
) : (
<span className="font-inter text-[#8e8e93]">Ratings off</span>
<span className="text-[var(--theme-card-text-color)]/50">Ratings off</span>
)}
</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."}
</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
</button>
<div className="mt-4 flex flex-col gap-2 border-t border-[#f2f2f7] pt-4 text-xs text-[#24282f]">
</button> */}
<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">
<Calendar className="size-4 shrink-0 text-[#8e8e93]" strokeWidth={1.75} />
<span className="font-inter">{schedule || "—"}</span>
<Calendar className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="">{schedule || "—"}</span>
</div>
{event.location?.trim() ? (
<div className="flex items-center gap-2">
<MapPin className="size-4 shrink-0 text-[#8e8e93]" strokeWidth={1.75} />
<span className="font-inter">{event.location.trim()}</span>
<MapPin className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="">{event.location.trim()}</span>
</div>
) : null}
</div>
@ -186,12 +186,12 @@ export function EventLandingPage() {
</section>
{bookmarkedIds.size > 0 ? (
<section className="w-full">
<div className="w-full px-4">
<h2 className="font-inter text-base font-semibold text-[#24282f]">My Sessions</h2>
</div>
<section className="w-full card-bg rounded-lg shadow-md p-4">
<h2 className="text-md font-semibold">My Sessions</h2>
{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
sessions={bookmarkedSessionsSorted}
@ -203,17 +203,17 @@ export function EventLandingPage() {
</section>
) : null}
<section className="w-full">
<div className="w-full px-4">
<h2 className="font-inter text-base font-semibold text-[#24282f]">Agenda</h2>
</div>
<section className="w-full card-bg rounded-lg shadow-md p-4">
<h2 className="text-base font-semibold">Agenda</h2>
{agendaErr ? (
<p className="mt-3 px-4 text-sm text-red-600">{agendaErr}</p>
) : null}
{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 ? (
<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
days={agendaDays}

View File

@ -109,11 +109,11 @@ export function EventTrackPage() {
if (!eventId || !trackId) {
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<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>
<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
</Link>
</main>
@ -123,10 +123,10 @@ export function EventTrackPage() {
if (loading) {
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<HeaderMobile eventId={eventId || null} />
<main className="flex flex-1 items-center justify-center bg-[#f2f2f7] px-4 py-10">
<p className="text-sm text-[#24282f]">Loading track…</p>
<main className="flex flex-1 items-center justify-center px-4 py-10">
<p className="text-sm text-[var(--theme-card-text-color)]">Loading track…</p>
</main>
</MobileShell>
);
@ -134,9 +134,9 @@ export function EventTrackPage() {
if (err || !track) {
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<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>
</main>
</MobileShell>
@ -160,42 +160,44 @@ export function EventTrackPage() {
(ctx?.eventId === eventId && ctx.event?.logoUrl?.trim() ? ctx.event.logoUrl.trim() : undefined);
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<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="mb-6 flex flex-col gap-4">
<div className="inline-flex w-fit rounded bg-[#e5e5ea] px-2 py-0.5">
<span className="text-[10px] tracking-wide text-[#24282f]">{eventName.toUpperCase()}</span>
<div className="mb-6 flex flex-col gap-4 card-bg p-4 rounded-lg">
<div className="inline-flex w-fit rounded bg-zinc-200 px-2 py-0.5">
<span className="text-[10px] tracking-wide">{eventName.toUpperCase()}</span>
</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() ? (
<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}
<p className="text-[10px] uppercase tracking-wide text-[#8e8e93]">{trackTypeLabel}</p>
<div className="flex flex-col gap-1 text-xs text-[#24282f]">
<p className="text-xs uppercase font-semibold tracking-wide text-[var(--theme-card-text-color)]/70">{trackTypeLabel}</p>
<div className="flex flex-col gap-2 text-xs text-[var(--theme-card-text-color)]/80">
<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>
</div>
<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>
</div>
<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>
</div>
</div>
</div>
<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
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">
<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} />
</button>
</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">
{agendaRows.map((row) => (
<AgendaRow key={row.time + row.sessionType} row={row} />
@ -213,9 +216,9 @@ export function EventTrackPage() {
</div>
</div>
<p className="mt-6 text-center text-xs text-[#24282f]/60">
<p className="mt-6 text-center text-sm">
<Link
className="text-[#1e72ef] underline"
className="text-[var(--theme-text-color)] underline"
to={`/questions?event_id=${encodeURIComponent(eventId)}`}
>
Ask &amp; session questions

View File

@ -43,50 +43,50 @@ export function EventTrackV2Page() {
const [tab, setTab] = useState(0);
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<HeaderMobile
logoUrl={ctx?.eventId === eventId ? ctx.event?.logoUrl : undefined}
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="mb-4 flex flex-col gap-4">
<span className="inline-flex w-fit rounded bg-[#e5e5ea] px-2 py-0.5 text-[10px] text-[#24282f]">
<div className="mb-4 flex flex-col gap-4 card-bg p-4 rounded-lg">
<span className="inline-flex w-fit rounded bg-zinc-200 px-2 py-0.5 text-[10px] text-gray-600">
NAPCON JAIPUR 2026
</span>
<h1 className="text-lg font-semibold text-[#24282f]">This will be Track Name</h1>
<div className="flex items-center gap-2 text-xs text-[#24282f]">
<MapPin className="size-4 shrink-0" strokeWidth={1.75} />
<h1 className="text-lg font-semibold">This will be Track Name</h1>
<div className="flex items-center gap-2 text-xs">
<MapPin className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span className="font-inter">Health and Wellness Expo</span>
</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">
<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>
</div>
<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>
</div>
<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>
</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) => (
<button
key={d}
type="button"
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
? "border-b-2 border-[#1e72ef] font-semibold text-[#24282f]"
: "font-normal text-[#24282f]"
? "border-b-2 border-[var(--theme-button-color)] font-semibold"
: "font-normal"
}`}
style={{ marginBottom: -2 }}
>
@ -95,7 +95,7 @@ export function EventTrackV2Page() {
))}
</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">
{rowsV2.map((row) => (
<AgendaRowTrackV2 key={row.time + row.sessionType} row={row} />
@ -103,8 +103,8 @@ export function EventTrackV2Page() {
</div>
</div>
<p className="mt-4 text-center text-xs">
<Link className="text-[#1e72ef] underline" to="/event">
<p className="mt-4 text-center text-sm">
<Link className="text-[var(--theme-text-color)] underline" to="/event">
Back to event
</Link>
</p>

View File

@ -98,14 +98,16 @@ export function FeedbackPage() {
return (
<MobileShell className="relative">
<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]">
<h1 className="font-inter text-xl font-semibold text-[#24282f]">Feedback</h1>
<p className="mt-3 text-sm text-[rgba(36,40,47,0.67)]">Open feedback from your event or session link.</p>
<Link to="/" className="mt-4 inline-block text-sm font-semibold text-[#1e72ef] underline">
<div className="card-bg rounded-lg shadow-md p-4">
<h1 className="font-inter text-xl font-semibold text-[var(--theme-card-text-color)]">Feedback</h1>
<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
</Link>
</div>
</div>
</main>
</MobileShell>
);
@ -115,14 +117,16 @@ export function FeedbackPage() {
return (
<MobileShell className="relative">
<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]">
<h1 className="font-inter text-xl font-semibold text-[#24282f]">Feedback</h1>
<p className="mt-3 text-sm text-[rgba(36,40,47,0.67)]">Feedback is disabled for this event.</p>
<Link to="/event" className="mt-4 inline-block text-sm font-semibold text-[#1e72ef] underline">
<div className="card-bg rounded-lg shadow-md p-4">
<h1 className="text-xl font-semibold text-[var(--theme-card-text-color)]">Feedback</h1>
<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
</Link>
</div>
</div>
</main>
</MobileShell>
);
@ -131,27 +135,29 @@ export function FeedbackPage() {
return (
<MobileShell className="relative">
<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]">
<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 ? (
<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 ? (
<div className="mt-4">
<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.
</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
</Link>
</div>
) : schema ? (
<div className="mt-6">
<DynamicFeedbackForm eventId={eventId} schema={schema} variant="light" />
<div className="mt-2">
<DynamicFeedbackForm eventId={eventId} schema={schema} variant="light" className="px-0" />
</div>
) : null}
</div>
</div>
</main>
</MobileShell>
);

View File

@ -9,25 +9,22 @@ export function Home() {
return (
<AuthShell>
<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="h-1.5 w-full bg-gradient-to-r from-[#34A7F2] via-[#1D66EF] to-[#ef8d1d]" aria-hidden />
<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 /> */}
<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 size-12 items-center justify-center rounded-full"
style={{
background: "linear-gradient(135deg, rgba(52,167,242,0.12) 0%, rgba(29,102,239,0.16) 100%)",
}}
className="flex size-12 items-center justify-center rounded-full bg-[var(--theme-button-color)]/10"
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 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
</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
invite, on the event screen, or on a poster at the venue.
</p>
@ -35,7 +32,7 @@ export function Home() {
</div>
<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
id="join-event-code"
type="text"
@ -46,7 +43,7 @@ export function Home() {
autoComplete="off"
autoCapitalize="characters"
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>
@ -59,7 +56,7 @@ export function Home() {
<button
type="submit"
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"}
</button>

View File

@ -77,19 +77,42 @@ export function LeaderboardPage() {
return (
<MobileShell className="relative">
<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">
<Award className="size-[68px] text-amber-300" strokeWidth={1.25} />
<h1 className="mt-1 font-inter text-xl font-semibold text-white">Leaderboard</h1>
<div className="eb-gradient-auth flex flex-1 flex-col items-center px-6 pb-10 pt-2">
<div className="mt-6 w-full max-w-[354px] rounded-xl card-bg shadow-md">
<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="p-4">
<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 gap-2 justify-between w-full p-4">
<div className="flex gap-2 justify-between w-full">
<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>
<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)]">
{!signedIn ? "—" : loading ? "…" : yourRank != null ? yourRank : "—"}
</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
? "Sign in"
: loading
@ -99,14 +122,14 @@ export function LeaderboardPage() {
: "0 points"}
</span>
</div>
</div>
<h2 className="mt-6 font-inter text-base font-semibold text-[#24282f]">Top participants</h2>
</div> */}
<h2 className="mt-6 font-inter text-base font-semibold">Leaderboard</h2>
{err ? (
<p className="mt-3 text-xs text-red-600">{err}</p>
) : 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 ? (
<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.
</p>
) : (
@ -116,12 +139,12 @@ export function LeaderboardPage() {
return (
<li
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
</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}
</li>
);
@ -133,7 +156,7 @@ export function LeaderboardPage() {
<Link
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
</Link>

View File

@ -240,9 +240,9 @@ export function LoginPage() {
<AuthShell>
<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="rounded-xl bg-white">
<div className="rounded-xl card-bg">
<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 ? (
<img
src={preview.logoUrl}
@ -256,13 +256,13 @@ export function LoginPage() {
</div>
<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">
<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">
<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}
</p>
{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}
</div>
{previewError ? (
@ -276,11 +276,11 @@ export function LoginPage() {
</p>
) : null}
{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 ? (
<>
<Calendar className="size-4 shrink-0" strokeWidth={1.75} />
<span className="font-inter">{schedule}</span>
<span className="">{schedule}</span>
</>
) : null}
{schedule && preview?.location?.trim() ? (
@ -289,7 +289,7 @@ export function LoginPage() {
{preview?.location?.trim() ? (
<>
<MapPin className="size-4 shrink-0" strokeWidth={1.75} />
<span className="font-inter">{preview.location.trim()}</span>
<span className="">{preview.location.trim()}</span>
</>
) : null}
</div>
@ -298,25 +298,25 @@ export function LoginPage() {
{showInviteField ? (
<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
value={inviteInput}
onChange={(e) => setInviteInput(e.target.value)}
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>
) : null}
{showEmail ? (
<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
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
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>
) : null}
@ -335,7 +335,7 @@ export function LoginPage() {
<button
type="submit"
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}
</button>

View File

@ -44,7 +44,7 @@ function WhiteCard({ children, wide }: { children: ReactNode; wide?: boolean })
return (
<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="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>
);
@ -432,18 +432,18 @@ export function PhotoSearchPage() {
</div>
) : null}
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Find Photos</h1>
<p className="mb-5 text-center text-sm text-[#666]">
<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-[var(--theme-card-text-color)]/80">
Upload a selfie to find photos of you from {meta?.eventName || "the event"}.
</p>
{meta?.accessMode === "login" && !loggedIn ? (
<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
type="button"
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
</button>
@ -460,14 +460,14 @@ export function PhotoSearchPage() {
onChange={(e) => onPick(e.target.files?.[0] || null)}
/>
{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" />
</div>
) : null}
<button
type="button"
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"}
</button>
@ -477,7 +477,7 @@ export function PhotoSearchPage() {
type="button"
disabled={!file || searching}
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" />
{searching ? "Searching…" : "Find photos"}

View File

@ -59,9 +59,9 @@ export function PhotoWallPresentPage() {
return (
<div className="eb-gradient-auth min-h-dvh text-white">
<div className="px-4 pb-3 pt-4 text-center">
<p className="text-xs uppercase tracking-[0.2em] text-white/70">Photo Wall</p>
<h1 className="font-inter text-xl font-semibold sm:text-2xl">{data.eventName}</h1>
<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-[var(--theme-card-text-color)]/70">Photo Wall</p>
<h1 className="text-xl font-semibold sm:text-2xl text-[var(--theme-card-text-color)]">{data.eventName}</h1>
</div>
{data.photos.length === 0 ? (
@ -98,7 +98,7 @@ export function PhotoWallPresentPage() {
/>
{showNames && p.displayName ? (
<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}
>
{p.displayName}

View File

@ -51,7 +51,7 @@ function WhiteCard({ children }: { children: ReactNode }) {
return (
<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="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>
);
@ -371,7 +371,7 @@ export function PhotoWallUploadPage() {
return (
<AuthShell showLogo={false}>
<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>
</AuthShell>
);
@ -398,10 +398,10 @@ export function PhotoWallUploadPage() {
<PhotoWallEventHeader event={meta.event} display={meta.eventHeader || null} />
</div>
) : 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"}
</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
? "Thanks! Your photo is waiting for approval."
: done.framed
@ -435,7 +435,7 @@ export function PhotoWallUploadPage() {
<button
type="button"
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" />
Edit / Upload new photo
@ -453,7 +453,7 @@ export function PhotoWallUploadPage() {
</div>
{showShare ? (
<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
</p>
{useNativeShare ? (
@ -496,18 +496,18 @@ export function PhotoWallUploadPage() {
<PhotoWallEventHeader event={meta.event} display={meta.eventHeader || null} />
</div>
) : null}
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Upload your photo</h1>
<p className="mb-5 text-center text-sm text-[#666]">
<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-[var(--theme-card-text-color)]/80">
{meta?.form?.description || "Add a selfie to the event photo wall."}
</p>
{meta?.accessMode === "login" && !loggedIn ? (
<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
type="button"
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
</button>
@ -539,20 +539,20 @@ export function PhotoWallUploadPage() {
)}
</button>
{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}
</div>
{meta?.askUploaderName ? (
<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>
</span>
<input
type="text"
value={uploaderName}
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"
required
/>
@ -566,7 +566,7 @@ export function PhotoWallUploadPage() {
if (f.type === "textarea") {
return (
<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}
{required ? <span className="text-red-500"> *</span> : null}
</span>
@ -575,14 +575,14 @@ export function PhotoWallUploadPage() {
onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))}
required={required}
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>
);
}
return (
<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}
{required ? <span className="text-red-500"> *</span> : null}
</span>
@ -591,7 +591,7 @@ export function PhotoWallUploadPage() {
value={String(values[key] ?? "")}
onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))}
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>
);
@ -602,7 +602,7 @@ export function PhotoWallUploadPage() {
<button
type="submit"
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"}
</button>

View File

@ -103,15 +103,18 @@ export function Questions() {
if (!eventId) {
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<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]">
Add <span className="font-mono">event_id</span> to the URL to open questions.
</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
</Link>
</div>
</main>
</MobileShell>
);
@ -119,21 +122,24 @@ export function Questions() {
if (!sessionId) {
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<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]">
<h1 className="text-lg font-semibold text-[#24282f]">{ev?.name ?? "Event"}</h1>
<p className="mt-3 text-sm leading-relaxed text-[rgba(36,40,47,0.75)]">
<div className="card-bg p-4 rounded-lg">
<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.
</p>
<Link
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
</Link>
</div>
</div>
</main>
<HyperNavigationFAB eventId={eventId} sessionId={sessionId || undefined} />
</MobileShell>
@ -141,31 +147,34 @@ export function Questions() {
}
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<HeaderMobile logoUrl={headerLogoUrl} eventId={eventId || null} />
<main className="relative z-0 flex flex-1 flex-col bg-[#f2f2f7] pb-28 pt-6">
<div className="mx-auto w-full max-w-[370px] px-4">
<div className="mb-1 inline-flex w-fit rounded bg-[#e5e5ea] px-2 py-0.5">
<span className="text-[10px] tracking-wide text-[#24282f]">Audience questions</span>
<main className="relative z-0 flex flex-1 flex-col pb-28 pt-6">
<div className="mx-auto w-full max-w-[370px] px-3">
<div className="flex flex-col gap-2 card-bg p-4 rounded-lg">
<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>
<h1 className="text-lg font-semibold text-[#24282f]">
<h1 className="text-lg font-semibold">
{sessionTitle ?? "Session questions"}
</h1>
{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}
<button
type="button"
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
</span>
<ChevronRight className="size-4 text-white" strokeWidth={2.5} />
</button>
</div>
<div className="mt-6 flex flex-col gap-3">
{listLoading ? (
@ -186,8 +195,8 @@ export function Questions() {
</div>
{sessionHref ? (
<p className="mt-6 text-center text-xs">
<Link className="text-[#1e72ef] underline" to={sessionHref}>
<p className="mt-6 text-center text-md">
<Link className="text-[var(--theme-text-color)] underline" to={sessionHref}>
Back to session
</Link>
</p>

View File

@ -636,11 +636,13 @@ export function QuizPollTakePage() {
return (
<MobileShell className="relative">
<HeaderMobile eventId={null} />
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10">
<p className="text-center text-sm text-[#24282f]">Invalid quiz link.</p>
<Link to="/" className="mt-4 text-sm font-semibold text-[#1e72ef] underline">
<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">Invalid quiz link.</p>
<Link to="/" className="mt-4 text-sm font-semibold text-[var(--theme-button-color)] underline">
Home
</Link>
</div>
</main>
</MobileShell>
);
@ -650,8 +652,10 @@ export function QuizPollTakePage() {
return (
<MobileShell className="relative">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} />
<main className="flex flex-1 items-center justify-center bg-[#f2f2f7] px-4 py-10">
<p className="text-sm text-[#24282f]">Loading…</p>
<main className="flex flex-1 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-sm">Loading…</p>
</div>
</main>
</MobileShell>
);
@ -661,11 +665,13 @@ export function QuizPollTakePage() {
return (
<MobileShell className="relative">
<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>
<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
</Link>
</div>
</main>
</MobileShell>
);
@ -676,8 +682,10 @@ export function QuizPollTakePage() {
return (
<MobileShell className="relative">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} />
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10">
<p className="text-center text-sm text-[#24282f]">Returning to session…</p>
<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-[var(--theme-card-text-color)]/80">Returning to session…</p>
</div>
</main>
</MobileShell>
);
@ -687,15 +695,17 @@ export function QuizPollTakePage() {
return (
<MobileShell className="relative">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} />
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10">
<p className="font-inter text-lg font-semibold text-[#24282f]">Thank you</p>
<p className="mt-2 text-center text-sm text-[#24282f]">Your response was submitted.</p>
<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="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
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
</Link>
</div>
</main>
</MobileShell>
);
@ -712,20 +722,20 @@ export function QuizPollTakePage() {
<div className="flex shrink-0 flex-col">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} />
</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="rounded-lg border border-[rgba(116,112,112,0.2)] bg-white p-5 text-center shadow-sm">
<p className="font-inter text-lg font-semibold text-[#24282f]">Thank you</p>
<p className="mt-1 text-sm text-[#24282f]">Your response was submitted.</p>
<div className="rounded-lg card-bg p-5 text-center shadow-md">
<p className="font-inter text-lg font-semibold text-[var(--theme-card-text-color)]]">Thank you</p>
<p className="mt-1 text-sm text-[var(--theme-card-text-color)]/80">Your response was submitted.</p>
{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}
</div>
{showRes && isQuiz ? (
<div className="mt-6">
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#1e72ef]">Results</p>
<p className="mt-0.5 text-xs text-[rgba(36,40,47,0.55)]">Questions with correct answers</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-[var(--theme-card-text-color)]/80">Questions with correct answers</p>
<ul className="mt-4 flex flex-col gap-4">
{quiz.questions.map((q, qi) => {
const raw = answers[q._id];
@ -734,13 +744,13 @@ export function QuizPollTakePage() {
return (
<li
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]">
<span className="text-[#8e8e93]">{qi + 1}. </span>
<p className="text-sm font-semibold text-[var(--theme-card-text-color)]">
<span className="text-[var(--theme-card-text-color)]">{qi + 1}. </span>
{q.question}
</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
</p>
<p
@ -749,7 +759,7 @@ export function QuizPollTakePage() {
? "text-green-700"
: graded === false
? "text-red-600"
: "text-[#24282f]"
: "text-[var(--theme-card-text-color)]"
}`}
>
{formatUserAnswerLine(q, raw)}
@ -757,17 +767,17 @@ export function QuizPollTakePage() {
</p>
{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
</p>
<p className="text-sm font-semibold text-green-800">{corr}</p>
</>
) : 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.
</p>
) : 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.
</p>
) : null}
@ -780,16 +790,16 @@ export function QuizPollTakePage() {
{showRes && isPollOrSurvey ? (
<div className="mt-6">
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#1e72ef]">Results</p>
<p className="mt-0.5 text-xs text-[rgba(36,40,47,0.55)]">Share of responses per option</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-[var(--theme-card-text-color)]">Share of responses per option</p>
{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}
{pollResultsErr ? (
<p className="mt-4 text-center text-sm text-red-600">{pollResultsErr}</p>
) : null}
{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
{pollResults.totalSubmissions === 1 ? "" : "s"}
</p>
@ -799,11 +809,11 @@ export function QuizPollTakePage() {
{pollResults.questions.map((pq) => (
<li
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" ? (
<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
? "No text answers yet."
: `${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">
{(pq.options || []).map((opt) => (
<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="shrink-0 tabular-nums font-semibold text-[#1e72ef]">
<span className="shrink-0 tabular-nums font-semibold text-[var(--theme-button-color)]">
{opt.percent}%
</span>
</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
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))}%` }}
/>
</div>
@ -837,7 +847,7 @@ export function QuizPollTakePage() {
<Link
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
</Link>
@ -853,8 +863,10 @@ export function QuizPollTakePage() {
<div className="flex shrink-0 flex-col">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} />
</div>
<main className="flex flex-1 items-center justify-center bg-[#f2f2f7] px-4 py-10">
<p className="text-sm text-[#24282f]">Loading…</p>
<main className="flex flex-1 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-sm">Loading…</p>
</div>
</main>
</MobileShell>
);
@ -868,14 +880,16 @@ export function QuizPollTakePage() {
<div className="flex shrink-0 flex-col">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} />
</div>
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] 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">
<p className="font-inter text-lg font-semibold text-[#24282f]">Answer submitted</p>
<p className="mt-2 text-sm text-[rgba(36,40,47,0.75)]">
<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 text-center shadow-sm">
<div className="card-bg rounded-lg p-4">
<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.
</p>
</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
</Link>
</main>
@ -889,14 +903,14 @@ export function QuizPollTakePage() {
<div className="flex shrink-0 flex-col">
<HeaderMobile logoUrl={headerLogo} eventId={eventId} />
</div>
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] 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">
<p className="font-inter text-lg font-semibold text-[#24282f]">{quiz.title}</p>
<p className="mt-2 text-sm text-[rgba(36,40,47,0.75)]">
<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-5 text-center shadow-sm">
<p className="font-inter text-lg font-semibold">{quiz.title}</p>
<p className="mt-2 text-sm text-[var(--theme-card-text-color)]/80">
The host will start the next question soon.
</p>
</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
</Link>
</main>
@ -906,7 +920,7 @@ export function QuizPollTakePage() {
const footerBtnClass =
"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 (
<MobileShell className="relative h-dvh min-h-0 flex-col">
@ -915,33 +929,36 @@ export function QuizPollTakePage() {
</div>
<main
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">
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#1e72ef]">{typeHeading(quiz.type)}</p>
<h1 className="mt-1 font-inter text-xl font-semibold text-[#24282f]">{quiz.title}</h1>
<div className="flex flex-col gap-1 card-bg p-4 rounded-lg shadow-sm">
<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 ? (
<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}
</p>
) : 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}
</p>
) : null}
</div>
{timerActive && secondsLeft != null ? (
<div
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"}
</span>
<span
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)}
@ -958,9 +975,9 @@ export function QuizPollTakePage() {
).map((q, offset) => {
const qi = isQuizMasterMode ? liveQuestionIndex : multiQuestion ? stepSafe : offset;
return (
<li key={q._id} className="rounded-lg border border-[rgba(116,112,112,0.2)] bg-white p-4 shadow-sm">
<p className="text-sm font-semibold text-[#24282f]">
<span className="text-[#8e8e93]">{qi + 1}. </span>
<li key={q._id} className="rounded-lg card-bg p-4 shadow-sm">
<p className="text-sm font-semibold text-[var(--theme-card-text-color)]">
<span className="text-[var(--theme-card-text-color)]">{qi + 1}. </span>
{q.question}
</p>
<QuestionMedia url={q.imageUrl} alt="" />
@ -972,7 +989,7 @@ export function QuizPollTakePage() {
<label
key={opt._id}
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
@ -994,7 +1011,7 @@ export function QuizPollTakePage() {
value={typeof answers[q._id] === "string" ? answers[q._id] : ""}
onChange={(e) => setText(q, e.target.value)}
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"
/>
) : q.type === "rating" ? (
@ -1006,7 +1023,7 @@ export function QuizPollTakePage() {
onClick={() => setSingle(q, String(n))}
className={`size-10 rounded-full border text-sm font-semibold ${
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]"
}`}
>
@ -1020,7 +1037,7 @@ export function QuizPollTakePage() {
<label
key={opt._id}
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
@ -1044,20 +1061,20 @@ export function QuizPollTakePage() {
</ul>
{!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 ? (
<>
Use <span className="font-semibold text-[#24282f]">Next</span> or{" "}
<span className="font-semibold text-[#24282f]">Submit</span> below after{" "}
<Link className="font-semibold text-[#1e72ef] underline" to={visitorEventUrl(eventId)}>
Use <span className="font-semibold">Next</span> or{" "}
<span className="font-semibold">Submit</span> below after{" "}
<Link className="font-semibold theme-btn-link" to={visitorEventUrl(eventId)}>
signing in
</Link>
.
</>
) : (
<>
Tap <span className="font-semibold text-[#24282f]">Submit</span> below after{" "}
<Link className="font-semibold text-[#1e72ef] underline" to={visitorEventUrl(eventId)}>
Tap <span className="font-semibold">Submit</span> below after{" "}
<Link className="font-semibold theme-btn-link" to={visitorEventUrl(eventId)}>
signing in
</Link>
.
@ -1068,8 +1085,8 @@ export function QuizPollTakePage() {
{submitError ? <p className="mt-4 text-center text-sm text-red-600">{submitError}</p> : null}
<p className="mt-6 text-center text-xs">
<Link className="text-[#1e72ef] underline" to={sessionBackHref}>
<p className="mt-6 text-center text-md">
<Link className="text-[var(--theme-text-color)] underline" to={sessionBackHref}>
Back to session
</Link>
</p>
@ -1077,7 +1094,7 @@ export function QuizPollTakePage() {
</main>
<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 }}
>
<div className="mx-auto flex w-full max-w-[370px] gap-2">
@ -1096,7 +1113,7 @@ export function QuizPollTakePage() {
type="button"
disabled={submitting}
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}
</button>
@ -1104,7 +1121,7 @@ export function QuizPollTakePage() {
<button
type="button"
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
</button>
@ -1115,7 +1132,7 @@ export function QuizPollTakePage() {
type="button"
disabled={submitting}
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}
</button>

View File

@ -98,14 +98,16 @@ export function RatingsPage() {
return (
<MobileShell className="relative">
<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]">
<h1 className="font-inter text-xl font-semibold text-[#24282f]">Ratings</h1>
<p className="mt-3 text-sm text-[rgba(36,40,47,0.67)]">Open ratings from your event or session link.</p>
<Link to="/" className="mt-4 inline-block text-sm font-semibold text-[#1e72ef] underline">
<div className="card-bg rounded-lg shadow-md p-4">
<h1 className="text-xl font-semibold text-[var(--theme-card-text-color)]">Ratings</h1>
<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
</Link>
</div>
</div>
</main>
</MobileShell>
);
@ -115,14 +117,16 @@ export function RatingsPage() {
return (
<MobileShell className="relative">
<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]">
<h1 className="font-inter text-xl font-semibold text-[#24282f]">Ratings</h1>
<p className="mt-3 text-sm text-[rgba(36,40,47,0.67)]">Ratings are disabled for this event.</p>
<Link to="/event" className="mt-4 inline-block text-sm font-semibold text-[#1e72ef] underline">
<div className="card-bg rounded-lg shadow-md p-4">
<h1 className="text-xl font-semibold text-[var(--theme-card-text-color)]">Ratings</h1>
<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
</Link>
</div>
</div>
</main>
</MobileShell>
);
@ -131,27 +135,29 @@ export function RatingsPage() {
return (
<MobileShell className="relative">
<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]">
<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 ? (
<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 ? (
<div className="mt-4">
<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.
</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
</Link>
</div>
) : schema ? (
<div className="mt-6">
<DynamicFeedbackForm eventId={eventId} schema={schema} variant="light" />
<div className="mt-2">
<DynamicFeedbackForm eventId={eventId} schema={schema} variant="light" className="px-0" />
</div>
) : null}
</div>
</div>
</main>
</MobileShell>
);

View File

@ -70,31 +70,56 @@ export function RewardsPage() {
return (
<MobileShell className="relative">
<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} />
<h1 className="mt-1 font-inter text-xl font-semibold text-white">Your Rewards</h1>
<Link
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
</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="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)]">
<p className="font-inter text-base font-semibold uppercase tracking-wide">Total Points</p>
<p className="font-inter text-5xl font-bold tabular-nums drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)]">
{displayTotal}
</p>
<div className="mt-6 w-full max-w-[354px] rounded-xl bg-whit card-bg shadow-md">
<div className="flex gap-2 justify-between w-full p-4">
<div className="flex items-center gap-2 shrink-0">
<div className="bg-[var(--theme-button-color)]/12 rounded-full p-3">
<Trophy className="size-10 text-[var(--theme-button-color)]" strokeWidth={1.25} />
</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">
{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 && (
<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 &&
historyItems.map((row) => {
@ -108,13 +133,13 @@ export function RewardsPage() {
return (
<li
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}`}>
{signed}
{row.points} pts
</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)}
</span>
</li>

View File

@ -363,13 +363,13 @@ export function SingleEventPage() {
if (!eventId || !sessionId) {
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<HeaderMobile
eventName={headerEventNameForEvent(eventId, null, ctx)}
eventId={eventId || null}
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>
<Link to="/" className="mt-4 text-sm font-semibold text-[#1e72ef] underline">
Home
@ -381,14 +381,14 @@ export function SingleEventPage() {
if (loading) {
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<HeaderMobile
logoUrl={headerLogoForEvent(eventId, null, ctx)}
eventName={headerEventNameForEvent(eventId, null, ctx)}
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 session…</p>
</main>
</MobileShell>
@ -397,14 +397,14 @@ export function SingleEventPage() {
if (err || !session) {
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<HeaderMobile
logoUrl={headerLogoForEvent(eventId, eventInfo, ctx)}
eventName={headerEventNameForEvent(eventId, eventInfo, ctx)}
eventId={eventId || null}
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>
</main>
</MobileShell>
@ -451,7 +451,7 @@ export function SingleEventPage() {
Boolean(sessionNotes || sessionLocation || trackHall || hasCapacity);
return (
<MobileShell className="relative">
<MobileShell className="relative bg-transparent">
<HeaderMobile
logoUrl={headerLogoUrl}
eventName={headerEventName}
@ -469,7 +469,7 @@ export function SingleEventPage() {
</div>
) : 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">
{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">
@ -480,37 +480,40 @@ export function SingleEventPage() {
<button
type="button"
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-pressed={sessionBookmarked}
>
<Bookmark className={`size-6 ${sessionBookmarked ? "fill-current" : ""}`} strokeWidth={1.75} />
</button>
</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">
<span className="rounded bg-white/20 px-2 py-0.5">{typeLabel}</span>
<div className="flex max-w-full flex-wrap items-center gap-x-2 gap-y-1 pe-2 text-xs">
<span className="rounded bg-[var(--theme-button-color)]/20 px-2 py-0.5">{typeLabel}</span>
{timeLine ? <span>{timeLine}</span> : null}
{trackTitle !== typeLabel ? <span className="opacity-90">{trackTitle}</span> : null}
</div>
<h1 className="font-inter text-2xl font-bold 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">
<h1 className="text-xl font-semibold tracking-tight">{session.title}</h1>
<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 ? (
<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})
</span>
) : startT ? (
<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}
</span>
) : null}
{sessionDate ? (
<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}
</span>
) : null}
</div>
<div className="flex items-center justify-end gap-1.5">
{hasExpandableSessionMeta ? (
<button
type="button"
@ -519,7 +522,7 @@ export function SingleEventPage() {
aria-label={
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
className={`size-5 transition-transform ${sessionMetaOpen ? "rotate-180" : ""}`}
@ -530,27 +533,29 @@ export function SingleEventPage() {
) : null}
</div>
</div>
{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 ? (
<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>
</div>
) : null}
{sessionLocation || trackHall ? (
<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">
{sessionLocation ? (
<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>
</li>
) : null}
{trackHall ? (
<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>
</li>
) : null}
@ -558,13 +563,13 @@ export function SingleEventPage() {
</div>
) : null}
{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}
</div>
) : null}
</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">
{session && session.enableQnA !== false ? (
<SessionQuestionsCard
@ -578,33 +583,33 @@ export function SingleEventPage() {
<SessionQuizPollsSection eventId={eventId} sessionId={sessionId} />
{showSessionFeedback || showSessionRatings ? (
<div className="flex w-full max-w-none flex-col gap-2">
<p className="px-1 text-[10px] font-semibold uppercase tracking-wide text-[#8e8e93]">
<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-md font-semibold uppercas tracking-wide">
Feedback & ratings
</p>
<div className="flex flex-col gap-2">
{showSessionFeedback ? (
<Link
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">
<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>
<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>
) : null}
{showSessionRatings ? (
<Link
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">
<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>
<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>
) : null}
</div>

View File

@ -44,8 +44,8 @@ import {
function WhiteCard({ children }: { children: ReactNode }) {
return (
<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="rounded-xl bg-white px-6 py-6">{children}</div>
<div className="w-full rounded-xl shadow-md">
<div className="rounded-xl card-bg px-6 py-6">{children}</div>
</div>
</div>
);
@ -387,12 +387,12 @@ export function SocialSharePage() {
/>
</div>
) : null}
<h1 className="mb-2 text-center text-[22px] font-semibold text-[#1a1a1a]">Your photo</h1>
<p className="mb-4 text-center text-sm text-[#666]">
Here’s your framed photo. You can download or share it.
<h1 className="mb-2 text-center text-[22px] font-semibold">Your photo</h1>
<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.
</p>
{(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
ref={resultImgRef}
crossOrigin="anonymous"
@ -417,7 +417,7 @@ export function SocialSharePage() {
<button
type="button"
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
</button>
@ -425,7 +425,7 @@ export function SocialSharePage() {
<button
type="button"
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 photo
@ -434,14 +434,14 @@ export function SocialSharePage() {
</div>
{showShare ? (
<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
</p>
{useNativeShare ? (
<button
type="button"
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" />
Share
@ -453,7 +453,7 @@ export function SocialSharePage() {
key={item.key}
type="button"
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} />
{item.label}
@ -484,22 +484,22 @@ export function SocialSharePage() {
{meta?.accessMode === "login" && !loggedIn ? (
<div className="space-y-3">
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Social Share</h1>
<p className="text-center text-sm text-[#666]">Sign in to create and share your photo.</p>
<h1 className="mb-1 text-center text-[22px] font-semibold">Social Share</h1>
<p className="text-center text-sm text-[var(--theme-card-text-color)]/80">Sign in to create and share your photo.</p>
<button
type="button"
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
</button>
</div>
) : !selectedFrame ? (
<div className="space-y-4">
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Choose a frame</h1>
<p className="text-center text-sm text-[#666]">Pick a frame, then upload your photo.</p>
<h1 className="mb-1 text-center text-[22px] font-semibold">Choose a frame</h1>
<p className="text-center text-sm text-[var(--theme-card-text-color)]/80">Pick a frame, then upload your photo.</p>
{!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">
{meta.frames.map((frame) => (
@ -507,9 +507,9 @@ export function SocialSharePage() {
key={frame._id}
type="button"
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 ? (
<img
src={frame.frameImageUrl}
@ -517,12 +517,12 @@ export function SocialSharePage() {
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
</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}
</p>
</button>
@ -532,7 +532,7 @@ export function SocialSharePage() {
{err ? <p className="text-center text-sm text-red-600">{err}</p> : null}
</div>
) : (
<div className="space-y-4">
<div className="space-y-4 text-center">
<button
type="button"
onClick={() => {
@ -540,13 +540,13 @@ export function SocialSharePage() {
clearSelectedPhoto();
setErr(null);
}}
className="text-sm font-semibold text-[#D32F2F]"
className="text-sm font-semibold theme-btn-link"
>
← Change frame
</button>
<h1 className="mb-1 text-center text-[22px] font-semibold text-[#1a1a1a]">Upload photo</h1>
<p className="text-center text-sm text-[#666]">
Frame: <span className="font-semibold text-[#333]">{selectedFrame.name}</span>
<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-[var(--theme-card-text-color)]/80">
Frame: <span className="font-semibold">{selectedFrame.name}</span>
</p>
<input
@ -558,7 +558,7 @@ export function SocialSharePage() {
onChange={(e) => void handleFileChange(e.target.files?.[0] || null)}
/>
{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" />
</div>
) : null}
@ -566,7 +566,7 @@ export function SocialSharePage() {
type="button"
disabled={picking}
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"}
</button>
@ -574,7 +574,7 @@ export function SocialSharePage() {
type="button"
disabled={!file || submitting}
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"}
</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;
}
/** 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. */
export interface LoginPreviewResponse {
_id: string;