From b16b5828a434d422c1e7c8ca8c5a2a11840cbb67 Mon Sep 17 00:00:00 2001 From: Amey Bhagwat Date: Mon, 17 Aug 2026 19:19:08 +0530 Subject: [PATCH] Updated changes in the layout and all activity pages. --- src/components/HeaderMobile.tsx | 2 +- src/components/PublicEventChrome.tsx | 150 ++++++++++++++++ src/components/SharePlatformIcon.tsx | 65 +++++++ src/components/ThemeGate.tsx | 5 +- src/hooks/useEventTheme.ts | 160 ++++++++++++++--- src/pages/AiAvatarPage.tsx | 154 +++++++---------- src/pages/EventLandingPage.tsx | 98 +++++++---- src/pages/LeaderboardPage.tsx | 36 ++-- src/pages/LoginPage.tsx | 37 ++-- src/pages/PhotoSearchPage.tsx | 250 +++++++++++++-------------- src/pages/PhotoWallUploadPage.tsx | 13 +- src/pages/QuizPollTakePage.tsx | 18 +- src/pages/RewardsPage.tsx | 11 +- src/pages/SocialSharePage.tsx | 172 +++++++++--------- src/theme/visitorThemeCache.ts | 54 ++++++ 15 files changed, 816 insertions(+), 409 deletions(-) create mode 100644 src/components/PublicEventChrome.tsx create mode 100644 src/components/SharePlatformIcon.tsx create mode 100644 src/theme/visitorThemeCache.ts diff --git a/src/components/HeaderMobile.tsx b/src/components/HeaderMobile.tsx index 6dc0e37..d70e475 100644 --- a/src/components/HeaderMobile.tsx +++ b/src/components/HeaderMobile.tsx @@ -131,7 +131,7 @@ export function HeaderMobile({ return (
diff --git a/src/components/PublicEventChrome.tsx b/src/components/PublicEventChrome.tsx new file mode 100644 index 0000000..2efa51d --- /dev/null +++ b/src/components/PublicEventChrome.tsx @@ -0,0 +1,150 @@ +import { useState } from "react"; +import { Calendar, ChevronDown, MapPin } from "lucide-react"; +import type { PhotoWallEventHeaderDisplay, PhotoWallPublicEvent } from "../lib/api"; +import { formatEventScheduleRange } from "../lib/sessionDisplay"; +import { EventBanner } from "./EventBanner"; +import { HeaderMobile } from "./HeaderMobile"; + +const EVENT_TYPE_LABEL: Record = { + conference: "Conference", + workshop: "Workshop", + summit: "Summit", + cme: "CME", + webinar: "Webinar", +}; + +function resolveEventHeader(display?: PhotoWallEventHeaderDisplay | null) { + return { + showLogo: display?.showLogo !== false, + showBanner: display?.showBanner === true, + showTitle: display?.showTitle !== false, + showDateRange: display?.showDateRange !== false, + showEventType: display?.showEventType !== false, + showLocationType: display?.showLocationType !== false, + showVenue: display?.showVenue !== false, + }; +} + +function formatEventType(t?: string): string { + if (!t) return ""; + return EVENT_TYPE_LABEL[t] || t.replace(/^\w/, (c) => c.toUpperCase()); +} + +function formatLocationKind(locationType?: string): string { + if (locationType === "virtual") return "Virtual"; + if (locationType === "hybrid") return "Hybrid"; + if (locationType === "physical") return "In person"; + return locationType?.trim() || ""; +} + +export function PublicEventChrome({ + event, + eventHeader, + eventNameFallback = "", + eventId = "", +}: { + event?: PhotoWallPublicEvent | null; + eventHeader?: PhotoWallEventHeaderDisplay | null; + eventNameFallback?: string; + eventId?: string; +}) { + const [eventMetaOpen, setEventMetaOpen] = useState(false); + const flags = resolveEventHeader(eventHeader); + const eventName = event?.name?.trim() || eventNameFallback || "Event"; + const bannerSrc = event?.imageUrl?.trim() || ""; + const headerLogoUrl = flags.showLogo ? event?.logoUrl?.trim() || null : null; + const schedule = event?.startDate + ? formatEventScheduleRange( + typeof event.startDate === "string" ? event.startDate : String(event.startDate), + event.endDate + ? typeof event.endDate === "string" + ? event.endDate + : String(event.endDate) + : String(event.startDate), + ) + : ""; + const eventSubtitle = event?.subtitle?.trim() || ""; + const eventLocation = event?.location?.trim() || ""; + const eventTypeLabel = formatEventType(event?.eventType); + const locationKind = formatLocationKind(event?.locationType); + const showDate = flags.showDateRange && Boolean(schedule); + const showType = flags.showEventType && Boolean(eventTypeLabel); + const showLocationKind = flags.showLocationType && Boolean(locationKind); + const showVenue = flags.showVenue && Boolean(eventLocation); + const hasCollapsibleEventMeta = Boolean(eventSubtitle || showLocationKind || showVenue); + const showEventInfoCard = flags.showTitle || showDate || showType || hasCollapsibleEventMeta; + + return ( + <> + {flags.showLogo ? ( + + ) : null} + {flags.showBanner ? ( + + ) : null} + {showEventInfoCard ? ( +
+
+
+ {showType ? ( +
+ {eventTypeLabel} +
+ ) : null} + {flags.showTitle ?

{eventName}

: null} + {showDate ? ( +
+ + {schedule} +
+ ) : null} +
+ {hasCollapsibleEventMeta ? ( + + ) : null} +
+ {eventMetaOpen && hasCollapsibleEventMeta ? ( +
+ {eventSubtitle ? ( +

{eventSubtitle}

+ ) : null} +
+ {showLocationKind ? ( +
+ {locationKind} +
+ ) : null} + {showVenue ? ( +
+ + {eventLocation} +
+ ) : null} +
+
+ ) : null} +
+ ) : null} + + ); +} diff --git a/src/components/SharePlatformIcon.tsx b/src/components/SharePlatformIcon.tsx new file mode 100644 index 0000000..c9131b3 --- /dev/null +++ b/src/components/SharePlatformIcon.tsx @@ -0,0 +1,65 @@ +import type { ReactNode } from "react"; +import { Facebook, Instagram, Linkedin, Share2 } from "lucide-react"; +import type { PhotoWallSharePlatforms } from "../lib/api"; + +type SharePlatformIconProps = { + platform: keyof PhotoWallSharePlatforms; + className?: string; +}; + +function BrandSvg({ + className = "h-4 w-4", + children, + label, +}: { + className?: string; + children: ReactNode; + label: string; +}) { + return ( + + {children} + + ); +} + +function XIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +function WhatsAppIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +function SnapchatIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +/** Platform mark for share buttons. Lucide covers LinkedIn/Facebook/Instagram; X, WhatsApp, Snapchat are custom SVGs. */ +export function SharePlatformIcon({ platform, className = "size-5" }: SharePlatformIconProps) { + if (platform === "linkedin") return ; + if (platform === "facebook") return ; + if (platform === "instagram") return ; + if (platform === "x") return ; + if (platform === "whatsapp") return ; + if (platform === "snapchat") return ; + return ; +} diff --git a/src/components/ThemeGate.tsx b/src/components/ThemeGate.tsx index 599fd1e..b478dcc 100644 --- a/src/components/ThemeGate.tsx +++ b/src/components/ThemeGate.tsx @@ -33,6 +33,9 @@ export function ThemeGate({ ready, children }: ThemeGateProps) { return; } + // Cache hit / already themed: overlay was never shown, so skip the hold+fade. + if (!overlayVisible) return; + const elapsed = Date.now() - shownAtRef.current; const holdMs = Math.max(0, MIN_VISIBLE_MS - elapsed); @@ -48,7 +51,7 @@ export function ThemeGate({ ready, children }: ThemeGateProps) { if (holdTimerRef.current) clearTimeout(holdTimerRef.current); if (fadeTimerRef.current) clearTimeout(fadeTimerRef.current); }; - }, [ready]); + }, [ready, overlayVisible]); return ( <> diff --git a/src/hooks/useEventTheme.ts b/src/hooks/useEventTheme.ts index 8ac69ce..cbf9ba2 100644 --- a/src/hooks/useEventTheme.ts +++ b/src/hooks/useEventTheme.ts @@ -1,10 +1,30 @@ -import { useEffect, useState } from "react"; -import { useSearchParams } from "react-router-dom"; -import { getPublicVisitorTheme } from "../lib/api"; +import { useEffect, useMemo, useState } from "react"; +import { useLocation, useSearchParams } from "react-router-dom"; +import { + getAiAvatarMeta, + getPhotoSearchMeta, + getPhotoWallPresent, + getPhotoWallUploadMeta, + getPublicVisitorTheme, + getSocialShareMeta, +} from "../lib/api"; import { applyEventTheme } from "../theme/eventTheme"; +import { readCachedVisitorTheme, writeCachedVisitorTheme } from "../theme/visitorThemeCache"; import { readStoredVisitorContext } from "../types/visitorContext"; -function resolveEventIdFromSearch(searchParams: URLSearchParams): string { +type PublicTokenSource = + | "ai-avatar" + | "photo-search" + | "social-share" + | "photo-wall" + | "photo-wall-present"; + +type ThemeLookup = + | { kind: "none" } + | { kind: "eventId"; eventId: string } + | { kind: "token"; source: PublicTokenSource; token: string }; + +function eventIdFromQuery(searchParams: URLSearchParams): string { const fromEventId = searchParams.get("event_id")?.trim() || ""; if (fromEventId) return fromEventId; const fromEventIdCamel = searchParams.get("eventId")?.trim() || ""; @@ -12,11 +32,48 @@ function resolveEventIdFromSearch(searchParams: URLSearchParams): string { const type = searchParams.get("type"); const id = searchParams.get("id")?.trim() || ""; if (type === "event" && id) return id; - return readStoredVisitorContext()?.eventId || ""; + return ""; +} + +function resolveThemeLookup(pathname: string, searchParams: URLSearchParams): ThemeLookup { + const fromQuery = eventIdFromQuery(searchParams); + if (fromQuery) return { kind: "eventId", eventId: fromQuery }; + + const tokenRoutes: Array<{ re: RegExp; source: PublicTokenSource }> = [ + { re: /^\/ai-avatar\/([^/]+)/, source: "ai-avatar" }, + { re: /^\/photo-search\/([^/]+)/, source: "photo-search" }, + { re: /^\/social-share\/([^/]+)/, source: "social-share" }, + { re: /^\/photo-wall-present\/([^/]+)/, source: "photo-wall-present" }, + { re: /^\/photo-wall\/([^/]+)/, source: "photo-wall" }, + ]; + for (const { re, source } of tokenRoutes) { + const match = pathname.match(re); + const token = match?.[1] ? decodeURIComponent(match[1]).trim() : ""; + if (token) return { kind: "token", source, token }; + } + + const stored = readStoredVisitorContext()?.eventId?.trim() || ""; + if (stored) return { kind: "eventId", eventId: stored }; + return { kind: "none" }; +} + +async function eventIdFromPublicToken(source: PublicTokenSource, token: string): Promise { + if (source === "ai-avatar") return (await getAiAvatarMeta(token)).eventId; + if (source === "photo-search") return (await getPhotoSearchMeta(token)).eventId; + if (source === "social-share") return (await getSocialShareMeta(token)).eventId; + if (source === "photo-wall") return (await getPhotoWallUploadMeta(token)).eventId; + return (await getPhotoWallPresent(token)).eventId; +} + +function applyCachedTheme(eventId: string): boolean { + const cached = readCachedVisitorTheme(eventId); + if (!cached) return false; + applyEventTheme(cached.theme); + return true; } export type UseEventThemeResult = { - /** Event id used for the public theme request (empty when none). */ + /** Event id used for the public theme request (empty when none / still resolving). */ eventId: string; /** * True when either there is no event to theme, or the theme request has @@ -26,39 +83,96 @@ export type UseEventThemeResult = { }; /** - * Fetches GET /api/public/visitor-theme/:eventId (from URL / stored context) - * and applies CSS variables to ``. + * Loads GET /api/public/visitor-theme/:eventId once per event and caches it. + * In-app navigation for the same event reuses the cache (no loader / no refetch). + * Activity token routes resolve event id first, then use the same cache. */ export function useEventTheme(): UseEventThemeResult { const [searchParams] = useSearchParams(); - const eventId = resolveEventIdFromSearch(searchParams); - const [ready, setReady] = useState(!eventId); + const { pathname } = useLocation(); + const lookup = useMemo( + () => resolveThemeLookup(pathname, searchParams), + [pathname, searchParams], + ); + const kind = lookup.kind; + const lookupEventId = lookup.kind === "eventId" ? lookup.eventId : ""; + const tokenSource = lookup.kind === "token" ? lookup.source : ""; + const token = lookup.kind === "token" ? lookup.token : ""; + + const [ready, setReady] = useState(() => { + if (kind === "none") return true; + if (kind === "eventId") return applyCachedTheme(lookupEventId); + const cached = readCachedVisitorTheme(); + if (!cached) return false; + applyEventTheme(cached.theme); + return true; + }); + const [resolvedEventId, setResolvedEventId] = useState(lookupEventId); useEffect(() => { - if (!eventId) { + if (kind === "none") { + setResolvedEventId(""); setReady(true); return; } let cancelled = false; - setReady(false); - getPublicVisitorTheme(eventId) - .then((data) => { + if (kind === "eventId") { + setResolvedEventId(lookupEventId); + if (applyCachedTheme(lookupEventId)) { + setReady(true); + return; + } + setReady(false); + void (async () => { + try { + const data = await getPublicVisitorTheme(lookupEventId); + if (cancelled) return; + writeCachedVisitorTheme(data.eventId || lookupEventId, data.theme); + applyEventTheme(data.theme); + } catch { + /* keep defaults / previous theme */ + } finally { + if (!cancelled) setReady(true); + } + })(); + return () => { + cancelled = true; + }; + } + + // Token activity route: resolve event id, then cache or fetch theme. + // Do not flip ready off until we know this event is not cached. + void (async () => { + try { + const eventId = await eventIdFromPublicToken(tokenSource as PublicTokenSource, token); if (cancelled) return; + setResolvedEventId(eventId || ""); + if (!eventId) { + setReady(true); + return; + } + if (applyCachedTheme(eventId)) { + setReady(true); + return; + } + setReady(false); + const data = await getPublicVisitorTheme(eventId); + if (cancelled) return; + writeCachedVisitorTheme(data.eventId || eventId, data.theme); applyEventTheme(data.theme); - setReady(true); - }) - .catch(() => { - if (cancelled) return; - // Still unblock UI; keep defaults / any previously applied theme. - setReady(true); - }); + } catch { + /* keep defaults / previous theme */ + } finally { + if (!cancelled) setReady(true); + } + })(); return () => { cancelled = true; }; - }, [eventId]); + }, [kind, lookupEventId, tokenSource, token]); - return { eventId, ready }; + return { eventId: resolvedEventId, ready }; } diff --git a/src/pages/AiAvatarPage.tsx b/src/pages/AiAvatarPage.tsx index 1c5492c..75db857 100644 --- a/src/pages/AiAvatarPage.tsx +++ b/src/pages/AiAvatarPage.tsx @@ -8,16 +8,15 @@ import { } from "react"; import { Download, - Facebook, - Instagram, - Linkedin, Loader2, Share2, } from "lucide-react"; import { useParams } from "react-router-dom"; -import { AuthShell } from "../components/auth/AuthShell"; +import { HeaderMobile } from "../components/HeaderMobile"; +import { MobileShell } from "../components/MobileShell"; import { PhotoWallCropModal } from "../components/PhotoWallCropModal"; -import { PhotoWallEventHeader } from "../components/PhotoWallEventHeader"; +import { PublicEventChrome } from "../components/PublicEventChrome"; +import { SharePlatformIcon } from "../components/SharePlatformIcon"; import { getAiAvatarMeta, getVisitorLoginPreview, @@ -42,13 +41,29 @@ import { type EbVisitorContext, } from "../types/visitorContext"; -function WhiteCard({ children }: { children: ReactNode }) { +function AiAvatarLayout({ + children, + meta, +}: { + children: ReactNode; + meta?: AiAvatarMeta | null; +}) { return ( -
-
-
{children}
-
-
+ + {meta ? ( + + ) : ( + + )} +
+
{children}
+
+
); } @@ -85,13 +100,6 @@ const GENERATING_PIPELINE_MESSAGES = [ "Almost there — finishing your avatar…", ]; -function ShareIcon({ platform }: { platform: keyof PhotoWallSharePlatforms }) { - if (platform === "linkedin") return ; - if (platform === "facebook") return ; - if (platform === "instagram") return ; - return ; -} - export function AiAvatarPage() { const { uploadToken } = useParams<{ uploadToken: string }>(); const fileRef = useRef(null); @@ -255,7 +263,7 @@ export function AiAvatarPage() { }); } } catch (e2) { - setErr(e2 instanceof Error ? e2.message : "Could not create avatar"); + setErr(e2 instanceof Error ? e2.message : "Could not generate avatar"); } finally { setSubmitting(false); } @@ -365,31 +373,25 @@ export function AiAvatarPage() { if (!uploadToken) { return ( - - -

Missing link

-
-
+ +

Missing link

+
); } if (loading) { return ( - - -

Loading…

-
-
+ +

Loading…

+
); } if (err && !meta) { return ( - - -

{err}

-
-
+ +

{err}

+
); } @@ -398,22 +400,13 @@ export function AiAvatarPage() { const total = GENERATING_PIPELINE_MESSAGES.length; const progressPct = Math.round((step / total) * 100); return ( - - - {meta?.event ? ( -
- -
- ) : null} + +
-

Creating your avatar

+

Generating your avatar

This can take up to a minute — hang tight

- - +
+
); } if (done) { return ( - - - {meta?.event ? ( -
- -
- ) : null} -

Your avatar

+ +
+

Your avatar

Here's your AI avatar. You can download or share it.

@@ -479,7 +463,7 @@ export function AiAvatarPage() { onClick={resetForNew} className="inline-flex h-11 w-full items-center justify-center rounded-lg theme-btn-bg px-4 text-sm font-semibold text-white" > - Create another + Generate another {allowDownload ? ( ) : null} @@ -503,11 +487,11 @@ export function AiAvatarPage() { onClick={() => void onNativeShare()} className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-zinc-300 bg-zinc-100 px-4 text-sm font-semibold text-gray-600" > - + Share ) : ( -
+
{enabledSharePlatforms.map((item) => ( ))}
@@ -524,27 +508,17 @@ export function AiAvatarPage() {
) : null} {actionMsg ?

{actionMsg}

: null} - - +
+
); } return ( - - - {meta?.event ? ( -
- -
- ) : null} - + +
{meta?.accessMode === "login" && !loggedIn ? (
-

AI Avatar

+

AI Avatar

Sign in to create your AI avatar.

-

Upload selfie

+

Upload selfie

Avatar: {selectedAvatar.name}

@@ -634,14 +608,14 @@ export function AiAvatarPage() { type="button" disabled={!file || submitting} onClick={() => void onSubmit()} - className="inline-flex h-11 w-full items-center justify-center rounded-lg theme-btn-bg px-4 text-sm font-semibold text-white hover:bg-[#b71c1c] disabled:opacity-55" + className="inline-flex h-11 w-full items-center justify-center rounded-lg theme-btn-bg px-4 text-sm font-semibold text-white hover:bg-[var(--theme-button-color)]/85 disabled:opacity-55" > - {submitting ? "Creating avatar…" : "Create avatar"} + {submitting ? "Generating avatar…" : "Generate avatar"} {err ?

{err}

: null}
)} - +
{cropSession && cropTarget ? ( ) : null} -
+ ); } diff --git a/src/pages/EventLandingPage.tsx b/src/pages/EventLandingPage.tsx index 6fbed26..ef50d6b 100644 --- a/src/pages/EventLandingPage.tsx +++ b/src/pages/EventLandingPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from "react"; -import { Calendar, MapPin } from "lucide-react"; +import { Calendar, ChevronDown, MapPin } from "lucide-react"; import { Link, useSearchParams } from "react-router-dom"; import { getVisitorEventAgenda, @@ -83,6 +83,7 @@ export function EventLandingPage() { }, [eventId]); const [dayOpen, setDayOpen] = useState>({}); + const [eventMetaOpen, setEventMetaOpen] = useState(false); useEffect(() => { setDayOpen({}); @@ -118,6 +119,9 @@ export function EventLandingPage() { const ratingsHref = scopedQs ? `/ratings?${scopedQs}` : "/ratings"; const bannerSrc = event?.bannerImage?.trim() || event?.image?.trim() || ""; + const eventDescription = event?.description?.trim() || event?.subtitle?.trim() || ""; + const eventLocation = event?.location?.trim() || ""; + const hasExpandableEventMeta = Boolean(eventDescription || schedule || eventLocation); if (!eventId) { return ( @@ -147,43 +151,67 @@ export function EventLandingPage() { return ( - + + +
+
+

{event.name}

+ {hasExpandableEventMeta ? ( + + ) : null} +
+
+ + {reg.enableRatings ? ( + + rate now + + ) : ( + Ratings off + )} +
+ {eventMetaOpen && hasExpandableEventMeta ? ( +
+ {eventDescription ? ( +

{eventDescription}

+ ) : null} +
+ {schedule ? ( +
+ + {schedule} +
+ ) : null} + {eventLocation ? ( +
+ + {eventLocation} +
+ ) : null} +
+
+ ) : null} +
-
-
-

{event.name}

-
- - {reg.enableRatings ? ( - - rate now - - ) : ( - Ratings off - )} -
-

- {event.description?.trim() || event.subtitle?.trim() || "Welcome to this event."} -

- {/* */} -
-
- - {schedule || "—"} -
- {event.location?.trim() ? ( -
- - {event.location.trim()} -
- ) : null} -
-
-
+ {bookmarkedIds.size > 0 ? (
diff --git a/src/pages/LeaderboardPage.tsx b/src/pages/LeaderboardPage.tsx index d193fa0..e26fd11 100644 --- a/src/pages/LeaderboardPage.tsx +++ b/src/pages/LeaderboardPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Award } from "lucide-react"; +import { Award, User } from "lucide-react"; import { Link, useSearchParams } from "react-router-dom"; import { getVisitorEventLeaderboard } from "../lib/api"; import { getAttendeeToken } from "../lib/attendeeAuth"; @@ -11,10 +11,10 @@ import type { VisitorLeaderboardRow } from "../lib/api"; function Medal({ tier }: { tier: "gold" | "silver" | "bronze" }) { const cls = tier === "gold" - ? "text-amber-500" + ? "text-amber-400" : tier === "silver" ? "text-slate-400" - : "text-amber-800"; + : "text-rose-400"; return ; } @@ -92,7 +92,7 @@ export function LeaderboardPage() {
*/}

Your Rank

- + {!signedIn ? "Sign in" : loading ? "…" : yourTotal != null ? `${yourTotal} points` : "0 points"}
@@ -137,15 +137,31 @@ export function LeaderboardPage() { {top5.map((row) => { const medal = medalForRank(row.rank); return ( + //
  • + // + // #{row.rank} · {row.total} pts + // + // {row.displayName} + // {medal ? : null} + //
  • - - #{row.rank} · {row.total} pts - - {row.displayName} - {medal ? : null} +
    + +
    +
    + {row.displayName} + + #{row.rank} · {row.total} pts + +
    + {/* {row.displayName} */} + {medal ? : null}
  • ); })} diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 252d61a..9160ff6 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -221,8 +221,8 @@ export function LoginPage() { if (!ctx) { return ( - -
    + +

    Open this app from your event link or QR code to sign in.

    @@ -237,12 +237,12 @@ export function LoginPage() { const showInviteField = reg.registrationMode === "Invite Only"; return ( - +
    -
    -
    +
    +
    -
    +
    {preview?.logoUrl ? (
    -

    Log in to

    +

    Log in to

    -

    +

    {headline} + {/* Novel Lung Health Summit 2026 Kochi 22nd & 23rd August 2026 */}

    {preview?.subtitle ? (

    {preview.subtitle}

    @@ -276,21 +277,21 @@ export function LoginPage() {

    ) : null} {showMeta ? ( -
    +
    {schedule ? ( - <> - +
    + {schedule} - +
    ) : null} {schedule && preview?.location?.trim() ? ( - + ) : null} {preview?.location?.trim() ? ( - <> - - {preview.location.trim()} - +
    + + {preview.location.trim()} +
    ) : null}
    ) : null} @@ -303,7 +304,7 @@ export function LoginPage() { 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 text-sm text-[var(--theme-card-text-color)] transition-colors duration-300 ease-in-out focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[var(--theme-button-color)]/30" + className="h-[44px] w-full rounded-lg border border-[var(--theme-card-text-color)]/10 bg-[var(--theme-card-color)]/40 px-4 text-sm text-[var(--theme-card-text-color)] placeholder:text-[var(--theme-card-text-color)]/60 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 disabled:opacity-60" /> ) : null} diff --git a/src/pages/PhotoSearchPage.tsx b/src/pages/PhotoSearchPage.tsx index b6c5da8..ebded22 100644 --- a/src/pages/PhotoSearchPage.tsx +++ b/src/pages/PhotoSearchPage.tsx @@ -8,15 +8,14 @@ import { } from "react"; import { Download, - Facebook, - Instagram, - Linkedin, Search, Share2, } from "lucide-react"; import { useParams } from "react-router-dom"; -import { AuthShell } from "../components/auth/AuthShell"; -import { PhotoWallEventHeader } from "../components/PhotoWallEventHeader"; +import { HeaderMobile } from "../components/HeaderMobile"; +import { MobileShell } from "../components/MobileShell"; +import { PublicEventChrome } from "../components/PublicEventChrome"; +import { SharePlatformIcon } from "../components/SharePlatformIcon"; import { getPhotoSearchMeta, getVisitorLoginPreview, @@ -39,13 +38,12 @@ import { type EbVisitorContext, } from "../types/visitorContext"; -function WhiteCard({ children, wide }: { children: ReactNode; wide?: boolean }) { +function PhotoSearchShell({ children }: { children: ReactNode }) { return ( -
    -
    -
    {children}
    -
    -
    + + + {children} + ); } @@ -58,13 +56,6 @@ const SHARE_LABELS: Array<{ key: keyof PhotoWallSharePlatforms; label: string }> { key: "whatsapp", label: "WhatsApp" }, ]; -function ShareIcon({ platform }: { platform: keyof PhotoWallSharePlatforms }) { - if (platform === "linkedin") return ; - if (platform === "facebook") return ; - if (platform === "instagram") return ; - return ; -} - function MatchCard({ match, eventName, @@ -176,7 +167,7 @@ function MatchCard({ }; return ( -
    +
    -
    +
    {allowDownload ? ( ) : null} {showShare ? ( @@ -208,22 +198,21 @@ function MatchCard({ ) : ( -
    +
    {enabledSharePlatforms.map((item) => ( ))}
    @@ -281,6 +270,9 @@ export function PhotoSearchPage() { const showShare = enabledSharePlatforms.length > 0; const allowDownload = meta?.allowDownload !== false; + const eventId = meta?.eventId || ""; + const eventName = meta?.event?.name?.trim() || meta?.eventName || "the event"; + const clearFile = () => { setFile(null); setPreviewUrl((prev) => { @@ -346,127 +338,129 @@ export function PhotoSearchPage() { if (!searchToken) { return ( - - + +

    Missing search link

    - - +
    +
    ); } if (loading) { return ( - - -

    Loading…

    -
    -
    + +
    +

    Loading…

    +
    +
    ); } if (err && !meta) { return ( - - + +

    {err}

    - - +
    +
    ); } return ( - - 0)}> - {meta?.event ? ( -
    - -
    - ) : null} + + -

    Find Photos

    -

    - Upload a selfie to find photos of you from {meta?.eventName || "the event"}. -

    +
    +
    +
    +

    Find Photos

    +

    + Upload a selfie to find photos of you from {eventName}. +

    - {meta?.accessMode === "login" && !loggedIn ? ( -
    -

    Sign in to search for your photos.

    - -
    - ) : ( -
    -
    - onPick(e.target.files?.[0] || null)} - /> - {previewUrl ? ( -
    - Your selfie + {meta?.accessMode === "login" && !loggedIn ? ( +
    +

    Sign in to search for your photos.

    + +
    + ) : ( +
    +
    + onPick(e.target.files?.[0] || null)} + /> + {previewUrl && !searched ? ( +
    + Your selfie +
    + ) : null} +
    - ) : null} - -
    + {file && !searched ? ( + + ) : null} - + {err ?

    {err}

    : null} - {err ?

    {err}

    : null} - - {searched ? ( -
    -

    - {matches && matches.length > 0 - ? `${matches.length} match${matches.length === 1 ? "" : "es"}` - : "No matches found"} -

    - {matches && matches.length > 0 ? ( -
    - {matches.map((m) => ( - - ))} + {searched ? ( +
    +

    + {matches && matches.length > 0 + ? `${matches.length} match${matches.length === 1 ? "" : "es"}` + : "No matches found"} +

    + {matches && matches.length > 0 ? ( +
    + {matches.map((m) => ( + + ))} +
    + ) : null}
    ) : null}
    - ) : null} -
    - )} - - + )} +
    +
    +
    +
    ); } diff --git a/src/pages/PhotoWallUploadPage.tsx b/src/pages/PhotoWallUploadPage.tsx index 05098d4..bc21f95 100644 --- a/src/pages/PhotoWallUploadPage.tsx +++ b/src/pages/PhotoWallUploadPage.tsx @@ -9,16 +9,14 @@ import { } from "react"; import { Download, - Facebook, ImagePlus, - Instagram, - Linkedin, Share2, } from "lucide-react"; import { useParams } from "react-router-dom"; import { AuthShell } from "../components/auth/AuthShell"; import { PhotoWallCropModal } from "../components/PhotoWallCropModal"; import { PhotoWallEventHeader } from "../components/PhotoWallEventHeader"; +import { SharePlatformIcon } from "../components/SharePlatformIcon"; import { getPhotoWallUploadMeta, getVisitorLoginPreview, @@ -79,13 +77,6 @@ const SHARE_LABELS: Array<{ key: keyof PhotoWallSharePlatforms; label: string }> { key: "whatsapp", label: "WhatsApp" }, ]; -function ShareIcon({ platform }: { platform: keyof PhotoWallSharePlatforms }) { - if (platform === "linkedin") return ; - if (platform === "facebook") return ; - if (platform === "instagram") return ; - return ; -} - export function PhotoWallUploadPage() { const { uploadToken } = useParams<{ uploadToken: string }>(); const fileRef = useRef(null); @@ -474,7 +465,7 @@ export function PhotoWallUploadPage() { onClick={() => void onShare(item.key)} className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-[#ddd] bg-white px-3 text-xs font-semibold text-[#333] hover:bg-[#fafafa]" > - + {item.label} ))} diff --git a/src/pages/QuizPollTakePage.tsx b/src/pages/QuizPollTakePage.tsx index 8e51eb3..1f60f53 100644 --- a/src/pages/QuizPollTakePage.tsx +++ b/src/pages/QuizPollTakePage.tsx @@ -919,7 +919,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"; + "rounded-lg p-3 text-center font-semibold shadow-sm disabled:opacity-50 disabled:hover:bg-inherit"; const footerSecondaryClass = `${footerBtnClass} flex-1 border border-zinc-300 bg-zinc-100 text-gray-700`; return ( @@ -989,7 +989,7 @@ export function QuizPollTakePage() {