Compare commits
2 Commits
aea5bea7cb
...
418f85b402
| Author | SHA1 | Date |
|---|---|---|
|
|
418f85b402 | |
|
|
2c5ccff63a |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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") => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
@ -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% {
|
||||
|
|
|
|||
125
src/lib/api.ts
125
src/lib/api.ts
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</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}
|
||||
<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">
|
||||
|
||||
<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="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>
|
||||
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue