Compare commits

...

2 Commits

Author SHA1 Message Date
Amey Bhagwat ca5bce7134 Merge branch 'main' of http://192.168.1.200:3000/Stratawiz/EventBuzz_Website
* 'main' of http://192.168.1.200:3000/Stratawiz/EventBuzz_Website:
  Apply presenter photo shape styles on the Photo Wall present page.
2026-08-19 10:47:25 +05:30
Amey Bhagwat 48f0ff4c0f Latest updated: Upload control has gallery and camera
Photo wall header
2026-08-19 10:46:53 +05:30
17 changed files with 314 additions and 179 deletions

View File

@ -0,0 +1,76 @@
import { useMemo, useRef } from "react";
import { Camera, Images } from "lucide-react";
import { isMobileUa } from "../lib/photoWallShare";
type PhotoSourcePickerProps = {
disabled?: boolean;
picking?: boolean;
hasFile?: boolean;
onPick: (file: File | null) => void;
};
const btnClass =
"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 disabled:opacity-55";
/**
* Mobile: gallery + camera (capture forces camera-only on iOS/Android).
* Desktop: one “Upload Photo” control that uses the gallery/file input.
*/
export function PhotoSourcePicker({ disabled, picking, hasFile, onPick }: PhotoSourcePickerProps) {
const galleryRef = useRef<HTMLInputElement>(null);
const cameraRef = useRef<HTMLInputElement>(null);
const isMobile = useMemo(() => isMobileUa(), []);
const emitFile = (input: HTMLInputElement | null, selected: File | null) => {
onPick(selected);
if (input) input.value = "";
};
const galleryLabel = picking
? "Processing…"
: isMobile
? "Gallery"
: hasFile
? "Change photo"
: "Upload Photo";
return (
<div className={isMobile ? "grid grid-cols-2 gap-2" : undefined}>
<input
ref={galleryRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) => emitFile(galleryRef.current, e.target.files?.[0] || null)}
/>
<input
ref={cameraRef}
type="file"
accept="image/*"
capture="user"
className="hidden"
onChange={(e) => emitFile(cameraRef.current, e.target.files?.[0] || null)}
/>
<button
type="button"
disabled={disabled}
onClick={() => galleryRef.current?.click()}
className={btnClass}
>
<Images className="size-5 shrink-0" aria-hidden />
{galleryLabel}
</button>
{isMobile ? (
<button
type="button"
disabled={disabled}
onClick={() => cameraRef.current?.click()}
className={btnClass}
>
<Camera className="size-5 shrink-0" aria-hidden />
{picking ? "Processing…" : "Camera"}
</button>
) : null}
</div>
);
}

View File

