diff --git a/FUNCTIONALITY_BRIEF.md b/FUNCTIONALITY_BRIEF.md
new file mode 100644
index 0000000..fbfb827
--- /dev/null
+++ b/FUNCTIONALITY_BRIEF.md
@@ -0,0 +1,189 @@
+# EventBuzz Attendee App — Functionality Brief
+
+This is the **attendee-facing EventBuzz experience**: the phone-friendly website people open at an event (typically from a QR code, invite, or join link). It is not the organiser dashboard. Organisers set up the event elsewhere; attendees use this app to join, follow the programme, take part, and share moments.
+
+---
+
+## In one sentence
+
+Guests join an event with a short code, then use their phone to follow the agenda, ask questions, take quizzes and polls, earn points, give feedback, and create branded photos to share.
+
+---
+
+## Who it is for
+
+**Attendees** use it throughout the event: finding sessions, interacting during talks, and collecting photos afterwards.
+
+**Speakers and hosts** can also use dedicated **on-stage screens** (opened from a special link) so the room can see live questions, poll results, and a photo wall on the projector.
+
+**Organisers** do not run the event from this app, but they control what attendees see: branding, who can register, which sessions are live, and which extras (feedback, ratings, photos, rewards) are switched on.
+
+---
+
+## Getting in
+
+Attendees do not need to hunt for an app store listing. They open a link or scan a QR, then:
+
+1. **Enter a join code** (from an invite, slide, or poster), or land on a link that already includes the code.
+2. See the **event name, logo, dates, and venue**.
+3. Sign in the way the organiser has chosen:
+ - **Open / guest access** — continue with little or no friction (email may still be asked).
+ - **Email one-time code (OTP)** — a code is sent to their inbox.
+ - **Magic link** — a sign-in link is emailed (it expires after a short time).
+ - **Invite only** — they must also enter an invite code.
+
+If registration needs extra details (name, phone, custom questions), they complete a **profile** before entering. Some events require **organiser approval** first; those guests see a waiting screen until they are accepted.
+
+Returning guests who already registered can sign in again even if new registrations have closed.
+
+---
+
+## Event home
+
+Once inside, attendees land on the **event hub**:
+
+- Event name, description, banner image, and organiser branding
+- Dates and location
+- **Full agenda**, grouped by day, with session times and details
+- **My Sessions** — a personal list of talks they have bookmarked
+- Optional **event ratings** (if the organiser has turned that on)
+
+This is the home base they can return to from anywhere in the app.
+
+---
+
+## Sessions and the agenda
+
+Each session has its own page with:
+
+- Title, type (for example symposium or panel), time, duration, and room
+- Option to **bookmark** it for later
+- **Key people** — speakers, moderators, and similar, with photo, title, bio, organisation, and public links (LinkedIn, X, website) when provided
+- **My Sessions** and the **full agenda** again, so people can jump to the next talk without going home
+- Live **Q&A** and **quizzes / polls** for that room (when the organiser has them running)
+
+Organisers can hide individual tabs (key people, my sessions, or agenda) per session if they want a simpler room experience.
+
+**Tracks** (parallel programmes, halls, or streams) have their own landing page when someone joins with a track code.
+
+---
+
+## Live Q&A
+
+During a session, attendees can:
+
+- **Ask a question** from their phone (including anonymously if they are not fully signed in)
+- See questions from the rest of the room as they come in
+- **Upvote or downvote** questions so popular ones rise
+
+On the **presenter screen**, shortlisted questions can be shown to the room so the speaker can answer what the audience actually cares about.
+
+---
+
+## Quizzes, polls, and surveys
+
+Organisers can push live activities into a session. Attendees are taken into them (including automatically when something goes live) and can:
+
+- Answer **quizzes** (scored, with right/wrong answers)
+- Vote in **polls**
+- Complete **surveys** (including text answers and ratings)
+
+Question types include single choice, multiple choice, free text, and star ratings. Quizzes can run on a **timer**. After submitting, people may see their own answers, the correct answers (when the host allows it), and live result summaries.
+
+**On-stage presentation screens** let the host:
+
+- Put a question on air
+- Show live response charts
+- See who has answered
+- Show a **quiz leaderboard**
+- Reveal the correct answer when they choose
+- Switch between Q&A and quiz/poll views for the same session
+
+---
+
+## Rewards and leaderboard
+
+Participation can earn **points**. Typical ways to earn them include registering, asking or voting on questions, taking quizzes or polls, and submitting feedback or ratings. Organisers can also adjust points manually.
+
+Attendees can:
+
+- See their **total points** and a history of how they earned them
+- Open a **leaderboard** of top participants
+- See **their own rank** among the group
+
+A celebration can appear when points are awarded, so the reward feels immediate.
+
+---
+
+## Feedback and ratings
+
+When the organiser enables them, attendees can fill in **custom forms**:
+
+- **Feedback** — comments and structured questions about the event or a session
+- **Ratings** — typically star ratings and similar scores, including an overall event rating on the event home
+
+Forms are built by the organiser (required fields, multiple choice, checkboxes, stars, and so on). Submission is a simple one-screen flow on the phone.
+
+---
+
+## Photo experiences
+
+These are usually opened from a dedicated QR or link at a booth or after a session. They are designed for sharing on social media.
+
+### Photo wall
+
+Attendees take or upload a selfie (cropping to the right shape if needed). The photo can be placed in an event **frame**, and the organiser can ask for a name and extra fields. Photos may wait for **approval** before they appear.
+
+A **live photo wall** on a large screen shows approved photos as they come in.
+
+People can **download** their framed photo and **share** it to LinkedIn, Facebook, Instagram, Snapchat, X, WhatsApp, or the phone’s native share sheet — whichever the organiser has allowed.
+
+### Find my photos
+
+Attendees upload a selfie and the app **finds photos of them** from the event. Matches can be downloaded and shared the same way.
+
+### Social share (branded frames)
+
+Guests pick an official **event frame**, add their photo, crop it, and walk away with a ready-to-post image.
+
+### AI avatar
+
+Guests pick a themed avatar template, upload a selfie, and the app generates a **stylised portrait** of them in that look. They can download and share the result.
+
+---
+
+## Moving around the app
+
+A floating **quick menu** (on session and related screens) jumps to:
+
+- Event home
+- Rewards
+- Leaderboard
+- Feedback and ratings (when those are on for the session)
+- All questions
+
+The header shows the **event logo**, a **points total**, and sign-out. The layout is built for a phone in the hand at a venue.
+
+---
+
+## Branding and trust
+
+Each event can show its own **logo, banner, name, and colours**. Legal pages for **privacy** and **terms of service** are available from the marketing/legal area of the site.
+
+---
+
+## Typical attendee journey
+
+1. Scan a poster QR or type a code from a slide.
+2. Sign in (or continue as a guest) and complete a profile if asked.
+3. Browse the agenda, bookmark sessions, and open the current talk.
+4. Ask questions, vote, and take a live poll or quiz.
+5. Earn points and check the leaderboard.
+6. Rate the session or send feedback.
+7. At a photo booth or from a follow-up link, create a framed photo, find pictures of themselves, or generate an AI avatar — then share it.
+
+---
+
+## What this app is not
+
+It is **not** the organiser control panel (creating events, building forms, managing attendees). It is **not** a check-in / badge-scanning tool. Those live in other EventBuzz products. This app is what **guests and the room** use during and around the event.
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 6ef182e..6fbb63b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -18,6 +18,11 @@ 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 { PhotoSearchPage } from "./pages/PhotoSearchPage";
+import { SocialSharePage } from "./pages/SocialSharePage";
+import { AiAvatarPage } from "./pages/AiAvatarPage";
import { CodeEntryPage } from "./pages/CodeEntryPage";
import { PrivacyPolicyPage } from "./pages/PrivacyPolicyPage";
import { TermsOfServicePage } from "./pages/TermsOfServicePage";
@@ -45,6 +50,11 @@ 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.
+