YEG SpookyBooth 🎃
Edmonton Halloween Magic Photo Experience
An interactive, responsive, and kiosk-ready photo experience celebrating Canadian autumn folklore and Edmonton's river valley magic. Built with Next.js, React, TypeScript, and Tailwind CSS.
Users step into the enchanted portal, capture a photo, pick an iconic Canadian/Edmonton Halloween disguise, and receive a transformed 3D cartoon character portrait.
🌟 Key Features
- 🍁 Edmonton & Canadian Halloween Atmosphere:
- Eerie shimmering Northern Lights (Aurora Borealis).
- Silhouettes of the iconic High Level Bridge, Walterdale Arch, glowing Muttart Conservatory Pyramids, and river valley spruce trees.
- Radiant harvest moon, drifting autumn maple leaves, friendly bats, and a cute hovering ghost.
- 🎨 Stylized 3D Cartoon Character Disguises:
- 🌟 Vibe Matcher: Keep your exact face, outfit, hair & makeup completely unchanged, but replace the background with a funny, exaggerated scene that matches your vibe!
- 🎸 Classic Mullet: Keep your photo portrait intact while rocking an iconic 80s mullet hairstyle.
- 🏛️ Muttart Pyramid Witch: Whimsical young witch brewing emerald potions inside the glass conservatory pyramid.
- 🦇 High Level Vampire: Chibi vampire in a winter velvet coat hovering above the foggy bridge.
- 🧙♂️ 100% Magical Lore: Zero robotic AI technical jargon. The entire user experience is written as enchanting spooky spellcraft.
- 🕹️ Tactile Cartoon Arcade UI:
- Chunky 3D buttons (
.btn-cartoon-orange,.btn-cartoon-purple,.btn-cartoon-dark) with satisfying downward click physics. - Bold comic-book outlines and playful cartoon wobbles.
- Chunky 3D buttons (
- 🧭 Complete Navigation:
← Backbuttons across all intermediate screens (Camera, Confirm, Style Selection, and Loading).- Dedicated
Finish 🏁button on the final result screen to reset the booth. - Top navigation bars use a strictly non-overlapping 3-column layout that never covers or blocks buttons.
- 📲 QR Code & Print Ready:
- On-screen QR code allows visitors to scan and instantly save their portrait to their smartphone.
- Integrated 1-click photo printing formatted for 4x6 print media via
@media print.
- 🛡️ Intelligent Content Safety & Zero-Lockout Uptime:
- Automatically identifies and handles image safety filter rejections (e.g.
PROHIBITED_CONTENT, Safe-for-Work checks). - Safety rejections immediately return the user to the camera/photo capture screen with an interactive Spooky Safety Warning modal to snap or choose another photo.
- No API cooldown lockouts: input rejections never penalize or disable the booth for subsequent guests.
- Automatically identifies and handles image safety filter rejections (e.g.
- ⏱️ Kiosk Inactivity Auto-Reset: Automatically resets to the standby welcome screen after
Nseconds of inactivity. - 🏷️ Sokaris Studio Attribution: Built-in footer attribution and copyright link to sokaris.com.
🖥️ Display Modes
1. Standard Mode (Default)
By default, the booth renders in a centered, vertical 9:16 kiosk viewport (max-width: 540px, max-height: 960px) on desktop and automatically expands on mobile screens. Ideal for testing, desktop browsing, and tablets:
http://localhost:3000
2. Fullscreen Kiosk Mode (?mode=fullscreen)
For physical event touchscreens, vertical digital signage, or dedicated kiosk hardware, add ?mode=fullscreen to the URL:
http://localhost:3000/?mode=fullscreen
What this does:
- Expands the viewport to 100% screen width and 100% screen height (
100vwx100vh). - Removes all desktop borders, rounded corners, and margins.
- Automatically triggers native browser fullscreen (
document.documentElement.requestFullscreen()) upon the user's first tap.
🚀 Quick Start
1. Prerequisites
- Node.js 18.17+ or 20+
- npm or yarn
2. Install Dependencies
cmd /c npm install
3. Configure Environment Variables
Copy .env.example to .env.local and add your API credentials:
# Google Gemini API Key (Primary Provider)
GEMINI_API_KEY=your_gemini_api_key_here
# OpenRouter API Key (Automatic Backup Provider)
OPENROUTER_API_KEY=your_openrouter_api_key_here
# Model Selection Override (Optional: "nanobanana 2", "nanobanana", "pro", "lite")
GENERATION_MODEL=nanobanana 2
(If no keys are provided, the app will run gracefully in demo fallback mode using pre-rendered sample portraits).
4. Run Development Server
cmd /c npm run dev
Open http://localhost:3000 (or http://localhost:3000/?mode=fullscreen) in your browser.
5. Build for Production
cmd /c npm run build
cmd /c npm start
⚙️ Configuration Guide
1. General Settings (config/booth.config.json)
All booth behaviors and visuals can be customized without editing code:
{
"appName": "YEG SpookyBooth",
"kioskMode": true,
"theme": {
"backgroundType": "color",
"backgroundColor": "#0f091f",
"primaryColor": "#ff6d00",
"accentColor": "#22c55e",
"surfaceColor": "#1e1136",
"textColor": "#ffffff",
"logoUrl": "/assets/brand/spooky_logo.svg",
"logoWidth": 260
},
"camera": {
"defaultFacingMode": "user",
"aspectRatio": "9:16",
"countdownSeconds": 3,
"allowUpload": true,
"maxUploadSizeMb": 10
},
"features": {
"showQrCode": true,
"showDownloadButton": true,
"showPrintButton": true,
"showRetakeButton": true
},
"limits": {
"maxGenerationsPerDevice": 0,
"cooldownMinutes": 60
},
"kiosk": {
"enableAutoReset": true,
"idleTimeoutSeconds": 90
},
"labels": {
"welcomeTitle": "YEG SPOOKY BOOTH",
"welcomeSubtitle": "Edmonton Halloween Magic Portal",
"welcomeCta": "Tap To Enter If You Dare, Eh! 🎃",
"cameraInstruction": "Strike your most spooktacular pose!",
"cameraSnap": "Snap Spooky Photo",
"cameraUpload": "Upload Photo",
"confirmTitle": "Review Your Spooky Shot",
"styleSelectionTitle": "Pick Your Halloween Disguise",
"styleSelectionSubtitle": "Choose an iconic local legend to transform into",
"processingTitle": "Brewing Your Transformation Spell...",
"processingSubtitle": "Mixing river valley mist, northern starlight & potion sparks...",
"resultTitle": "Your Spooktacular Portrait! 🎃",
"scanQrLabel": "Scan to take your treat home",
"downloadLabel": "Save Portrait",
"printLabel": "Print Photo",
"startOverLabel": "Haunt Again"
}
}
2. Styles & Prompts (config/styles.json)
To add, remove, or modify character disguise presets, edit config/styles.json:
[
{
"id": "yeg_hockey_phantom",
"name": "YEG Hockey Phantom",
"description": "Friendly vintage hockey ghost on river ice under the glowing Northern Lights",
"thumbnail": "/assets/styles/hockey_ghost.jpg",
"prompt": "Vibrant 3D cartoon animated character portrait based on the person in the user's photo... Bubbly cartoon animation style, Hotel Transylvania and Pixar aesthetics...",
"negativePrompt": "scary, bloody, gore, grotesque, deformed hands, bad anatomy, realistic human skin pores, text watermark"
}
]
📁 Project Structure
├── app/
│ ├── api/
│ │ ├── config/route.ts # API endpoint serving booth config & styles
│ │ └── generate/route.ts # AI image transformation endpoint with failover
│ ├── globals.css # Design tokens, cartoon button physics & animations
│ ├── layout.tsx # Root HTML layout & meta tags
│ └── page.tsx # Main state machine routing screens
├── components/
│ ├── KioskFrame.tsx # Kiosk frame, ?mode=fullscreen handler & footer
│ ├── SpookyAtmosphere.tsx # Animated Edmonton skyline, aurora & falling leaves
│ └── screens/
│ ├── StandbyScreen.tsx # Welcome screen with bouncy hero pumpkin
│ ├── CameraScreen.tsx # Camera capture, countdown, and face guide
│ ├── ConfirmScreen.tsx # Photo review with retake / confirm
│ ├── StyleScreen.tsx # 2x2 grid of cartoon character cards
│ ├── LoadingScreen.tsx # Cauldron animation with magical brewing steps
│ ├── ResultScreen.tsx # Confetti celebration, download, QR code & Finish
│ └── RateLimitedScreen.tsx# Rate-limit cooldown screen
├── config/
│ ├── booth.config.json # Main configuration file
│ └── styles.json # Character styles and prompt presets
├── lib/
│ ├── ai/ # Gemini & OpenRouter provider orchestration
│ ├── config.ts # Server-side configuration loader
│ └── ratelimit.ts # Device generation limits & cooldown tracker
└── public/
└── assets/
├── brand/ # Spooky logo SVG & Sokaris Studio logos
└── styles/ # Cartoon character artwork images
🏷️ Credits & License
- Designed and developed for YEG Halloween Photo Experiences.
- Attribution & branding by Sokaris Studio (sokaris.com).