Skip to content

dannycahyo/kiosk

Repository files navigation

Snap & Go Photobooth

A modern web-based photobooth application built with React Router, XState, and Cloudinary.

Features

  • πŸ“Έ 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

Tech Stack

  • 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

Quick Start

1. Install Dependencies

pnpm install

2. Configure Cloudinary

Copy the environment template:

cp .env.template .env

Edit .env with your Cloudinary credentials:

VITE_CLOUDINARY_CLOUD_NAME=your_cloud_name
VITE_CLOUDINARY_UPLOAD_PRESET=your_unsigned_preset

Create an unsigned upload preset:

  1. Go to Cloudinary Console
  2. Settings β†’ Upload β†’ Upload presets
  3. Add upload preset β†’ Set "Signing Mode" to "Unsigned"
  4. Copy the preset name to your .env file

3. Run Development Server

pnpm dev

Visit http://localhost:5173 and allow camera permissions when prompted.

Project Structure

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

How It Works

State Machine Flow

  1. idle - Start screen with camera permission request
  2. countdown - 3-2-1 countdown with live preview
  3. capture - Take photo with flash animation
  4. checkProgress - Check if 3 photos captured
  5. stitching - Combine photos with frame overlay
  6. uploading - Upload to Cloudinary (3 retry attempts)
  7. success - Display photos + QR code
  8. failure - Error handling with retry option

Image Processing

  • 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)

Audio Generation

  • Generates sounds programmatically via Web Audio API
  • No external audio files required
  • 440Hz sine wave for countdown beeps
  • Percussive click for shutter sound

Customization

Frame Design

Edit public/assets/frame.svg or regenerate:

node scripts/generate-frame.js

Current frame includes:

  • 20px white border
  • Top branding area (80px)
  • Bottom branding area (80px)

Auto-Reset Timeout

Modify timeout in app/components/booth/views/SuccessView.tsx:

// Default: 60 seconds
<p>This session will auto-reset in 60 seconds</p>

Upload Retry Logic

Adjust in app/machines/boothMachine.ts:

uploadRetries: number; // Max 3 retries

Building for Production

Create production build:

pnpm build

Output:

build/
β”œβ”€β”€ client/    # Static assets
└── server/    # Server-side code

Deployment

Docker

docker build -t snap-and-go .
docker run -p 3000:3000 snap-and-go

Platforms

Compatible with:

  • Vercel
  • Netlify
  • AWS ECS
  • Google Cloud Run
  • Fly.io
  • Railway
  • Digital Ocean

Note: Ensure environment variables are configured in your deployment platform.

Routes

  • / - Main kiosk interface (desktop-optimized)
  • /photo/:publicId - Mobile retrieval page with download button

Technical Highlights

  • 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)

Troubleshooting

Camera not working

  • Ensure HTTPS in production (required for camera access)
  • Check browser permissions
  • Test on localhost first

Upload failures

  • Verify Cloudinary credentials in .env
  • Ensure upload preset is "Unsigned"
  • Check network connectivity

Photos not stitching

  • Ensure frame.svg exists in public/assets/
  • Check console for errors
  • Verify all 3 photos were captured

Documentation

  • Setup Guide - Detailed configuration
  • PRD - Product requirements
  • TRD - Technical requirements

Built with ❀️ using React Router, XState, and Cloudinary.

About

A modern web-based photobooth application built with React Router, XState, and Cloudinary.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors