From 229e3cdcda15587d8eb1f37e321da8e05133a076 Mon Sep 17 00:00:00 2001 From: Ravi Gulhane <> Date: Thu, 13 Aug 2026 10:37:39 +0530 Subject: [PATCH 1/2] Changes --- package-lock.json | 20 + package.json | 1 + src/App.tsx | 4 + src/components/EventBuzzLogo.tsx | 2 +- src/components/PhotoWallCropModal.tsx | 113 ++++ src/components/PhotoWallEventHeader.tsx | 141 +++++ src/components/auth/AuthShell.tsx | 24 +- src/components/present/PresentLeaderboard.tsx | 55 ++ .../present/PresentRespondersList.tsx | 79 +-- src/components/present/PresentStatsBars.tsx | 20 +- src/hooks/useAutoNavigateToLiveQuizPoll.ts | 65 +++ src/lib/api.ts | 194 +++++++ src/lib/cropImage.ts | 64 +++ src/lib/photoWallShare.ts | 172 ++++++ src/lib/sessionPresentationRtdb.ts | 42 ++ src/lib/sessionQuizPollsRtdb.ts | 36 +- src/pages/CodeEntryPage.tsx | 2 +- src/pages/FeedbackPage.tsx | 7 + src/pages/Home.tsx | 451 ++------------- src/pages/PhotoWallPresentPage.tsx | 113 ++++ src/pages/PhotoWallUploadPage.tsx | 538 ++++++++++++++++++ src/pages/Questions.tsx | 7 + src/pages/QuizPollPresentPage.tsx | 116 +++- src/pages/QuizPollTakePage.tsx | 35 +- src/pages/RatingsPage.tsx | 7 + src/pages/SingleEventPage.tsx | 29 +- src/types/visitorContext.ts | 24 + 27 files changed, 1845 insertions(+), 516 deletions(-) create mode 100644 src/components/PhotoWallCropModal.tsx create mode 100644 src/components/PhotoWallEventHeader.tsx create mode 100644 src/components/present/PresentLeaderboard.tsx create mode 100644 src/hooks/useAutoNavigateToLiveQuizPoll.ts create mode 100644 src/lib/cropImage.ts create mode 100644 src/lib/photoWallShare.ts create mode 100644 src/lib/sessionPresentationRtdb.ts create mode 100644 src/pages/PhotoWallPresentPage.tsx create mode 100644 src/pages/PhotoWallUploadPage.tsx diff --git a/package-lock.json b/package-lock.json index 95a9ade..6b89dda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "lucide-react": "0.487.0", "react": "18.3.1", "react-dom": "18.3.1", + "react-easy-crop": "^6.2.3", "react-router-dom": "^6.28.0" }, "devDependencies": { @@ -3082,6 +3083,12 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-wheel": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz", + "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==", + "license": "BSD-3-Clause" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3180,6 +3187,19 @@ "react": "^18.3.1" } }, + "node_modules/react-easy-crop": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-6.2.3.tgz", + "integrity": "sha512-ebimG3OGlzizjxEZ77Cj9CVLcg5vDZ6QVz8ud1rx4WmsCDWHIROEhgMi0GpJ/jKAuwHrH0M+QZUK4hyvxbYhOA==", + "license": "MIT", + "dependencies": { + "normalize-wheel": "^1.0.1" + }, + "peerDependencies": { + "react": ">=16.4.0", + "react-dom": ">=16.4.0" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", diff --git a/package.json b/package.json index 41d810d..f56c082 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "lucide-react": "0.487.0", "react": "18.3.1", "react-dom": "18.3.1", + "react-easy-crop": "^6.2.3", "react-router-dom": "^6.28.0" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index cbf32d3..b13a98c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,8 @@ import { SingleEventPage } from "./pages/SingleEventPage"; import { QuizPollTakePage } from "./pages/QuizPollTakePage"; import { QuizPollPresentPage } from "./pages/QuizPollPresentPage"; import { QuestionsPresentPage } from "./pages/QuestionsPresentPage"; +import { PhotoWallUploadPage } from "./pages/PhotoWallUploadPage"; +import { PhotoWallPresentPage } from "./pages/PhotoWallPresentPage"; import { CodeEntryPage } from "./pages/CodeEntryPage"; import { PrivacyPolicyPage } from "./pages/PrivacyPolicyPage"; import { TermsOfServicePage } from "./pages/TermsOfServicePage"; @@ -41,6 +43,8 @@ export default function App() { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/src/components/EventBuzzLogo.tsx b/src/components/EventBuzzLogo.tsx index b578495..2278470 100644 --- a/src/components/EventBuzzLogo.tsx +++ b/src/components/EventBuzzLogo.tsx @@ -20,7 +20,7 @@ export function EventBuzzLogo({ alt={alt} width={256} height={48} - className={["block h-auto min-h-[36px] w-auto max-w-full shrink-0 object-contain", className] + className={["block w-auto max-w-full shrink-0 object-contain", className] .filter(Boolean) .join(" ")} /> diff --git a/src/components/PhotoWallCropModal.tsx b/src/components/PhotoWallCropModal.tsx new file mode 100644 index 0000000..5ba0c09 --- /dev/null +++ b/src/components/PhotoWallCropModal.tsx @@ -0,0 +1,113 @@ +import { useCallback, useState } from "react"; +import Cropper, { type Area } from "react-easy-crop"; +import { getCroppedImageFile } from "../lib/cropImage"; + +type PhotoWallCropModalProps = { + imageSrc: string; + aspect: number; + sourceFileName?: string; + onCancel: () => void; + onConfirm: (file: File) => void; +}; + +export function PhotoWallCropModal({ + imageSrc, + aspect, + sourceFileName, + onCancel, + onConfirm, +}: PhotoWallCropModalProps) { + const [crop, setCrop] = useState({ x: 0, y: 0 }); + const [zoom, setZoom] = useState(1); + const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); + const [working, setWorking] = useState(false); + const [err, setErr] = useState(null); + + const onCropComplete = useCallback((_area: Area, pixels: Area) => { + setCroppedAreaPixels(pixels); + }, []); + + const confirm = async () => { + if (!croppedAreaPixels) return; + setWorking(true); + setErr(null); + try { + const file = await getCroppedImageFile( + imageSrc, + croppedAreaPixels, + sourceFileName || "photo.jpg", + "image/jpeg", + ); + onConfirm(file); + } catch (e) { + setErr(e instanceof Error ? e.message : "Could not crop photo"); + } finally { + setWorking(false); + } + }; + + return ( +
+
+
+

Crop your photo

+

+ Adjust to match the wall photo ratio before uploading. +

+
+ +
+ +
+ +
+ + + {err ? ( +

+ {err} +

+ ) : null} + +
+ + +
+
+
+
+ ); +} diff --git a/src/components/PhotoWallEventHeader.tsx b/src/components/PhotoWallEventHeader.tsx new file mode 100644 index 0000000..93022c8 --- /dev/null +++ b/src/components/PhotoWallEventHeader.tsx @@ -0,0 +1,141 @@ +import type { ReactNode } from "react"; +import { Calendar, MapPin } from "lucide-react"; +import type { PhotoWallEventHeaderDisplay, PhotoWallPublicEvent } from "../lib/api"; + +function resolveDisplay(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, + }; +} + +const EVENT_TYPE_LABEL: Record = { + conference: "Conference", + workshop: "Workshop", + summit: "Summit", + cme: "CME", + webinar: "Webinar", +}; + +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"; + return "In person"; +} + +function formatEventDateRange(ev: PhotoWallPublicEvent): string { + const { startDate, endDate } = ev; + if (!startDate) return ""; + const s = new Date(startDate); + const e = endDate ? new Date(endDate) : s; + if (Number.isNaN(s.getTime())) return ""; + const sameDay = !Number.isNaN(e.getTime()) && s.toDateString() === e.toDateString(); + const dOpt: Intl.DateTimeFormatOptions = { month: "short", day: "numeric", year: "numeric" }; + if (sameDay) return s.toLocaleDateString(undefined, { weekday: "short", ...dOpt }); + return `${s.toLocaleDateString(undefined, dOpt)} – ${e.toLocaleDateString(undefined, dOpt)}`; +} + +export function PhotoWallEventHeader({ + event, + display, +}: { + event: PhotoWallPublicEvent; + display?: PhotoWallEventHeaderDisplay | null; +}) { + const d = resolveDisplay(display); + const typeLabel = formatEventType(event.eventType); + const dateLine = formatEventDateRange(event); + const kind = formatLocationKind(event.locationType); + const venue = event.location?.trim() || ""; + const showLoc = + d.showVenue && + !!venue && + (event.locationType === "physical" || event.locationType === "hybrid" || !event.locationType); + + const showBanner = d.showBanner && !!event.imageUrl; + const showLogo = d.showLogo && !!event.logoUrl; + const showTitle = d.showTitle && !!event.name; + const showSubtitle = d.showTitle && !!event.subtitle?.trim(); + + const metaItems: ReactNode[] = []; + if (d.showDateRange && dateLine) { + metaItems.push( + + + {dateLine} + , + ); + } + if (d.showEventType && typeLabel) metaItems.push({typeLabel}); + if (d.showLocationType && kind) metaItems.push({kind}); + if (showLoc) { + metaItems.push( + + + {venue} + , + ); + } + + if (!showBanner && !showLogo && !showTitle && !showSubtitle && metaItems.length === 0) { + return null; + } + + return ( +
+ {showLogo ? ( +
+ {event.name} +
+ ) : null} + {showBanner ? ( +
+ +
+ ) : null} + {showTitle || showSubtitle || metaItems.length > 0 ? ( +
+

Photo Wall

+ {showTitle ? ( +

+ {event.name} +

+ ) : null} + {showSubtitle ? ( +

{event.subtitle}

+ ) : null} + {metaItems.length > 0 ? ( +
+ {metaItems.map((item, index) => ( + + {index > 0 ? ( + + ) : null} + {item} + + ))} +
+ ) : null} +
+ ) : ( +

Photo Wall

+ )} +
+ ); +} diff --git a/src/components/auth/AuthShell.tsx b/src/components/auth/AuthShell.tsx index 0b16e59..7de9c67 100644 --- a/src/components/auth/AuthShell.tsx +++ b/src/components/auth/AuthShell.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from "react"; +import { EventBuzzLogo } from "../EventBuzzLogo"; import { EventBuzzMark } from "../EventBuzzMark"; -import { LegalFooterLinks } from "../marketing/LegalFooterLinks"; type AuthShellProps = { children: ReactNode; @@ -8,9 +8,16 @@ type AuthShellProps = { mainClassName?: string; /** Show EventBuzz white logo pinned to the top of the auth shell */ showLogo?: boolean; + /** Classes for the footer EventBuzz mark (default h-5) */ + footerLogoClassName?: string; }; -export function AuthShell({ children, mainClassName = "", showLogo = true }: AuthShellProps) { +export function AuthShell({ + children, + mainClassName = "", + showLogo = true, + footerLogoClassName = "h-5 max-h-5 min-h-0 w-auto", +}: AuthShellProps) { return (
{showLogo ? ( @@ -23,8 +30,17 @@ export function AuthShell({ children, mainClassName = "", showLogo = true }: Aut > {children}
-