A modern web-based photobooth application built with React Router, XState, and Cloudinary.
- πΈ 3-Photo Capture Sequence - Takes 3 photos with countdown timer
- π¨ Instant Photo Strip - Stitches photos into a branded vertical strip (1080x1920)
- π± QR Code Retrieval - Generate QR codes for mobile download
- π Audio Feedback - Countdown beeps and shutter sounds
- π₯ Live Camera Preview - Mirrored webcam preview during capture
- βοΈ Cloudinary Integration - Unsigned uploads for serverless architecture
- β»οΈ Auto-Reset - Returns to idle after 60 seconds
- π Error Recovery - Retry logic for failed uploads
- React Router 7 - File-based routing with SSR
- XState v5 - Finite state machine for booth flow
- TypeScript - Type-safe development
- Tailwind CSS - Utility-first styling
- react-webcam - Camera integration
- Canvas API - Client-side image stitching
- Cloudinary - Image hosting and delivery
- ts-pattern - Pattern matching for state rendering
pnpm installCopy the environment template:
cp .env.template .envEdit .env with your Cloudinary credentials:
VITE_CLOUDINARY_CLOUD_NAME=your_cloud_name
VITE_CLOUDINARY_UPLOAD_PRESET=your_unsigned_presetCreate an unsigned upload preset:
- Go to Cloudinary Console
- Settings β Upload β Upload presets
- Add upload preset β Set "Signing Mode" to "Unsigned"
- Copy the preset name to your
.envfile
pnpm devVisit http://localhost:5173 and allow camera permissions when prompted.
app/
βββ machines/
β βββ boothMachine.ts # XState FSM (7 states)
βββ lib/
β βββ canvas-stitcher.ts # Image processing
β βββ cloudinary-upload.ts # Upload service
β βββ audio-utils.ts # Audio generation
β βββ utils.ts # Utilities
βββ components/
β βββ booth/
β β βββ BoothContainer.tsx # Main controller
β β βββ views/ # State-specific views
β β βββ IdleView.tsx
β β βββ CountdownView.tsx
β β βββ CaptureView.tsx
β β βββ ProcessingView.tsx
β β βββ SuccessView.tsx
β β βββ FailureView.tsx
β βββ ui/
β βββ button.tsx # shadcn/ui button
βββ routes/
βββ _index.tsx # Kiosk route (/)
βββ photo.$id.tsx # Retrieval route (/photo/:id)
public/
βββ assets/
βββ frame.svg # Photo strip overlay
scripts/
βββ generate-frame.js # Frame generation utility
- idle - Start screen with camera permission request
- countdown - 3-2-1 countdown with live preview
- capture - Take photo with flash animation
- checkProgress - Check if 3 photos captured
- stitching - Combine photos with frame overlay
- uploading - Upload to Cloudinary (3 retry attempts)
- success - Display photos + QR code
- failure - Error handling with retry option
- Captures 3 photos as base64 data URLs
- Center-crops photos to maintain aspect ratio
- Stitches into 1080Γ1920px portrait strip
- Overlays SVG frame with white borders
- Exports as JPEG (90% quality)
- Generates sounds programmatically via Web Audio API
- No external audio files required
- 440Hz sine wave for countdown beeps
- Percussive click for shutter sound
Edit public/assets/frame.svg or regenerate:
node scripts/generate-frame.jsCurrent frame includes:
- 20px white border
- Top branding area (80px)
- Bottom branding area (80px)
Modify timeout in app/components/booth/views/SuccessView.tsx:
// Default: 60 seconds
<p>This session will auto-reset in 60 seconds</p>Adjust in app/machines/boothMachine.ts:
uploadRetries: number; // Max 3 retriesCreate production build:
pnpm buildOutput:
build/
βββ client/ # Static assets
βββ server/ # Server-side code
docker build -t snap-and-go .
docker run -p 3000:3000 snap-and-goCompatible with:
- Vercel
- Netlify
- AWS ECS
- Google Cloud Run
- Fly.io
- Railway
- Digital Ocean
Note: Ensure environment variables are configured in your deployment platform.
/- Main kiosk interface (desktop-optimized)/photo/:publicId- Mobile retrieval page with download button
- No backend required - Uses Cloudinary unsigned uploads
- Client-side processing - All image stitching done in browser
- Pattern matching - Clean state rendering with ts-pattern
- Type-safe - Full TypeScript coverage
- Responsive - Desktop kiosk + mobile retrieval views
- Privacy-first - No PII storage, images auto-delete (configure in Cloudinary)
- Ensure HTTPS in production (required for camera access)
- Check browser permissions
- Test on localhost first
- Verify Cloudinary credentials in
.env - Ensure upload preset is "Unsigned"
- Check network connectivity
- Ensure
frame.svgexists inpublic/assets/ - Check console for errors
- Verify all 3 photos were captured
- Setup Guide - Detailed configuration
- PRD - Product requirements
- TRD - Technical requirements
Built with β€οΈ using React Router, XState, and Cloudinary.