@ -0,0 +1,137 @@
import { Calendar, MapPin } from "lucide-react";
import type { PhotoWallEventHeaderDisplay, PhotoWallPublicEvent } from "../lib/api";
import { formatEventScheduleRange } from "../lib/sessionDisplay";
import { EventBanner } from "./EventBanner";
import { EventBuzzLogo } from "./EventBuzzLogo";
const EVENT_TYPE_LABEL: Record<string, string> = {
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 PhotoWallPresentChrome({
event,
eventHeader,
eventNameFallback = "",
}: {
event?: PhotoWallPublicEvent | null;
eventHeader?: PhotoWallEventHeaderDisplay | null;
eventNameFallback?: string;
}) {
const flags = resolveEventHeader(eventHeader);
const eventName = event?.name?.trim() || eventNameFallback || "Event";
const bannerSrc = event?.imageUrl?.trim() || "";
const logoSrc = event?.logoUrl?.trim() || "";
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 eventTypeLabel = formatEventType(event?.eventType);
const locationKind = formatLocationKind(event?.locationType);
const eventLocation = event?.location?.trim() || "";
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 showTitle = flags.showTitle && Boolean(eventName);
const showEventInfo = showTitle || showDate || showType || showLocationKind || showVenue;
const showHeader = flags.showLogo || showEventInfo;
if (!showHeader && !flags.showBanner) return null;
return (
<>
{showHeader ? (
<header className="flex flex-col items-start justify-start min-h-[64px] w-full sm:flex-row sm:items-center sm:justify-between gap-6 bg-[var(--theme-header-color)] text-[var(--theme-card-text-color)] px-4 py-3 shadow-xs sm:px-8">
<div className="flex min-w-0 shrink-0 items-center">
{flags.showLogo ? (
logoSrc ? (
<img
src={logoSrc}
alt={eventName}
className="max-h-10 max-w-[min(220px,40vw)] object-contain object-left"
/>
) : (
<EventBuzzLogo variant="color" className="h-8 max-h-8 w-auto" />
)
) : (
<span className="block w-0" aria-hidden />
)}
</div>
{showEventInfo ? (
<div className="min-w-0 sm:max-w-[min(640px,58vw)] flex flex-col items-start gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col items-start gap-1">
{showType ? (
<div className="mb-1 flex items-center justify-end text-xs">
<span className="rounded bg-[var(--theme-button-color)]/20 px-2 py-0.5">{eventTypeLabel}</span>
</div>
) : null}
{showTitle ? (
<h1 className="truncate text-base font-semibold leading-tight sm:text-lg">{eventName}</h1>
) : null}
</div>
<div className="flex flex-col items-start gap-1">
{showDate ? (
<div
className={`flex items-center justify-end gap-2 text-xs font-semibold text-[var(--theme-card-text-color)]/90 ${showTitle ? "mt-1" : ""}`}
>
<Calendar className="size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={2} />
<span>{schedule}</span>
</div>
) : null}
{showLocationKind || showVenue ? (
<div className="mt-1 flex flex-wrap flex-col items-start gap-x-3 gap-y-1 text-xs font-semibold text-[var(--theme-card-text-color)]/85">
{showLocationKind ? <span className="font-semibold uppercase">{locationKind}</span> : null}
{showVenue ? (
<span className="inline-flex max-w-full items-start gap-1.5 text-left">
<MapPin className="mt-0.5 size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={2} />
<span className="">{eventLocation}</span>
</span>
) : null}
</div>
) : null}
</div>
</div>
) : null}
</header>
) : null}
{flags.showBanner ? (
<EventBanner imageUrl={bannerSrc} alt={eventName} className="w-full shadow-sm mb-3" />
) : null}
</>
);
}

View File

@ -12,13 +12,13 @@ export function PresentFooterNav({
nextDisabled: boolean;
}) {
return (
<footer className="fixed inset-x-0 bottom-0 z-40 border-t border-[#e5e5ea] bg-white/95 shadow-[0_-4px_12px_rgba(0,0,0,0.06)] backdrop-blur-md">
<footer className="fixed inset-x-0 bottom-0 z-40 border-t-0 border-[#e5e5ea bg-white/ card-bg shadow-[0_-4px_12px_rgba(0,0,0,0.06) shadow-md backdrop-blur-m">
<div className="mx-auto flex h-16 max-w-5xl items-center justify-between gap-4 px-4 sm:px-6">
<button
type="button"
disabled={prevDisabled}
onClick={onPrev}
className="min-w-[88px] rounded-lg border border-[#e5e5ea] bg-white px-4 py-2 text-sm font-semibold text-[#24282f] transition hover:bg-[#f2f2f7] disabled:cursor-not-allowed disabled:opacity-40"
className="min-w-[88px] rounded-lg border-0 border-[#e5e5ea bg-whit bg-zinc-100 px-4 py-2 text-sm font-semibold text-[#24282f text-gray-800 transition hover:bg-[#f2f2f7] disabled:cursor-not-allowed disabled:opacity-40"
>
◀ Prev
</button>
@ -29,7 +29,7 @@ export function PresentFooterNav({
type="button"
disabled={nextDisabled}
onClick={onNext}
className="min-w-[88px] rounded-lg border border-[#e5e5ea] bg-white px-4 py-2 text-sm font-semibold text-[#24282f] transition hover:bg-[#f2f2f7] disabled:cursor-not-allowed disabled:opacity-40"
className="min-w-[88px] rounded-lg border-0 border-[#e5e5ea bg-whit bg-zinc-100 px-4 py-2 text-sm font-semibold text-[#24282f text-gray-800 transition hover:bg-[#f2f2f7] disabled:cursor-not-allowed disabled:opacity-40"
>
Next ▶
</button>

View File

@ -26,25 +26,25 @@ export function PresentLeaderboard({
}
return (
<div className="overflow-hidden rounded-lg border border-[#e5e5ea] bg-white shadow-sm">
<div className="grid grid-cols-[48px_minmax(0,1fr)_88px_72px] gap-2 border-b border-[#e5e5ea] bg-[#f2f2f7] px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-[rgba(36,40,47,0.55)] sm:grid-cols-[56px_minmax(0,1fr)_110px_88px] sm:px-4">
<div className="overflow-hidden rounded-lg border-0 border-[#e5e5ea bg-whit card-bg shadow-md">
<div className="grid grid-cols-[48px_minmax(0,1fr)_88px_72px] gap-2 border-b border-[#e5e5ea] bg-[#f2f2f7 bg-[var(--theme-card-color)]/50 px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-[rgba(36,40,47,0.55) text-[var(--theme-card-text-color)]/70 sm:grid-cols-[56px_minmax(0,1fr)_110px_88px] sm:px-4">
<span>Rank</span>
<span>Name</span>
<span className="text-right">Correct</span>
<span className="text-right">Time</span>
</div>
<ul className="divide-y divide-[#e5e5ea]">
<ul className="divide-y divide-[#e5e5ea divide-[var(--theme-card-text-color)]/30">
{entries.map((row) => (
<li
key={`${row.participantId || row.displayName}-${row.rank}`}
className="grid grid-cols-[48px_minmax(0,1fr)_88px_72px] items-center gap-2 px-3 py-2.5 sm:grid-cols-[56px_minmax(0,1fr)_110px_88px] sm:px-4"
>
<span className="text-sm font-bold tabular-nums text-[rgba(36,40,47,0.45)]">#{row.rank}</span>
<span className="truncate text-sm font-semibold text-[#24282f] sm:text-base">{row.displayName}</span>
<span className="text-right text-sm font-semibold tabular-nums text-[#24282f]">
<span className="text-sm font-bold tabular-nums text-[rgba(36,40,47,0.45) text-[var(--theme-card-text-color)]/70">#{row.rank}</span>
<span className="truncate text-sm font-semibold text-[#24282f sm:text-base">{row.displayName}</span>
<span className="text-right text-sm font-semibold tabular-nums text-[#24282f ">
{row.correctCount}/{row.totalQuestions}
</span>
<span className="text-right text-sm font-semibold tabular-nums text-[#1e72ef]">
<span className="text-right text-sm font-semibold tabular-nums text-[#1e72ef text-[var(--theme-button-color)]">
{formatTimeTaken(row.timeTakenSec)}
</span>
</li>

View File

@ -34,14 +34,14 @@ export function PresentRespondersList({
const reveal = Boolean(isQuiz && showCorrectness);
return (
<div className="overflow-hidden rounded-lg border border-[#e5e5ea] bg-white shadow-sm">
<div className="grid grid-cols-[48px_minmax(0,1fr)_96px_72px] gap-2 border-b border-[#e5e5ea] bg-[#f2f2f7] px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-[rgba(36,40,47,0.55)] sm:grid-cols-[56px_minmax(0,1fr)_110px_88px] sm:px-4">
<div className="overflow-hidden rounded-lg border-0 border-[#e5e5ea bg-whit card-bg shadow-md">
<div className="grid grid-cols-[48px_minmax(0,1fr)_96px_72px] gap-2 border-b border-[#e5e5ea] bg-[#f2f2f7 bg-[var(--theme-card-color)]/50 px-3 py-2 text-[11px] font-semibold uppercase tracking-wide text-[rgba(36,40,47,0.55) text-[var(--theme-card-text-color)]/70 sm:grid-cols-[56px_minmax(0,1fr)_110px_88px] sm:px-4">
<span>Rank</span>
<span>Name</span>
<span className="text-right">Result</span>
<span className="text-right">Time</span>
</div>
<ul className="divide-y divide-[#e5e5ea]">
<ul className="divide-y divide-[#e5e5ea divide-[var(--theme-card-text-color)]/30">
{responders.map((r, i) => {
const label = resultLabel(r.isCorrect, reveal);
return (
@ -49,20 +49,20 @@ export function PresentRespondersList({
key={`${r.participantId}-${r.answeredAt}-${i}`}
className="grid grid-cols-[48px_minmax(0,1fr)_96px_72px] items-center gap-2 px-3 py-2.5 sm:grid-cols-[56px_minmax(0,1fr)_110px_88px] sm:px-4"
>
<span className="text-sm font-bold tabular-nums text-[rgba(36,40,47,0.45)]">#{i + 1}</span>
<span className="truncate text-sm font-semibold text-[#24282f] sm:text-base">{r.displayName}</span>
<span className="text-sm font-bold tabular-nums text-[rgba(36,40,47,0.45) text-[var(--theme-card-text-color)]/70">#{i + 1}</span>
<span className="truncate text-sm font-semibold text-[#24282f sm:text-base">{r.displayName}</span>
<span
className={`text-right text-sm font-semibold ${
label === "Correct"
? "text-emerald-700"
: label === "Incorrect"
? "text-red-600"
: "text-[rgba(36,40,47,0.45)]"
: "text-[rgba(36,40,47,0.45) text-[var(--theme-card-text-color)]/70"
}`}
>
{label}
</span>
<span className="text-right text-sm font-semibold tabular-nums text-[#1e72ef]">
<span className="text-right text-sm font-semibold tabular-nums text-[#1e72ef text-[var(--theme-button-color)]">
{formatTimeTaken(r.responseTimeSec)}
</span>
</li>

View File

@ -77,7 +77,7 @@ export function PresentSessionMenu({
aria-expanded={open}
aria-controls={menuId}
onClick={() => setOpen((v) => !v)}
className="flex h-11 w-11 items-center justify-center rounded-lg border border-[#e5e5ea] bg-white text-[#24282f] shadow-sm transition hover:bg-[#f2f2f7]"
className="flex h-11 w-11 items-center justify-center rounded-lg border-0 border-[#e5e5ea bg-whit text-[#24282f card-bg shadow-sm transition hover:bg-[#f2f2f7]"
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
@ -96,11 +96,11 @@ export function PresentSessionMenu({
className="absolute left-0 top-full z-50 mt-2 max-h-[70vh] w-[min(22rem,calc(100vw-2rem))] overflow-y-auto rounded-xl border border-[#e5e5ea] bg-white py-2 shadow-lg"
>
{groups.length === 0 ? (
<p className="px-4 py-3 text-sm text-[rgba(36,40,47,0.55)]">No activities</p>
<p className="px-4 py-3 text-sm text-[rgba(36,40,47,0.55) text-[var(--theme-card-text-color)]/70">No activities</p>
) : (
groups.map((group) => (
<div key={group.label} className="py-1">
<p className="px-4 py-1.5 text-xs font-bold uppercase tracking-wide text-[rgba(36,40,47,0.45)]">
<p className="px-4 py-1.5 text-xs font-bold uppercase tracking-wide text-[rgba(36,40,47,0.45) text-[var(--theme-card-text-color)]/70">
{group.label}
</p>
{group.items.map((item) => {
@ -118,7 +118,7 @@ export function PresentSessionMenu({
isCurrent ? "bg-[#eef5ff]" : ""
}`}
>
<span className="min-w-0 flex-1 truncate font-semibold text-[#24282f]">
<span className="min-w-0 flex-1 truncate font-semibold text-[#24282f text-[var(--theme-card-text-color)]">
{item.title || "Untitled"}
</span>
{item.isOnAir ? (
@ -127,7 +127,7 @@ export function PresentSessionMenu({
</span>
) : null}
{isCurrent ? (
<span className="flex-shrink-0 text-xs font-semibold text-[#1e72ef]">Current</span>
<span className="flex-shrink-0 text-xs font-semibold text-[#1e72ef text-[var(--theme-button-color)]">Current</span>
) : null}
</button>
);

View File

@ -40,31 +40,31 @@ export function PresentStatsBars({
<div className="flex flex-col gap-3">
{options.map((opt) => {
const highlighted = revealCorrect && opt.isCorrect === true;
const barColor = highlighted ? "bg-emerald-500" : "bg-[#1e72ef]";
const barColor = highlighted ? "bg-emerald-500" : "bg-[#1e72ef bg-[var(--theme-button-color)]";
const borderColor = highlighted ? "border-emerald-400 ring-2 ring-emerald-200" : "border-[#e5e5ea]";
return (
<div key={opt.optionId} className={`rounded-lg border bg-white p-4 shadow-sm ${borderColor}`}>
<div key={opt.optionId} className={`rounded-lg border bg-whit card-bg p-4 shadow-sm ${borderColor}`}>
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
<span className="min-w-0 flex-1 text-base font-medium leading-snug text-[#24282f]">
<span className="min-w-0 flex-1 text-base font-medium leading-snug text-[#24282f">
{opt.text || "—"}
{highlighted ? (
<span className="ml-2 inline-flex rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-emerald-800">
<span className="ml-2 inline-flex rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-bold uppercase tracking-wide text-emerald-800">
Correct answer
</span>
) : null}
</span>
<div className="flex shrink-0 items-center gap-2">
<span className="text-sm font-semibold tabular-nums text-[#1e72ef]">{opt.percent}%</span>
<div className="flex items-center gap-1.5 rounded-full bg-[#f2f2f7] px-2.5 py-0.5">
<span className="text-base font-bold tabular-nums leading-none text-[#24282f]">{opt.count}</span>
<span className="text-[10px] font-semibold uppercase tracking-wide text-[rgba(36,40,47,0.55)]">
<span className="text-sm font-semibold tabular-nums text-[#1e72ef text-[var(--theme-button-color)]">{opt.percent}%</span>
<div className="flex items-center gap-1.5 rounded-full bg-[#f2f2f7 bg-zinc-100 px-2.5 py-0.5">
<span className="text-base font-bold tabular-nums leading-none text-[#24282f">{opt.count}</span>
<span className="text-[10px] font-semibold uppercase tracking-wide text-[rgba(36,40,47,0.55) text-[var(--theme-card-text-color)]/70">
{opt.count === 1 ? "response" : "responses"}
</span>
</div>
</div>
</div>
<div className="h-3 overflow-hidden rounded-full bg-[#e5e5ea]">
<div className="h-3 overflow-hidden rounded-full bg-[#e5e5ea bg-zinc-400/30">
<div
className={`h-full rounded-full transition-[width] duration-500 ease-out ${barColor}`}
style={{ width: `${Math.min(100, Math.max(opt.count > 0 ? 2 : 0, opt.percent))}%` }}

View File

@ -59,7 +59,7 @@ body {
/* Auth / modal shells (Figma EventBuzz_Event_Mobile_V1_Register) */
.eb-gradient-auth {
background-color: var(--theme-bg-color, transparent);
/* background-color: var(--theme-bg-color, transparent);
background-image: var(
--theme-bg-image,
linear-gradient(
@ -71,7 +71,7 @@ body {
);
background-size: var(--theme-bg-size, cover);
background-repeat: var(--theme-bg-repeat, no-repeat);
background-position: var(--theme-bg-position, center center);
background-position: var(--theme-bg-position, center center); */
}
/* Rewards / Leaderboard “Total Points” card */

View File

@ -10,10 +10,12 @@ import {
Download,
Loader2,
Share2,
ArrowLeft,
} from "lucide-react";
import { useParams } from "react-router-dom";
import { HeaderMobile } from "../components/HeaderMobile";
import { MobileShell } from "../components/MobileShell";
import { PhotoSourcePicker } from "../components/PhotoSourcePicker";
import { PhotoWallCropModal } from "../components/PhotoWallCropModal";
import { PublicEventChrome } from "../components/PublicEventChrome";
import { SharePlatformIcon } from "../components/SharePlatformIcon";
@ -102,7 +104,6 @@ const GENERATING_PIPELINE_MESSAGES = [
export function AiAvatarPage() {
const { uploadToken } = useParams<{ uploadToken: string }>();
const fileRef = useRef<HTMLInputElement>(null);
const resultImgRef = useRef<HTMLImageElement>(null);
const [meta, setMeta] = useState<AiAvatarMeta | null>(null);
const [loading, setLoading] = useState(true);
@ -191,7 +192,6 @@ export function AiAvatarPage() {
if (prev) URL.revokeObjectURL(prev);
return null;
});
if (fileRef.current) fileRef.current.value = "";
}, []);
const applySelectedFile = useCallback((next: File) => {
@ -212,10 +212,7 @@ export function AiAvatarPage() {
}, [selectedAvatar]);
const handleFileChange = async (selected: File | null) => {
if (!selected) {
clearSelectedPhoto();
return;
}
if (!selected) return;
if (!selected.type.startsWith("image/")) {
setErr("Please choose an image file");
return;
@ -238,7 +235,6 @@ export function AiAvatarPage() {
setErr(e instanceof Error ? e.message : "Could not process image");
} finally {
setPicking(false);
if (fileRef.current) fileRef.current.value = "";
}
};
@ -477,10 +473,7 @@ export function AiAvatarPage() {
) : null}
</div>
{showShare ? (
<div className="mt-4">
<p className="mb-2 text-center text-xs font-semibold uppercase tracking-wide text-[var(--theme-card-text-color)]/60">
Share
</p>
<div className="mt-2">
{useNativeShare ? (
<button
type="button"
@ -574,36 +567,29 @@ export function AiAvatarPage() {
clearSelectedPhoto();
setErr(null);
}}
className="text-sm font-semibold text-[var(--theme-card-text-color)]"
className="text-xs font-semibold inline-flex items-center gap-1 text-[var(--theme-card-text-color)]/80"
>
← Change avatar
<span className="flex items-center justify-center w-7 h-7 bg-[var(--theme-card-text-color)]/10 rounded-full">
<ArrowLeft className="size-4" />
</span>
Change avatar
</button>
<h2 className="mb-1 text-center text-[22px] font-semibold text-[var(--theme-card-text-color)]">Upload selfie</h2>
<p className="text-center text-sm text-[var(--theme-card-text-color)]/80">
Avatar: <span className="font-semibold">{selectedAvatar.name}</span>
</p>
<input
ref={fileRef}
type="file"
accept="image/*"
capture="user"
className="hidden"
onChange={(e) => void handleFileChange(e.target.files?.[0] || null)}
/>
{previewUrl ? (
<div className="overflow-hidden rounded-lg">
<img src={previewUrl} alt="Your selfie" className="mx-auto max-h-[240px] w-full object-contain" />
</div>
) : null}
<button
type="button"
<PhotoSourcePicker
disabled={picking}
onClick={() => fileRef.current?.click()}
className="inline-flex h-11 w-full items-center justify-center rounded-lg border border-zinc-300 bg-zinc-100 px-4 text-sm font-semibold text-gray-600 disabled:opacity-55"
>
{picking ? "Processing…" : file ? "Change selfie" : "Choose selfie"}
</button>
picking={picking}
hasFile={Boolean(file)}
onPick={(selected) => void handleFileChange(selected)}
/>
<button
type="button"
disabled={!file || submitting}

View File

@ -77,7 +77,7 @@ export function LeaderboardPage() {
return (
<MobileShell className="relative">
<HeaderMobile logoUrl={ctx?.event?.logoUrl} eventId={eventId || null} />
<div className="eb-gradient-auth flex flex-1 flex-col items-center px-6 pb-10 pt-2">
<div className="eb-gradient-auth flex flex-1 flex-col items-center px-4 pb-10 pt-2">
<div className="mt-6 w-full max-w-[354px] rounded-xl card-bg shadow-md">
<div className="flex gap-2 justify-between w-full p-4">

View File

@ -14,6 +14,7 @@ import {
import { useParams } from "react-router-dom";
import { HeaderMobile } from "../components/HeaderMobile";
import { MobileShell } from "../components/MobileShell";
import { PhotoSourcePicker } from "../components/PhotoSourcePicker";
import { PublicEventChrome } from "../components/PublicEventChrome";
import { SharePlatformIcon } from "../components/SharePlatformIcon";
import {
@ -218,7 +219,6 @@ function MatchCard({
export function PhotoSearchPage() {
const { searchToken } = useParams<{ searchToken: string }>();
const fileRef = useRef<HTMLInputElement>(null);
const [meta, setMeta] = useState<PhotoSearchMeta | null>(null);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState<string | null>(null);
@ -271,14 +271,10 @@ export function PhotoSearchPage() {
if (prev) URL.revokeObjectURL(prev);
return null;
});
if (fileRef.current) fileRef.current.value = "";
};
const onPick = (selected: File | null) => {
if (!selected) {
clearFile();
return;
}
if (!selected) return;
if (!selected.type.startsWith("image/")) {
setErr("Please choose an image file");
return;
@ -389,26 +385,15 @@ export function PhotoSearchPage() {
) : (
<div className="space-y-4">
<div>
<input
ref={fileRef}
type="file"
accept="image/*"
capture="user"
className="hidden"
onChange={(e) => onPick(e.target.files?.[0] || null)}
/>
{previewUrl && !searched ? (
<div className="mb-3 overflow-hidden rounded-lg">
<img src={previewUrl} alt="Your selfie" className="mx-auto max-h-[240px] w-full object-contain" />
</div>
) : null}
<button
type="button"
onClick={() => fileRef.current?.click()}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-zinc-300 bg-zinc-100 px-4 text-sm font-semibold text-gray-600"
>
{file ? "Change your photo" : "Upload your photo"}
</button>
<PhotoSourcePicker
hasFile={Boolean(file)}
onPick={(selected) => onPick(selected)}
/>
</div>
{file && !searched ? (
<button

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { getPhotoWallPresent, type PhotoWallPresentData } from "../lib/api";
import { PhotoWallEventHeader } from "../components/PhotoWallEventHeader";
import { PhotoWallPresentChrome } from "../components/PhotoWallPresentChrome";
const POLL_MS = 4000;
@ -60,7 +60,7 @@ export function PhotoWallPresentPage() {
if (!data) {
return (
<div className="eb-gradient-auth flex min-h-dvh flex-col items-center justify-center gap-2 px-6 text-center text-white">
<div className="eb-gradient-aut card-bg p-4 flex min-h-dvh flex-col items-center justify-center gap-2 px-6 text-center text-white">
<p className="text-lg font-semibold">Photo wall unavailable</p>
<p className="text-sm text-white/80">{err}</p>
</div>
@ -89,22 +89,11 @@ export function PhotoWallPresentPage() {
return (
<div className="eb-gradient-auth min-h-dvh text-white">
{data.event ? (
<div className="px-4 pb-3 pt-4">
<div className="card-bg mx-auto mb-3 max-w-[400px] rounded-lg p-4 shadow-sm [&:not(:has(header))]:hidden">
<PhotoWallEventHeader
event={data.event}
display={data.eventHeader || null}
eyebrow={data.eventHeader?.showTitle === false ? null : "Photo Wall"}
/>
</div>
</div>
) : (
<div className="card-bg mx-auto mb-3 max-w-[400px] rounded-lg px-4 pb-3 pt-4 text-center shadow-sm">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--theme-card-text-color)]/70">Photo Wall</p>
<h1 className="text-xl font-semibold text-[var(--theme-card-text-color)] sm:text-2xl">{data.eventName}</h1>
</div>
)}
<PhotoWallPresentChrome
event={data.event}
eventHeader={data.eventHeader}
eventNameFallback={data.eventName}
/>
{data.photos.length === 0 ? (
<div className="flex min-h-[60dvh] items-center justify-center px-6 text-center text-white/80">

View File

@ -16,6 +16,7 @@ import {
import { useParams } from "react-router-dom";
import { HeaderMobile } from "../components/HeaderMobile";
import { MobileShell } from "../components/MobileShell";
import { PhotoSourcePicker } from "../components/PhotoSourcePicker";
import { PhotoWallCropModal } from "../components/PhotoWallCropModal";
import { PublicEventChrome } from "../components/PublicEventChrome";
import { SharePlatformIcon } from "../components/SharePlatformIcon";
@ -97,7 +98,6 @@ const SHARE_LABELS: Array<{ key: keyof PhotoWallSharePlatforms; label: string }>
export function PhotoWallUploadPage() {
const { uploadToken } = useParams<{ uploadToken: string }>();
const fileRef = useRef<HTMLInputElement>(null);
const resultImgRef = useRef<HTMLImageElement>(null);
const [meta, setMeta] = useState<PhotoWallUploadMeta | null>(null);
const [loading, setLoading] = useState(true);
@ -164,7 +164,6 @@ export function PhotoWallUploadPage() {
if (prev) URL.revokeObjectURL(prev);
return null;
});
if (fileRef.current) fileRef.current.value = "";
}, []);
useEffect(() => {
@ -209,10 +208,7 @@ export function PhotoWallUploadPage() {
}, []);
const handleFileChange = async (selected: File | null) => {
if (!selected) {
clearSelectedPhoto();
return;
}
if (!selected) return;
if (!selected.type.startsWith("image/")) {
setErr("Please choose an image file");
return;
@ -238,7 +234,6 @@ export function PhotoWallUploadPage() {
setErr(e instanceof Error ? e.message : "Could not process image");
} finally {
setPicking(false);
if (fileRef.current) fileRef.current.value = "";
}
};
@ -560,29 +555,17 @@ export function PhotoWallUploadPage() {
) : (
<form onSubmit={(e) => void onSubmit(e)} className="space-y-4">
<div>
<input
ref={fileRef}
type="file"
accept="image/*"
capture="user"
className="hidden"
onChange={(e) => void handleFileChange(e.target.files?.[0] || null)}
/>
<button
type="button"
{previewUrl ? (
<div className="mb-3 overflow-hidden rounded-lg">
<img src={previewUrl} alt="Preview" className="mx-auto max-h-48 rounded-md object-contain" />
</div>
) : null}
<PhotoSourcePicker
disabled={picking}
onClick={() => fileRef.current?.click()}
className="flex w-full flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-[var(--theme-card-text-color)]/30 bg-[var(--theme-card-color)]/80 px-4 py-4 text-sm text-[var(--theme-card-text-color)] hover:border-[var(--theme-button-color)]/70"
>
{previewUrl ? (
<img src={previewUrl} alt="Preview" className="max-h-48 rounded-md object-contain" />
) : (
<>
<ImagePlus className="size-8 text-[var(--theme-button-color)]" />
<span>{picking ? "Preparing…" : "Tap to take or choose a photo"}</span>
</>
)}
</button>
picking={picking}
hasFile={Boolean(file)}
onPick={(selected) => void handleFileChange(selected)}
/>
{file ? (
<p className="mt-2 truncate text-center text-xs text-[var(--theme-card-text-color)]/80">{file.name}</p>
) : null}

View File

@ -444,9 +444,9 @@ export function QuizPollPresentPage() {
const t = token?.trim() || "";
return (
<div className="flex min-h-dvh flex-col bg-[#f2f2f7] pb-20 text-[#24282f]">
<div className="flex min-h-dvh flex-col bg-[#f2f2f7 pb-20 text-[#24282f text-[var(--theme-card-text-color)]">
<main className="mx-auto flex w-full max-w-4xl flex-1 flex-col gap-3 px-4 py-3 sm:px-6">
<div className="rounded-xl border border-[#e5e5ea] bg-white px-5 py-3 shadow-sm">
<div className="rounded-xl border-0 border-[#e5e5ea bg-whit card-bg px-5 py-3 shadow-md relative z-10">
<div className="mb-2 flex flex-wrap items-start justify-between gap-3">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<PresentSessionMenu
@ -470,7 +470,7 @@ export function QuizPollPresentPage() {
Live · Question {liveQuestionIndex + 1}
</span>
) : (
<span className="rounded-full bg-[#f2f2f7] px-3 py-1 text-xs font-bold uppercase tracking-wide text-[rgba(36,40,47,0.55)]">
<span className="rounded-full bg-[#f2f2f7 px-3 py-1 text-xs font-bold uppercase tracking-wide text-[rgba(36,40,47,0.55) bg-zinc-100 text-[var(--theme-card-text-color)]/70">
Offline · No question live
</span>
)
@ -479,7 +479,7 @@ export function QuizPollPresentPage() {
Live
</span>
) : (
<span className="rounded-full bg-[#f2f2f7] px-3 py-1 text-xs font-bold uppercase tracking-wide text-[rgba(36,40,47,0.55)]">
<span className="rounded-full bg-[#f2f2f7 px-3 py-1 text-xs font-bold uppercase tracking-wide text-[rgba(36,40,47,0.55) bg-zinc-100 text-[var(--theme-card-text-color)]/70">
Offline
</span>
)}
@ -534,21 +534,21 @@ export function QuizPollPresentPage() {
</div>
</div>
<p className="text-sm font-medium uppercase tracking-wide text-[#1e72ef]">
<p className="text-xs font-semibold uppercase tracking-wide text-[#1e72ef text-[var(--theme-button-color)]">
Question {questionSafe + 1} of {questionCount}
</p>
<h1 className="mt-1 text-base font-semibold leading-snug text-[#24282f] sm:text-lg">{currentQuestion.question}</h1>
<h1 className="mt-1 text-base font-semibold leading-snug text-[#24282f sm:text-lg">{currentQuestion.question}</h1>
{statsLoading && !stats ? (
<p className="mt-2 text-xs text-[rgba(36,40,47,0.45)]">Updating…</p>
<p className="mt-2 text-xs text-[rgba(36,40,47,0.45) text-[var(--theme-card-text-color)]/60">Updating…</p>
) : null}
</div>
<div className="flex gap-2 rounded-lg border border-[#e5e5ea] bg-white p-1 shadow-sm">
<div className="flex gap-2 rounded-lg border-0 border-[#e5e5ea bg-white card-bg p-1 shadow-md">
<button
type="button"
onClick={() => setTab("responses")}
className={`flex-1 rounded-md px-3 py-2 text-sm font-semibold transition ${
tab === "responses" ? "bg-[#1e72ef] text-white" : "text-[rgba(36,40,47,0.65)] hover:text-[#24282f]"
tab === "responses" ? "bg-[#1e72ef theme-btn-bg text-white" : "text-[rgba(36,40,47,0.65) hover:text-[#24282f text-[var(--theme-card-text-color)]/80 hover:text-[var(--theme-card-text-color)]"
}`}
>
Responses{stats ? ` (${stats.totalResponses})` : ""}
@ -557,7 +557,7 @@ export function QuizPollPresentPage() {
type="button"
onClick={() => setTab("participants")}
className={`flex-1 rounded-md px-3 py-2 text-sm font-semibold transition ${
tab === "participants" ? "bg-[#1e72ef] text-white" : "text-[rgba(36,40,47,0.65)] hover:text-[#24282f]"
tab === "participants" ? "bg-[#1e72ef theme-btn-bg text-white" : "text-[rgba(36,40,47,0.65) hover:text-[#24282f text-[var(--theme-card-text-color)]/80 hover:text-[var(--theme-card-text-color)]"
}`}
>
Participants
@ -567,7 +567,7 @@ export function QuizPollPresentPage() {
type="button"
onClick={() => setTab("leaderboard")}
className={`flex-1 rounded-md px-3 py-2 text-sm font-semibold transition ${
tab === "leaderboard" ? "bg-[#1e72ef] text-white" : "text-[rgba(36,40,47,0.65)] hover:text-[#24282f]"
tab === "leaderboard" ? "bg-[#1e72ef theme-btn-bg text-white" : "text-[rgba(36,40,47,0.65) hover:text-[#24282f text-[var(--theme-card-text-color)]/80 hover:text-[var(--theme-card-text-color)]"
}`}
>
Leaderboard{leaderboard.length > 0 ? ` (${leaderboard.length})` : ""}

View File

@ -722,7 +722,7 @@ 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 px-4 pb-8 pt-6">
<main className="min-h-0 flex-1 overflow-y-auto px-4 pb-8 pt-6">
<div className="mx-auto w-full max-w-[370px]">
<div className="rounded-lg card-bg p-5 text-center shadow-md">
<p className="font-inter text-lg font-semibold text-[var(--theme-card-text-color)]]">Thank you</p>
@ -929,7 +929,7 @@ export function QuizPollTakePage() {
</div>
<main
ref={mainScrollRef}
className="relative z-0 min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-6"
className="relative z-0 min-h-0 flex-1 overflow-y-auto px-4 pt-6"
>
<div className="mx-auto w-full max-w-[370px] pb-4">
<div className="flex flex-col gap-1 card-bg p-4 rounded-lg shadow-sm">

View File

@ -70,7 +70,7 @@ export function RewardsPage() {
return (
<MobileShell className="relative">
<HeaderMobile logoUrl={ctx?.event?.logoUrl} eventId={eventId || null} />
<div className="eb-gradient-auth flex flex-1 flex-col items-center px-6 pb-10 pt-2">
<div className="eb-gradient-auth flex flex-1 flex-col items-center px-4 pb-10 pt-2">
{/* <div className="flex flex-col w-full max-w-[354px] items-center card-bg p-4 rounded-md">
<Trophy className="size-[68px] text-amber-300" strokeWidth={1.25} />
<h1 className="mt-1 font-inter text-xl font-semibold text-white">Your Rewards</h1>
@ -98,12 +98,6 @@ export function RewardsPage() {
</div>
<div className="flex justify-center px-4">
{/* <Link
to={eventId ? `/leaderboard?event_id=${encodeURIComponent(eventId)}` : "/leaderboard"}
className="w-full rounded-ful rounded-lg border border-[rgba(214,200,200,0.6)] bg-[var(--theme-button-color)] transition-colors duration-300 ease-in-out hover:bg-[var(--theme-button-color)]/90 py-3 text-center text-xs font-semibold uppercase tracking-wide text-white shadow-[0_4px_4px_rgba(0,0,0,0.15)]"
>
Leaderboard
</Link> */}
<Link
to={eventId ? `/leaderboard?event_id=${encodeURIComponent(eventId)}` : "/leaderboard"}
className="w-full rounded-ful rounded-lg border border-[var(--theme-button-color)]/25 bg-[var(--theme-button-color)]/20 text-[var(--theme-button-color)] py-3 text-center text-sm font-semibold uppercase tracking-wide shadow-sm"
@ -133,7 +127,7 @@ export function RewardsPage() {
return (
<li
key={row._id}
className="flex items-center gap-4 border-b border-[var(--theme-card-text-color)]/5 pb-3 last:border-0"
className="flex items-center gap-2 border-b border-[var(--theme-card-text-color)]/5 pb-3 last:border-0"
>
<span className={`shrink-0 px-1 py-1 text-sm font-semibold ${cls}`}>
{signed}

View File

@ -9,10 +9,12 @@ import {
import {
Download,
Share2,
ArrowLeft,
} from "lucide-react";
import { useParams } from "react-router-dom";
import { HeaderMobile } from "../components/HeaderMobile";
import { MobileShell } from "../components/MobileShell";
import { PhotoSourcePicker } from "../components/PhotoSourcePicker";
import { PhotoWallCropModal } from "../components/PhotoWallCropModal";
import { PublicEventChrome } from "../components/PublicEventChrome";
import { SharePlatformIcon } from "../components/SharePlatformIcon";
@ -87,7 +89,6 @@ const SHARE_LABELS: Array<{ key: keyof PhotoWallSharePlatforms; label: string }>
export function SocialSharePage() {
const { uploadToken } = useParams<{ uploadToken: string }>();
const fileRef = useRef<HTMLInputElement>(null);
const resultImgRef = useRef<HTMLImageElement>(null);
const [meta, setMeta] = useState<SocialShareMeta | null>(null);
const [loading, setLoading] = useState(true);
@ -174,7 +175,6 @@ export function SocialSharePage() {
if (prev) URL.revokeObjectURL(prev);
return null;
});
if (fileRef.current) fileRef.current.value = "";
}, []);
const applySelectedFile = useCallback((next: File) => {
@ -201,10 +201,7 @@ export function SocialSharePage() {
}, [selectedFrame, meta?.requireUploadCrop, meta?.maxPhotoWidth, meta?.maxPhotoHeight]);
const handleFileChange = async (selected: File | null) => {
if (!selected) {
clearSelectedPhoto();
return;
}
if (!selected) return;
if (!selected.type.startsWith("image/")) {
setErr("Please choose an image file");
return;
@ -229,7 +226,6 @@ export function SocialSharePage() {
setErr(e instanceof Error ? e.message : "Could not process image");
} finally {
setPicking(false);
if (fileRef.current) fileRef.current.value = "";
}
};
@ -459,10 +455,7 @@ export function SocialSharePage() {
) : null}
</div>
{showShare ? (
<div className="mt-4">
<p className="mb-2 text-center text-xs font-semibold uppercase tracking-wide text-[var(--theme-card-text-color)]/60">
Share
</p>
<div className="mt-2">
{useNativeShare ? (
<button
type="button"
@ -556,36 +549,28 @@ export function SocialSharePage() {
clearSelectedPhoto();
setErr(null);
}}
className="text-sm font-semibold theme-btn-lin text-[var(--theme-card-text-color)]"
className="text-xs font-semibold inline-flex items-center gap-1 text-[var(--theme-card-text-color)]/80"
>
← Change frame
<span className="flex items-center justify-center w-7 h-7 bg-[var(--theme-card-text-color)]/10 rounded-full">
<ArrowLeft className="size-4" />
</span>Change frame
</button>
<h2 className="mb-2 text-center text-[22px] font-semibold text-[var(--theme-card-text-color)]">Upload photo</h2>
<p className="text-center text-sm text-[var(--theme-card-text-color)]/80">
Selected frame: <span className="font-semibold">{selectedFrame.name}</span>
</p>
<input
ref={fileRef}
type="file"
accept="image/*"
capture="user"
className="hidden"
onChange={(e) => void handleFileChange(e.target.files?.[0] || null)}
/>
{previewUrl ? (
<div className="overflow-hidden rounded-lg">
<img src={previewUrl} alt="Your photo" className="mx-auto max-h-[240px] w-full object-contain" />
</div>
) : null}
<button
type="button"
<PhotoSourcePicker
disabled={picking}
onClick={() => fileRef.current?.click()}
className="inline-flex h-11 w-full items-center justify-center rounded-lg border border-zinc-300 bg-zinc-100 px-4 text-sm font-semibold text-gray-600 disabled:opacity-55"
>
{picking ? "Processing…" : file ? "Change photo" : "Choose photo"}
</button>
picking={picking}
hasFile={Boolean(file)}
onPick={(selected) => void handleFileChange(selected)}
/>
<button
type="button"
disabled={!file || submitting}