Compare commits

...

2 Commits

Author SHA1 Message Date
Amey Bhagwat 418f85b402 Merge branch 'main' of http://192.168.1.200:3000/Stratawiz/EventBuzz_Website
* 'main' of http://192.168.1.200:3000/Stratawiz/EventBuzz_Website:
  Add attendee photo search, social share, and AI avatar flows.
  Changes

# Conflicts:
#	src/types/visitorContext.ts
2026-08-14 13:18:32 +05:30
Amey Bhagwat 2c5ccff63a UI changes and theme setup 2026-08-14 13:16:19 +05:30
24 changed files with 592 additions and 146 deletions

View File

@ -26,8 +26,12 @@ 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";
export default function App() {
// Re-apply theme from stored visitor context on refresh / navigation.
useEventTheme();
return (
<div className="flex min-h-dvh flex-1 flex-col">
<Routes>

View File

@ -35,8 +35,8 @@ 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 bg-[var(--theme-button-color)] 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-[var(--theme-button-color)]/40"
: "inline-flex h-8 w-[84px] items-center justify-between rounded-full bg-[var(--theme-button-color)] 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-[0_4px_4px_rgba(0,0,0,0.15)]";
return (
<div className="relative w-full pb-4">

View File

@ -79,7 +79,7 @@ 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 bg-whit card-bg 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"
>
<div className="mb-4 flex items-start justify-between gap-3">
<div className="min-w-0">
@ -92,7 +92,7 @@ export function AskQuestionModal({
</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-[#24282f] transition-colors duration-300 ease-in-out hover:bg-[#e4e4e4]"
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-[#c7c7cc] bg-[#f2f2f7]/40 px-3 py-3 text-sm text-[#24282f] placeholder:text-[rgba(36,40,47,0.45)] 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 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-xl bg-[#ef8d1 bg-[var(--theme-button-color)] py-3 text-sm font-semibold text-white shadow-[0_4px_4px_rgba(0,0,0,0.12)] transition-all duration-300 ease-in-out hover:bg-[#e0800c] transition hover:bg-[var(--theme-button-color)]/90 active:scale-[0.99] 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-md border border-[#f2f2f7] card-bg p-4 shadow-[0_1px_4px_rgba(12,12,13,0.05)]";
const doVote = useCallback(
async (direction: "up" | "down") => {

View File

@ -101,7 +101,7 @@ 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 bg-whit card-bg p-4 shadow-[0_1px_4px_rgba(12,12,13,0.05)]";
return (
<form onSubmit={onSubmit} className={fieldShell}>
@ -120,7 +120,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-full bg-[var(--theme-button-color)] py-3 text-xs 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,7 +154,7 @@ 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 (
@ -182,11 +182,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>

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-x-0 border-[rgba(116,112,112,0.2)] bg-whit card-bg shadow-sm sm:border-x"
>
<button
type="button"
@ -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 bg-[#fafafa p-4 shadow-[0_1px_4px_rgba(12,12,13,0.05)] sm:rounded-md">
<div className="flex w-full flex-col gap-4">
{tg.sessions.map((s) => (
<AgendaRowTrackV2

View File

@ -140,7 +140,7 @@ 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-[#f2f2f7] text-[#777777] transition-colors duration-300 ease-in-out hover:bg-[#e4e4e4]"
aria-label="Back"
>
<ChevronLeft className="size-6" strokeWidth={2} />
@ -169,9 +169,9 @@ 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} />
<Trophy className="size-5 shrink-0 text-[var(--theme-button-color)] sm:size-6" strokeWidth={1.75} />
<span className="min-w-[1.25rem] text-sm font-medium tabular-nums tracking-wide text-[#24282f]">
{pointsLabel}
</span>
@ -179,7 +179,7 @@ export function HeaderMobile({
<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

@ -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

@ -93,7 +93,7 @@ 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 border border-[rgba(116,112,112,0.2)] bg-whit card-bg py-4 shadow-sm"
data-name="Session Questions"
>
<div className="flex h-8 items-center justify-between gap-2 px-4">
@ -150,7 +150,7 @@ export function SessionQuestionsCard({
{viewAllTo ? (
<Link
to={viewAllTo}
className="font-inter text-xs font-semibold uppercase tracking-wider text-[#1e72ef]"
className="font-inter text-xs font-semibold uppercase tracking-wider text-[#1e72e theme-btn-link"
>
View All
</Link>
@ -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-[#ef8d1 bg-[var(--theme-button-color)] px-3 py-2 text-xs font-semibold text-white shadow-[0_2px_4px_rgba(0,0,0,0.1)] transition-all duration-300 ease-in-out hover:bg-[#e0800c] transition hover:bg-[var(--theme-button-color)]/90 disabled:opacity-50"
>
Ask your Question
</button>

View File

@ -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 border border-[rgba(116,112,112,0.2)] bg-whit card-bg shadow-sm">
<button
type="button"
className="flex w-full items-center justify-between gap-2 px-4 py-3 text-left"
@ -58,9 +58,9 @@ export function SessionQuizPollsSection({ eventId, sessionId }: SessionQuizPolls
</span>
</div>
{open ? (
<ChevronUp className="size-4 shrink-0 text-[#1d66ef]" strokeWidth={2} />
<ChevronUp className="size-4 shrink-0 text-[#24282f]" strokeWidth={2} />
) : (
<ChevronDown className="size-4 shrink-0 text-[#1d66ef]" strokeWidth={2} />
<ChevronDown className="size-4 shrink-0 text-[#24282f]" strokeWidth={2} />
)}
</button>
@ -91,7 +91,7 @@ export function SessionQuizPollsSection({ eventId, sessionId }: SessionQuizPolls
</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 bg-[#0b4dd theme-btn-bg px-3 py-1.5 text-center text-xs font-semibold text-white transition hover:bg-[#0a43b8]"
>
{actionLabel(item.type)}
</Link>

View File

@ -0,0 +1,22 @@
import { useEffect } from "react";
import { applyEventTheme, clearEventTheme } from "../theme/eventTheme";
import type { EventThemeConfig } from "../types/visitorContext";
import { readStoredVisitorContext } from "../types/visitorContext";
/**
* Applies event theme CSS variables to `<html>`.
* When `theme` is omitted, uses theme from stored visitor context (if any).
*/
export function useEventTheme(theme?: EventThemeConfig | null) {
useEffect(() => {
const resolved = theme ?? readStoredVisitorContext()?.event?.theme ?? null;
if (resolved) {
applyEventTheme(resolved);
}
return () => {
// Keep vars while navigating between themed event pages;
// only clear when an explicit null theme is passed.
if (theme === null) clearEventTheme();
};
}, [theme]);
}

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(
77.92deg,
rgb(29, 102, 239) 0%,
rgb(30, 121, 240) 45%,
rgb(189, 53, 230) 100%
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% {

View File

@ -1,7 +1,102 @@
import type { AttendeeParticipant } from "./attendeeAuth";
import { getAttendeeToken } from "./attendeeAuth";
import { applyEventTheme } from "../theme/eventTheme";
import type { LoginPreviewResponse, ResolveCodeResponse, VisitorJoinType } from "../types/visitorContext";
/** Temporary test payload for getVisitorLoginPreview — remove when wiring live API again. */
const LOGIN_PREVIEW_SAMPLE: LoginPreviewResponse = {
_id: "69ce3c91be56b4a3b09b2333",
organization: "69b58b13aff81b3887b0026c",
name: "COPD Conference",
description: "COPD Conference in various locations joined using Zoom",
type: "conference",
status: "upcoming",
startDate: "2026-06-25T12:00:00.000Z",
endDate: "2026-09-04T12:00:00.000Z",
location:
"Stratawiz Technologies Pvt Ltd, First Floor, D-Building, Anant Shilp Society, DSK Raanwaara Rd, Patil Nagar, Bavdhan, Pune, Maharashtra 411021",
locationType: "virtual",
tags: [],
image:
"https://did94hhhcgq1d.cloudfront.net/eventbuzz/69b58b13aff81b3887b0026c/69ce3c91be56b4a3b09b2333/event/logo/event-logo-1775123603963.png",
bannerImage:
"https://did94hhhcgq1d.cloudfront.net/eventbuzz/69b58b13aff81b3887b0026c/69ce3c91be56b4a3b09b2333/event/banner/event-banner-1782452674436.png",
metadata: {
totalRevenue: 0,
rating: 3.9,
ratingCount: 8,
registrationCustomFields: [],
},
category: undefined,
virtualLink: "https://zoom.us",
subtitle: undefined,
settings: {
registrationOpen: true,
requiresApproval: false,
registrationMode: "Open",
verifyEmail: false,
registrationDeadline: "2026-09-12T12:00:00.000Z",
enableFeedback: true,
enableRatings: true,
authenticationType: "none",
registrationBaseFields: "name_email",
},
logoUrl:
"https://did94hhhcgq1d.cloudfront.net/eventbuzz/69b58b13aff81b3887b0026c/69ce3c91be56b4a3b09b2333/event/logo/event-logo-1775123603963.png",
ratings: {
average: 3.9,
count: 8,
},
// theme: {
// "theme": "simple",
// "color": "#151937",
// "font": "Roboto",
// "primaryColor": "#303F9F",
// "secondaryColor": "#4CAF50",
// "headerColor": "",
// "buttonColor": "#2c357a",
// "textColor": "#ffffff",
// "textHighlightColor": "",
// "cardColor": "rgba(255,255,255,0.65)",
// "cardTextColor": "#ffffff"
// },
// theme: {
// theme: "gradient",
// color: "#bd35e6",
// // "gradient": {
// // "color1": "#1e72ef",
// // "color2": "#ef8d1d"
// // },
// font: "Open Sans",
// primaryColor: "#1e72ef",
// secondaryColor: "#bd35e6",
// headerColor: "",
// buttonColor: "#1e72ef",
// textColor: "#ffffff",
// textHighlightColor: "",
// cardColor: "rgba(255, 255, 255, 0.9)",
// cardTextColor: "#333333",
// },
theme: {
theme: "background",
color: "#1e72ef",
background: {
url: "https://did94hhhcgq1d.cloudfront.net/rapport/backgrounds/eventbuzz-bg2.jpg",
size: "cover",
repeat: "no",
},
font: "Poppins",
primaryColor: "#1e72ef",
secondaryColor: "#bd35e6",
headerColor: "",
buttonColor: "#1e72ef",
textColor: "#ffffff",
textHighlightColor: "",
cardColor: "rgba(255, 255, 255, 0.9)",
cardTextColor: "#333333",
},
};
function getBaseUrl(): string {
const raw = import.meta.env.VITE_API_BASE_URL;
if (typeof raw !== "string" || !raw.trim()) return "";
@ -55,17 +150,25 @@ export async function postVisitorResolveCode(code: string): Promise<ResolveCodeR
}
export async function getVisitorLoginPreview(eventId: string): Promise<LoginPreviewResponse> {
const base = getBaseUrl();
const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/login-preview`;
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");
}
return (body as { data: LoginPreviewResponse }).data;
// TODO: restore live API call below once theme testing is done
void eventId;
const data = LOGIN_PREVIEW_SAMPLE;
if (data.theme) applyEventTheme(data.theme);
return data;
// const base = getBaseUrl();
// const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/login-preview`;
// 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: LoginPreviewResponse }).data;
// if (data?.theme) applyEventTheme(data.theme);
// return data;
}
export type VisitorEventTrackListItem = {

View File

@ -149,15 +149,15 @@ export function EventLandingPage() {
<HeaderMobile logoUrl={event.logoUrl} eventId={eventId || null} showBack={false} />
<EventBanner imageUrl={bannerSrc} alt={event.name} />
<main className="relative z-0 flex w-full max-w-none flex-1 flex-col bg-[#f2f2f7] pb-8 pt-6">
<main className="relative z-0 flex w-full max-w-none flex-1 flex-col bg-[#f2f2f pb-8 pt-6">
<div className="flex w-full max-w-none flex-col gap-6">
<section className="w-full">
<div className="w-full rounded-lg border border-[rgba(116,112,112,0.2)] bg-white p-4 shadow-sm">
<div className="w-full rounded-lg border border-[rgba(116,112,112,0.2)] bg-whit card-bg p-4 shadow-sm">
<h1 className="text-lg font-semibold text-[#24282f]">{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-inter font-semibold uppercase tracking-wide text-[#1e72e theme-btn-link">
rate now
</Link>
) : (
@ -167,7 +167,7 @@ export function EventLandingPage() {
<p className="mt-3 text-base leading-5 text-[rgba(36,40,47,0.87)]">
{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 font-inter 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]">
@ -188,7 +188,7 @@ export function EventLandingPage() {
{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>
<h2 className="font-inter text-base font-semibold text-[var(--theme-text-color)]">My Sessions</h2>
</div>
{agendaLoading && !agendaSessions.length ? (
<p className="mt-3 px-4 text-sm text-[rgba(36,40,47,0.55)]">Loading…</p>
@ -205,7 +205,7 @@ export function EventLandingPage() {
<section className="w-full">
<div className="w-full px-4">
<h2 className="font-inter text-base font-semibold text-[#24282f]">Agenda</h2>
<h2 className="font-inter text-base font-semibold text-[var(--theme-text-color)]">Agenda</h2>
</div>
{agendaErr ? (
<p className="mt-3 px-4 text-sm text-red-600">{agendaErr}</p>

View File

@ -131,9 +131,9 @@ 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 bg-[#f2f2f7 bg-transparent 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>
<h1 className="font-inter text-xl font-semibold text-[var(--theme-text-color)]">{schema?.name || "Feedback"}</h1>
{schemaLoading ? (
<p className="mt-4 text-sm text-[rgba(36,40,47,0.67)]">Loading form…</p>
) : schemaErr ? (

View File

@ -81,7 +81,7 @@ export function LeaderboardPage() {
<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="mt-6 w-full max-w-[354px] rounded-xl bg-white shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]">
<div className="mt-6 w-full max-w-[354px] rounded-xl bg-whit card-bg shadow-[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)]">
<p className="text-center font-inter text-base font-semibold uppercase tracking-wide">Your rank</p>
@ -89,7 +89,7 @@ export function LeaderboardPage() {
<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
@ -133,7 +133,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 bg-whit 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-[#f2f2f 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="font-inte text-base font-semibold text-[#24282f]">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="font-inte text-[28px] font-bold leading-tight text-[#24282f] sm:text-[32px]">
{headline}
</p>
{preview?.subtitle ? (
<p className="font-inter text-sm font-medium text-[#24282f]/80">{preview.subtitle}</p>
<p className="font-inte text-sm font-medium text-[#24282f]/80">{preview.subtitle}</p>
) : null}
</div>
{previewError ? (
@ -280,7 +280,7 @@ export function LoginPage() {
{schedule ? (
<>
<Calendar className="size-4 shrink-0" strokeWidth={1.75} />
<span className="font-inter">{schedule}</span>
<span className="font-inte">{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="font-inte">{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="font-inte text-sm font-semibold text-[#24282f]">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 font-inter text-sm text-[#24282f] 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="font-inte text-base font-semibold text-[#24282f]">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-[rgba(116,112,112,0.3)] bg-[rgba(157,157,172,0.07)] px-4 font-inte text-sm tracking-wide text-[#24282f] placeholder:text-[rgba(36,40,47,0.6)] 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}
@ -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-full border border-[rgba(214,200,200,0.6)] bg-[var(--theme-button-color)] py-3 text-xs 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"
>
{loading ? "Please wait…" : reg.primaryCtaLabel}
</button>

View File

@ -144,28 +144,31 @@ export function Questions() {
<MobileShell className="relative">
<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 bg-[#f2f2f card-b 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-md">
<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>
</div>
<h1 className="text-lg font-semibold text-[#2428">
{sessionTitle ?? "Session questions"}
</h1>
{ev?.name ? (
<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-[var(--theme-button-color)] 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">
Ask a question
</span>
<ChevronRight className="size-4 text-white" strokeWidth={2.5} />
</button>
</div>
<h1 className="text-lg font-semibold text-[#24282f]">
{sessionTitle ?? "Session questions"}
</h1>
{ev?.name ? (
<p className="mt-1 text-xs text-[rgba(36,40,47,0.65)]">{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]"
>
<span className="text-xs font-semibold uppercase tracking-wide text-white">
Ask a question
</span>
<ChevronRight className="size-4 text-white" strokeWidth={2.5} />
</button>
<div className="mt-6 flex flex-col gap-3">
{listLoading ? (
@ -186,8 +189,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

@ -712,9 +712,9 @@ 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 bg-[#f2f2f 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">
<div className="rounded-lg border border-[rgba(116,112,112,0.2)] bg-whit card-bg 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>
{showRes && isQuiz && typeof scoreSubmitted === "number" ? (
@ -906,7 +906,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-[#e5e5ea] bg-white/40 text-[#24282f] hover:bg-white/40`;
return (
<MobileShell className="relative h-dvh min-h-0 flex-col">
@ -915,20 +915,23 @@ 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 bg-[#f2f2f7 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>
{isQuizMasterMode && liveQuestion ? (
<p className="mt-2 text-xs font-medium tabular-nums text-[rgba(36,40,47,0.55)]">
Question {liveQuestionIndex + 1} of {totalQuestions}
</p>
) : multiQuestion ? (
<p className="mt-2 text-xs font-medium tabular-nums text-[rgba(36,40,47,0.55)]">
Question {stepSafe + 1} of {totalQuestions}
</p>
) : null}
<div className="flex flex-col gap-1 card-bg p-4 rounded-lg border border-[rgba(116,112,112,0.2)] 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 text-[#24282f]">{quiz.title}</h1>
{isQuizMasterMode && liveQuestion ? (
<p className="mt-2 text-xs font-medium tabular-nums text-[rgba(36,40,47,0.55)]">
Question {liveQuestionIndex + 1} of {totalQuestions}
</p>
) : multiQuestion ? (
<p className="mt-2 text-xs font-medium tabular-nums text-[rgba(36,40,47,0.55)]">
Question {stepSafe + 1} of {totalQuestions}
</p>
) : null}
</div>
{timerActive && secondsLeft != null ? (
<div
@ -958,7 +961,7 @@ 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">
<li key={q._id} className="rounded-lg border border-[rgba(116,112,112,0.2)] bg-whit card-bg p-4 shadow-sm">
<p className="text-sm font-semibold text-[#24282f]">
<span className="text-[#8e8e93]">{qi + 1}. </span>
{q.question}
@ -972,7 +975,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
@ -1020,7 +1023,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
@ -1068,8 +1071,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 +1080,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 bg-whit card-bg shadow-[0_-4px_12px_rgba(0,0,0,0.06)]"
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 +1099,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 +1107,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 +1118,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

@ -131,9 +131,9 @@ 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 bg-[#f2f2f7 bg-transparent 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>
<h1 className="font-inter text-xl font-semibold text-[var(--theme-text-color)]">{schema?.name || "Ratings"}</h1>
{schemaLoading ? (
<p className="mt-4 text-sm text-[rgba(36,40,47,0.67)]">Loading form…</p>
) : schemaErr ? (

View File

@ -71,14 +71,16 @@ export function RewardsPage() {
<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">
<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)]"
>
Leaderboard
</Link>
<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-[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">

View File

@ -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="eb-gradient-banne card-bg relative flex flex-col items-cente justify-center rounded-b-lg gap-1.5 px-6 pb-4 pt-3 text-cente text-whit 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,20 +480,20 @@ 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)] hover:bg-white/15"
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 justify-cente gap-x-2 gap-y-1 pe-2 text-[10px]">
<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">
<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-[var(--theme-card-text-color)]/70">
{duration ? (
<span className="flex items-center gap-1.5">
<Clock className="size-4 shrink-0" strokeWidth={1.75} />
@ -519,7 +519,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 text-[var(--theme-card-text-color)] hover:bg-[var(--theme-button-color)]/15"
>
<ChevronDown
className={`size-5 transition-transform ${sessionMetaOpen ? "rotate-180" : ""}`}
@ -531,20 +531,20 @@ export function SingleEventPage() {
</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-color)]/35 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-[10px] 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-[10px] 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}
@ -564,7 +564,7 @@ export function SingleEventPage() {
) : 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 bg-transparent px-4 pb-28 pt-4">
<div className="flex w-full max-w-none flex-col gap-4">
{session && session.enableQnA !== false ? (
<SessionQuestionsCard
@ -586,7 +586,7 @@ export function SingleEventPage() {
{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-[rgba(116,112,112,0.2)] bg-whit card-bg px-3 py-3 text-sm font-medium text-[#24282f] shadow-[0_1px_4px_rgba(12,12,13,0.05)] active:bg-[#f2f2f7]"
>
<span className="flex min-w-0 items-center gap-2.5">
<MessageCircle className="size-[18px] shrink-0 text-[#1e72ef]" strokeWidth={1.75} />
@ -598,7 +598,7 @@ export function SingleEventPage() {
{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-[rgba(116,112,112,0.2)] bg-whit card-bg px-3 py-3 text-sm font-medium text-[#24282f] shadow-[0_1px_4px_rgba(12,12,13,0.05)] active:bg-[#f2f2f7]"
>
<span className="flex min-w-0 items-center gap-2.5">
<Star className="size-[18px] shrink-0 text-[#1e72ef]" strokeWidth={1.75} />

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 login-preview `theme`. */
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 login-preview → 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;
@ -52,6 +88,8 @@ export interface LoginPreviewResponse {
logoUrl: string;
/** Star ratings for this event only (from feedback/survey submissions). */
ratings?: { average: number; count: number };
/** Dynamic page theme → CSS variables on `<html>`. */
theme?: EventThemeConfig | null;
}
export interface EbVisitorContext {