An open API service indexing awesome lists of open source software.

https://github.com/disane87/tap-and-tell

Tap & Tell is a modern, NFC-powered digital guestbook that transforms how guests leave their mark at events. Guests tap their phone on an NFC tag (or scan a QR code), and a beautiful multi-step wizard guides them through leaving their name, photo, and a personal message. No app install required! πŸ“±βœ¨
https://github.com/disane87/tap-and-tell

Last synced: 4 months ago
JSON representation

Tap & Tell is a modern, NFC-powered digital guestbook that transforms how guests leave their mark at events. Guests tap their phone on an NFC tag (or scan a QR code), and a beautiful multi-step wizard guides them through leaving their name, photo, and a personal message. No app install required! πŸ“±βœ¨

Awesome Lists containing this project

README

          

[![Nuxt](https://img.shields.io/badge/Nuxt-4.3-00DC82?logo=nuxt.js&logoColor=white)](https://nuxt.com/)
[![Vue](https://img.shields.io/badge/Vue-3.5-4FC08D?logo=vue.js&logoColor=white)](https://vuejs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-v4-38B2AC?logo=tailwind-css&logoColor=white)](https://tailwindcss.com/)
[![pnpm](https://img.shields.io/badge/pnpm-package_manager-F69220?logo=pnpm&logoColor=white)](https://pnpm.io/)
[![License](https://img.shields.io/github/license/Disane87/tap-and-tell)](LICENSE)
[![GitHub issues](https://img.shields.io/github/issues/Disane87/tap-and-tell?color=red)](https://github.com/Disane87/tap-and-tell/issues)
# 🎯 Tap & Tell β€” NFC-Enabled Digital Guestbook

Hey there! πŸ‘‹ **Tap & Tell** is a modern, NFC-powered digital guestbook that transforms how guests leave their mark at events. Guests tap their phone on an NFC tag (or scan a QR code), and a beautiful multi-step wizard guides them through leaving their name, photo, and a personal message. No app install required! πŸ“±βœ¨

> Perfect for weddings πŸ’, birthday parties πŸŽ‚, corporate events 🏒, or any gathering where you want to capture memories digitally!

> [!NOTE]
> **πŸ€– AI-Aided Development (AIAD)**
>
> This project openly uses AI-assisted development (e.g. Claude Code) to accelerate workflows, improve code quality, and gain more development momentum. All AI-generated code is reviewed and approved by humans β€” this is not a vibe-coding project, but a deliberate effort to build a useful product while exploring the boundaries, benefits, and trade-offs of AI-aided development.

---

✨ What Can This Thing Do?

Glad you asked! Here's the good stuff:

- πŸ“± **NFC & QR Code Entry** β€” Guests tap an NFC tag or scan a QR code to open the guestbook instantly β€” no app download needed!
- πŸ§™ **Multi-Step Wizard** β€” A beautiful 4-step form guides guests through leaving their entry (Basics β†’ Favorites β†’ Fun Facts β†’ Message)
- πŸ“Έ **Photo Upload with Compression** β€” Guests snap a selfie or upload a photo, automatically compressed client-side for fast uploads
- 🎨 **Polaroid-Style Cards** β€” Entries are displayed as gorgeous polaroid-style cards with handwritten fonts
- πŸŒ™ **Dark Mode** β€” Full light/dark/system theme support with zero flash of unstyled content (FOUC)
- 🌍 **Multilingual** β€” English and German out of the box with `@nuxtjs/i18n`
- πŸ–₯️ **Slideshow Mode** β€” Full-screen auto-advancing slideshow, perfect for displaying on a TV at your event
- πŸ“„ **PDF Export** β€” Download your entire guestbook as a beautifully formatted PDF
- πŸ” **Admin Dashboard** β€” Password-protected admin panel for entry moderation (approve, reject, delete)
- πŸ“Š **Entry Moderation** β€” Three-state system: pending β†’ approved / rejected β€” keep your guestbook clean!
- πŸ”„ **Offline Support** β€” Entries are queued in IndexedDB when offline and synced when back online
- πŸ“± **PWA Ready** β€” Install as a Progressive Web App on any device
- 🐳 **Docker Support** β€” Ready-to-use Dockerfile and docker-compose for easy self-hosting

---

# πŸ“± How It Works

The magic is simple β€” here's the flow:

```
1. πŸ“² Guest taps NFC tag or scans QR code
↓
2. 🌐 Browser opens Tap & Tell (no app install!)
↓
3. πŸ§™ 4-step wizard collects:
Step 1: Name + Photo (required)
Step 2: Favorites β€” color, food, movie, song, video (optional)
Step 3: Fun Facts β€” superpowers, hidden talents, preferences (optional)
Step 4: Personal Message (required)
↓
4. πŸ’Ύ Entry saved with photo compression
↓
5. πŸŽ‰ Entry appears in the guestbook!
```

> [!NOTE]
> πŸ“ Steps 1 (Basics) and 4 (Message) are required. Steps 2 (Favorites) and 3 (Fun Facts) are completely optional β€” guests can skip them!

---

# πŸš€ Getting Started

Ready to set up your own digital guestbook? Let's go! πŸŽ‰

## Prerequisites

- **Node.js** 18+ installed
- **pnpm** package manager (`npm install -g pnpm`)

## Quick Start

```bash
# 1. Clone the repo
git clone https://github.com/Disane87/tap-and-tell.git
cd tap-and-tell

# 2. Install dependencies
pnpm install

# 3. Start the dev server
pnpm dev
```

That's it! Open `http://localhost:3000` and you're running! πŸŽ‰

## Environment Variables

Create a `.env` file in the project root:

```env
# PostgreSQL connection string
POSTGRES_URL=postgresql://user:password@localhost:5432/tapandtell

# JWT signing secret (CHANGE THIS in production!)
JWT_SECRET=your-jwt-secret-here

# CSRF token signing secret (CHANGE THIS in production!)
CSRF_SECRET=your-csrf-secret-here

# Master encryption key for photo encryption (64 hex chars, REQUIRED in production!)
ENCRYPTION_MASTER_KEY=

# Storage directory for entries and photos
DATA_DIR=.data
```

> [!CAUTION]
> ⚠️ **Security First!** Always set secure values for `JWT_SECRET`, `CSRF_SECRET`, and `ENCRYPTION_MASTER_KEY` in production!

---

# 🐳 Docker Deployment

Prefer containers? We've got you covered!

### Docker Compose β€” Production (Recommended)

The `docker-compose.prod.yml` is a self-contained stack (app + PostgreSQL) ready for **Portainer** or any Docker host.

**1. Generate secrets**

All secrets must be set before first start. Generate them with `openssl`:

```bash
# JWT signing secret (64-char hex)
openssl rand -hex 32

# CSRF token secret (64-char hex)
openssl rand -hex 32

# Photo encryption master key (64-char hex)
openssl rand -hex 32

# PostgreSQL password (64-char hex)
openssl rand -hex 32

# API token secret (base64)
openssl rand -base64 32
```

**2. Configure environment variables**

Set the generated values in `docker-compose.prod.yml` or pass them as environment variables:

| Variable | Format | Description |
|---|---|---|
| `POSTGRES_PASSWORD` | 64-char hex | PostgreSQL password (same in `postgres` and `app` services) |
| `JWT_SECRET` | 64-char hex | JWT signing key for authentication |
| `CSRF_SECRET` | 64-char hex | CSRF double-submit cookie secret |
| `ENCRYPTION_MASTER_KEY` | 64-char hex | AES-256-GCM photo encryption key |
| `TOKEN_SECRET` | base64 string | API token signing secret |
| `DB_SSL` | `"false"` | Set to `"false"` for Docker-to-Docker connections (no SSL). Omit for external DBs (Neon, Supabase) where SSL is required. |

> [!CAUTION]
> Never commit secrets to version control. Use environment variables, Docker secrets, or Portainer's environment variable UI instead.

**3. Deploy**

```bash
# Direct Docker Compose
docker compose -f docker-compose.prod.yml up -d
```

Or in **Portainer**: Stacks β†’ Add Stack β†’ paste the compose file β†’ set environment variables in the UI.

### Docker Compose β€” Development

```bash
docker compose up -d
```

### Standalone Docker

```bash
# Build the image
docker build -t tap-and-tell .

# Run the container
docker run -d \
-p 3000:3000 \
-e POSTGRES_URL=postgresql://user:password@host:5432/tapandtell \
-e DB_SSL=false \
-e JWT_SECRET=$(openssl rand -hex 32) \
-e CSRF_SECRET=$(openssl rand -hex 32) \
-e ENCRYPTION_MASTER_KEY=$(openssl rand -hex 32) \
-e TOKEN_SECRET=$(openssl rand -base64 32) \
-v tap-and-tell-data:/app/data \
tap-and-tell
```

> [!IMPORTANT]
> πŸ“‚ Mount a volume to `/app/data` to persist your guestbook entries and photos across container restarts!

---

# πŸ“– Pages & Features

Here's a tour of everything Tap & Tell offers:

## 🏠 Landing Page (`/`)

The main entry point for guests! Features:
- 🎠 **Swipeable Carousel** β€” Intro slide followed by existing entry slides
- πŸ“ **Bottom Sheet Wizard** β€” The 4-step form slides up from the bottom
- ⌨️ **Keyboard & Swipe Navigation** β€” Navigate entries with arrow keys or swipe gestures
- πŸ“± **NFC Context Detection** β€” Personalized welcome when entering via NFC tag
- πŸ”΅ **Pagination Dots** β€” Visual indicators for carousel position

## πŸ“– Guestbook (`/guestbook`)

Browse all approved entries in a beautiful grid:
- πŸ” **Search by Name** β€” Debounced search (300ms) for instant filtering
- πŸ”„ **Sort Options** β€” Newest first or oldest first
- πŸ“„ **PDF Export** β€” Download the entire guestbook as a formatted PDF
- πŸ–₯️ **Slideshow Link** β€” Quick access to slideshow mode
- πŸ‘† **Detail View** β€” Click any card to see the full entry in a bottom sheet

## πŸ–₯️ Slideshow (`/slideshow`)

Perfect for displaying on a TV at your event!
- ▢️ **Auto-Advancing** β€” Configurable interval (3–30 seconds, default 8)
- ⏸️ **Play/Pause Controls** β€” Take control when you want
- πŸ–₯️ **Fullscreen Mode** β€” True fullscreen for maximum impact
- ⌨️ **Keyboard Controls** β€” Arrow keys, Space, P (pause), F (fullscreen), ESC (exit)
- πŸ‘» **Auto-Hide Controls** β€” Controls fade away during playback

## πŸ” Admin Dashboard (`/admin`)

Manage your guestbook with a password-protected admin panel:
- πŸ“Š **Status Tabs** β€” Filter by All, Pending, Approved, Rejected
- βœ… **Bulk Actions** β€” Approve or reject multiple entries at once
- πŸ—‘οΈ **Individual Management** β€” Delete or change status of single entries
- πŸ”’ **Entry Counts** β€” See counts per status at a glance
- πŸšͺ **Secure Logout** β€” Token-based session management

## πŸ“± QR Code Generator (`/admin/qr`)

Generate QR codes for your event:
- 🎯 **Custom Event Name** β€” Embed your event name in the URL
- πŸ“₯ **Download Options** β€” Export as PNG or SVG
- πŸ“‹ **Copy URL** β€” Quick copy to clipboard
- πŸ”— **NFC-Compatible URLs** β€” Generates `?source=nfc&event=YourEvent` links

---

# πŸ—οΈ Architecture

Let's peek under the hood! Here's how Tap & Tell is built:

## Tech Stack

| Layer | Technology |
|-------|-----------|
| **Framework** | Nuxt 4.3 (SSR disabled β€” client-side SPA) |
| **UI Library** | Vue 3.5 with Composition API |
| **Styling** | Tailwind CSS v4 via `@tailwindcss/vite` |
| **Components** | shadcn-vue (headless UI) |
| **Language** | TypeScript 5.9 |
| **Icons** | Lucide Vue Next |
| **Database** | PostgreSQL 16+ with Row-Level Security (RLS) |
| **ORM** | Drizzle ORM |
| **Auth** | JWT (jose) + 2FA (TOTP / Email OTP) |
| **Encryption** | AES-256-GCM per-tenant photo encryption |
| **i18n** | @nuxtjs/i18n (EN + DE) |
| **PDF Generation** | jsPDF |
| **QR Codes** | qrcode |
| **Utilities** | VueUse |
| **Toasts** | vue-sonner |
| **PWA** | @vite-pwa/nuxt |
| **Package Manager** | pnpm |
| **Deployment** | Docker (self-hosted) |

## Project Structure

```
tap-and-tell/
β”œβ”€β”€ app/ # πŸ–₯️ Nuxt client application
β”‚ β”œβ”€β”€ pages/ # Route pages
β”‚ β”œβ”€β”€ components/ # Vue components
β”‚ β”‚ β”œβ”€β”€ form/ # Wizard form steps
β”‚ β”‚ └── ui/ # shadcn-vue base components
β”‚ β”œβ”€β”€ composables/ # Vue composables (state & logic)
β”‚ β”œβ”€β”€ types/ # TypeScript type definitions
β”‚ β”œβ”€β”€ plugins/ # Nuxt client plugins
β”‚ β”œβ”€β”€ layouts/ # Page layouts
β”‚ β”œβ”€β”€ lib/ # Utility functions
β”‚ └── assets/ # Static assets (CSS, images)
β”‚
β”œβ”€β”€ server/ # βš™οΈ Nitro server
β”‚ β”œβ”€β”€ routes/api/ # API endpoints
β”‚ β”‚ β”œβ”€β”€ g/ # Public guest endpoints (flat routes)
β”‚ β”‚ β”œβ”€β”€ auth/ # Authentication + 2FA
β”‚ β”‚ β”œβ”€β”€ tenants/ # Tenant/guestbook management
β”‚ β”‚ └── photos/ # Photo serving (encrypted)
β”‚ β”œβ”€β”€ database/ # Schema + migrations (Drizzle ORM)
β”‚ β”œβ”€β”€ utils/ # Server utilities (crypto, auth, storage)
β”‚ └── plugins/ # Server startup plugins
β”‚
β”œβ”€β”€ i18n/ # 🌍 Internationalization
β”‚ └── locales/ # EN + DE translation files
β”‚
β”œβ”€β”€ public/ # πŸ“‚ Static public assets
β”‚ └── icons/ # PWA icons
β”‚
β”œβ”€β”€ plans/ # πŸ“‹ Development plan documents
β”œβ”€β”€ nuxt.config.ts # βš™οΈ Nuxt configuration
β”œβ”€β”€ package.json # πŸ“¦ Dependencies
└── tsconfig.json # πŸ”§ TypeScript config
```

## Storage Layer

Tap & Tell uses **PostgreSQL 16+** with Row-Level Security (RLS) for multi-tenant data isolation. Photos are stored on disk with **AES-256-GCM per-tenant encryption**.

```
PostgreSQL (via Drizzle ORM)
β”œβ”€β”€ users, sessions, user_two_factor # Auth & 2FA
β”œβ”€β”€ tenants, tenant_members # Multi-tenancy
β”œβ”€β”€ guestbooks, entries # Core data (RLS-protected)
└── audit_logs, api_apps, api_tokens # Security & API access

.data/photos/
β”œβ”€β”€ [guestbookId]/[entryId].[ext] # AES-256-GCM encrypted photos
└── ...
```

> [!NOTE]
> Photo storage is configurable via `STORAGE_DRIVER` (`local`, `vercel-blob`, or `s3`) and `DATA_DIR` (default: `.data/`).

---

# πŸ”Œ API Reference

All API endpoints at a glance. Authenticated endpoints use HTTP-only JWT cookies with CSRF protection.

## Public β€” Guest Endpoints (No Auth)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/g/[id]/info` | Guestbook info (name, settings, type) |
| `GET` | `/api/g/[id]/entries` | Approved entries for a guestbook |
| `POST` | `/api/g/[id]/entries` | Create a new guest entry (rate-limited) |
| `GET` | `/api/photos/[tenantId]/[filename]` | Serve encrypted photo |
| `GET` | `/api/health` | Health check endpoint |
| `GET` | `/api/og` | Locale-aware OG image (`?lang=de\|en`) |

## Authentication & Profile

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/auth/login` | Login with email/password, set JWT cookies |
| `POST` | `/api/auth/register` | Register a new account |
| `POST` | `/api/auth/logout` | Clear auth cookies |
| `POST` | `/api/auth/refresh` | Refresh access token |
| `GET` | `/api/auth/me` | Get current user profile |
| `PUT` | `/api/auth/me` | Update name and/or email |
| `DELETE` | `/api/auth/me` | Delete account (requires password) |
| `PUT` | `/api/auth/password` | Change password |
| `GET` | `/api/auth/csrf` | Get CSRF token |
| `POST` | `/api/auth/avatar` | Upload avatar (multipart, max 5 MB) |
| `DELETE` | `/api/auth/avatar` | Delete avatar |
| `GET` | `/api/auth/avatar/[userId]` | Serve avatar image (public) |

## Two-Factor Authentication (2FA)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/auth/2fa/setup` | Start 2FA setup (returns QR code) |
| `POST` | `/api/auth/2fa/verify-setup` | Verify TOTP code to activate 2FA |
| `GET` | `/api/auth/2fa/status` | Check if 2FA is enabled |
| `POST` | `/api/auth/2fa/verify` | Verify 2FA code during login |
| `POST` | `/api/auth/2fa/disable` | Disable 2FA |
| `POST` | `/api/auth/2fa/resend` | Resend email OTP code |

## Tenant Management (JWT Required)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/tenants` | List user's tenants |
| `POST` | `/api/tenants` | Create a new tenant |
| `GET` | `/api/tenants/[uuid]` | Get tenant details |
| `PUT` | `/api/tenants/[uuid]` | Update tenant settings |
| `DELETE` | `/api/tenants/[uuid]` | Delete tenant |
| `POST` | `/api/tenants/[uuid]/rotate-key` | Rotate encryption key |

## Guestbook Management (JWT Required)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/tenants/[uuid]/guestbooks` | List guestbooks with entry counts |
| `POST` | `/api/tenants/[uuid]/guestbooks` | Create a new guestbook |
| `GET` | `/api/tenants/[uuid]/guestbooks/[gbUuid]` | Get guestbook details |
| `PUT` | `/api/tenants/[uuid]/guestbooks/[gbUuid]` | Update guestbook settings |
| `DELETE` | `/api/tenants/[uuid]/guestbooks/[gbUuid]` | Delete guestbook (cascades entries) |
| `POST` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/header` | Upload header image |
| `DELETE` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/header` | Delete header image |
| `POST` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/background` | Upload background image |
| `DELETE` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/background` | Delete background image |

## Entry Moderation (JWT Required)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/entries` | All entries (admin view) |
| `PATCH` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/entries/[id]` | Update entry status |
| `DELETE` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/entries/[id]` | Delete an entry |
| `POST` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/entries/bulk` | Bulk status update |

## Team Members (JWT Required)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/tenants/[uuid]/members` | List team members |
| `POST` | `/api/tenants/[uuid]/members/invite` | Invite team member |
| `GET` | `/api/tenants/[uuid]/members/invites` | List pending invites |
| `DELETE` | `/api/tenants/[uuid]/members/invites/[id]` | Cancel invite |
| `DELETE` | `/api/tenants/[uuid]/members/[userId]` | Remove team member |
| `GET` | `/api/invites/[token]` | Get invite details (public) |
| `POST` | `/api/invites/accept` | Accept team invite (public) |

## API Apps & Tokens (JWT Required)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/tenants/[uuid]/apps` | List API apps |
| `POST` | `/api/tenants/[uuid]/apps` | Create API app |
| `GET` | `/api/tenants/[uuid]/apps/[appId]` | Get API app details |
| `PUT` | `/api/tenants/[uuid]/apps/[appId]` | Update API app |
| `DELETE` | `/api/tenants/[uuid]/apps/[appId]` | Delete API app |
| `GET` | `/api/tenants/[uuid]/apps/[appId]/tokens` | List tokens |
| `POST` | `/api/tenants/[uuid]/apps/[appId]/tokens` | Create token |
| `DELETE` | `/api/tenants/[uuid]/apps/[appId]/tokens/[tokenId]` | Revoke token |
| `GET` | `/api/scopes` | List available API scopes |

## Analytics (JWT Required)

| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/analytics/events` | Track analytics event |
| `GET` | `/api/tenants/[uuid]/analytics/overview` | Dashboard overview |
| `GET` | `/api/tenants/[uuid]/analytics/traffic` | Traffic analytics |
| `GET` | `/api/tenants/[uuid]/analytics/sources` | Traffic sources |
| `GET` | `/api/tenants/[uuid]/analytics/devices` | Device breakdown |
| `GET` | `/api/tenants/[uuid]/analytics/funnel` | Conversion funnel |

### Quick Start Example

```bash
# Login
curl -X POST /api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "your-password"}'
# β†’ Sets access_token and refresh_token cookies

# Create a guest entry (public, no auth)
curl -X POST /api/g/your-guestbook-id/entries \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Doe",
"message": "What an amazing event!",
"photo": "data:image/jpeg;base64,..."
}'
```

---

# 🧩 Composables

The brain of Tap & Tell lives in these composables β€” each one handles a specific concern:

| Composable | What It Does |
|-----------|-------------|
| `useAuth()` | πŸ” JWT cookie-based authentication (login, register, logout, profile) |
| `useGuests()` | πŸ“‹ CRUD operations for guestbook entries. Module-level shared state across the app |
| `useGuestForm()` | πŸ§™ 4-step wizard state management with per-step validation |
| `useGuestbook()` | πŸ“– Public guestbook operations using flat `/api/g/[id]` endpoints |
| `useTenantAdmin()` | πŸ‘” Admin entry operations (fetch all, delete, update status, bulk) |
| `useTheme()` | πŸŒ™ Light/dark/system theme with localStorage persistence & FOUC prevention |
| `useNfc()` | πŸ“± Detects NFC context from URL query params (`?source=nfc&event=...`) |
| `useSlideshow()` | πŸ–₯️ Auto-advancing slideshow with play/pause/fullscreen controls |
| `useEntryFilters()` | πŸ” Debounced search & sort for the guestbook page |
| `usePdfExport()` | πŸ“„ Multi-page PDF generation with photos, favorites, and fun facts |
| `useImageCompression()` | πŸ“Έ Client-side image compression (max 1920px, target 500KB) |
| `useOfflineQueue()` | πŸ”„ IndexedDB-based offline entry queuing with auto-sync |

---

# 🎨 Theme System

Tap & Tell features a **3-layer theme initialization** to prevent any flash of unstyled content (FOUC):

```
Layer 1: Inline in <head>
└─ Runs BEFORE first paint
└─ Reads localStorage, applies `dark` class to <html>
└─ Zero visual flash! ⚑

Layer 2: Client Plugin (theme.client.ts)
└─ Syncs reactive Vue state with DOM
└─ Listens for system preference changes

Layer 3: <ClientOnly> Wrapper
└─ ThemeToggle component only renders on client
└─ Prevents SSR hydration mismatches
```

Toggle between **Light** β˜€οΈ, **Dark** πŸŒ™, and **System** πŸ’» modes with a single click.

---

# 🌍 Internationalization

All user-facing text is translatable β€” no hardcoded strings anywhere!

| Feature | Details |
|---------|---------|
| **Languages** | πŸ‡¬πŸ‡§ English (default) + πŸ‡©πŸ‡ͺ Deutsch |
| **Strategy** | No URL prefix, browser detection |
| **Persistence** | Cookie `i18n_locale` |
| **Module** | `@nuxtjs/i18n` |

Translation files live in `i18n/locales/` covering all scopes: form, guestbook, admin, navigation, slideshow, toasts, and more.

---

# πŸ“± NFC & QR Code Setup

Setting up NFC tags or QR codes for your event is easy!

## NFC Tags

1. Get writable NFC tags (NTAG215 or similar)
2. Use any NFC writer app to write the URL:
```
https://your-domain.com/?source=nfc&event=YourEventName
```
3. Place tags at your event venue β€” guests tap and they're in! πŸ“²

## QR Codes

1. Go to `/admin/qr` in your admin panel
2. Enter your event name
3. Download as PNG or SVG
4. Print and display at your venue! πŸ–¨οΈ

> [!TIP]
> πŸ’‘ **Pro tip**: Use both NFC tags AND QR codes! NFC for quick access, QR as a fallback for phones without NFC support.

---

# πŸ“„ Data Model

Here's what a guest entry looks like under the hood:

```typescript
interface GuestEntry {
id: string // UUID
name: string // Guest's name
message: string // Personal message
photoUrl?: string // Photo path (e.g., /api/photos/{id}.jpg)
answers?: GuestAnswers // Optional form answers
createdAt: string // ISO 8601 timestamp
status?: EntryStatus // 'pending' | 'approved' | 'rejected'
rejectionReason?: string // Why entry was rejected
}

interface GuestAnswers {
// 🎨 Favorites
favoriteColor?: string
favoriteFood?: string
favoriteMovie?: string
favoriteSong?: { title: string; artist?: string; url?: string }
favoriteVideo?: { title: string; url?: string }

// 🎭 Fun Facts
superpower?: string
hiddenTalent?: string
desertIslandItems?: string
coffeeOrTea?: 'coffee' | 'tea'
nightOwlOrEarlyBird?: 'night_owl' | 'early_bird'
beachOrMountains?: 'beach' | 'mountains'

// πŸ“– Our Story
howWeMet?: string
bestMemory?: string
}
```

---

<details>
<summary><h2>⚑ Advanced Configuration</h2></summary>

### Image Compression Settings

Client-side image compression is applied automatically before upload:

| Setting | Value |
|---------|-------|
| Max dimension | 1920px |
| Target file size | 500KB |
| Initial JPEG quality | 0.8 |
| Minimum JPEG quality | 0.3 (adaptive) |

### PWA Configuration

Tap & Tell is a fully-configured Progressive Web App:

| Setting | Value |
|---------|-------|
| Display mode | Standalone |
| Orientation | Portrait |
| Theme color | Dark |
| Icon | SVG (any size, maskable) |
| Font caching | Google Fonts (1-year CacheFirst) |
| Offline | Navigate fallback to `/` |

### Server-Side Validation

Entries are validated server-side with these constraints:

| Field | Constraint |
|-------|-----------|
| `name` | Required, 1–100 characters |
| `message` | Required, 1–1000 characters |
| `photo` | Optional, max 7MB (base64) |

### Auth Token Details

| Property | Value |
|----------|-------|
| Algorithm | HS256 (JWT via `jose`) |
| Access Token | 15 minutes, HTTP-only cookie |
| Refresh Token | 7 days, HTTP-only cookie, stored in DB |
| CSRF | Double-submit cookie pattern |
| 2FA | TOTP (RFC 6238) + Email OTP |

</details>

---

# πŸ› οΈ Development

Want to contribute or customize? Here's how to get the development environment running:

## Commands

```bash
pnpm install # Install dependencies
pnpm dev # Start development server (https://localhost:3000)
pnpm build # Build for production
pnpm preview # Preview production build locally
pnpm exec nuxi typecheck # Run TypeScript type checking
```

## Key Architectural Decisions

Here are the "why"s behind the design:

| Decision | Reasoning |
|----------|-----------|
| **SSR Disabled** | Client-side SPA avoids hydration mismatches with localStorage, NFC APIs, and browser-only features |
| **Module-Level State** | Composables use module-level `ref()` instead of `useState()` to prevent SSR payload conflicts |
| **PostgreSQL + RLS** | Multi-tenant isolation via Row-Level Security, per-tenant encryption for photos |
| **JWT Cookies** | HTTP-only access (15min) + refresh (7d) tokens with CSRF protection |
| **Client-Side Compression** | Reduces upload size and server load β€” images compressed before sending |
| **IndexedDB Offline Queue** | Entries are never lost, even without internet β€” syncs automatically when back online |
| **3-Layer Theme Init** | Prevents FOUC completely β€” no flash between page load and theme application |

---

# 🀝 Contributing

Want to make Tap & Tell even better? That's awesome! πŸŽ‰

Here's how to get started:

1. 🍴 **Fork** the repository
2. 🌿 **Create** a feature branch (`git checkout -b feature/amazing-feature`)
3. πŸ’» **Make** your changes
4. βœ… **Build** to verify (`pnpm build`)
5. πŸ“ **Commit** with conventional commits (`feat: add amazing feature`)
6. πŸš€ **Push** and open a Pull Request

### Guidelines

- πŸ”€ **Code comments & JSDoc** in English
- 🌍 **All user-facing text** must use i18n translation keys
- 🎨 **Styling** with Tailwind CSS utility classes
- 🧩 **UI components** follow shadcn-vue conventions
- β™Ώ **Accessibility** (a11y) best practices
- πŸ“ **TypeScript** β€” avoid `any` type
- πŸ”’ **Security** β€” no hardcoded secrets, validate at boundaries

### Commit Convention

We use [Conventional Commits](https://www.conventionalcommits.org/):

```
feat: add new feature
fix: resolve a bug
docs: update documentation
refactor: restructure code
style: formatting changes
test: add or update tests
chore: maintenance tasks
```

---

# πŸ”’ Security Notes

> [!CAUTION]
> **Before going to production, make sure to:**
> - πŸ”‘ Set a secure `JWT_SECRET` (not the default)
> - πŸ”‘ Set a secure `CSRF_SECRET` (not the default)
> - πŸ”‘ Generate a 64-character hex `ENCRYPTION_MASTER_KEY` for photo encryption
> - πŸ” All admin features require 2FA (TOTP or Email OTP)

---

# πŸ’‘ Use Case Ideas

Here are some creative ways to use Tap & Tell:

- πŸ’ **Weddings** β€” Let guests leave their wishes and photos for the couple
- πŸŽ‚ **Birthday Parties** β€” Collect fun facts and memories from attendees
- 🏒 **Corporate Events** β€” Gather feedback and networking connections
- πŸŽ“ **Graduations** β€” Classmates share their favorite memories
- πŸŽ„ **Holiday Parties** β€” Guests share their holiday traditions and wishes
- 🏠 **Housewarming** β€” Visitors leave advice and well-wishes for the new home
- 🎸 **Concerts & Festivals** β€” Fans share their experience and favorite moments

---

# πŸŽ‰ That's a Wrap!

Thanks for checking out **Tap & Tell**! If you find it useful, give it a ⭐ on GitHub β€” it really helps! πŸ™Œ

Got a bug to report? Have an idea for a new feature? [Open an issue](https://github.com/Disane87/tap-and-tell/issues) and let's make this better together! πŸš€

---

<p align="center">
Made with ❀️ using <a href="https://nuxt.com/">Nuxt</a>, <a href="https://vuejs.org/">Vue</a>, and <a href="https://tailwindcss.com/">Tailwind CSS</a>
</p>