{"id":43667514,"url":"https://github.com/disane87/tap-and-tell","last_synced_at":"2026-03-17T09:08:26.445Z","repository":{"id":336526297,"uuid":"1145014820","full_name":"Disane87/tap-and-tell","owner":"Disane87","description":"Tap \u0026 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! 📱✨","archived":false,"fork":false,"pushed_at":"2026-02-07T21:23:12.000Z","size":5405,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-07T23:54:30.895Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Disane87.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"Disane87","patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"lfx_crowdfunding":null,"polar":null,"buy_me_a_coffee":null,"thanks_dev":null,"custom":null}},"created_at":"2026-01-29T10:13:28.000Z","updated_at":"2026-02-07T21:23:14.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Disane87/tap-and-tell","commit_stats":null,"previous_names":["disane87/tap-and-tell"],"tags_count":34,"template":false,"template_full_name":null,"purl":"pkg:github/Disane87/tap-and-tell","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Disane87%2Ftap-and-tell","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Disane87%2Ftap-and-tell/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Disane87%2Ftap-and-tell/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Disane87%2Ftap-and-tell/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Disane87","download_url":"https://codeload.github.com/Disane87/tap-and-tell/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Disane87%2Ftap-and-tell/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29411138,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-13T06:24:03.484Z","status":"ssl_error","status_checked_at":"2026-02-13T06:23:12.830Z","response_time":78,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-04T23:04:56.741Z","updated_at":"2026-03-17T09:08:26.438Z","avatar_url":"https://github.com/Disane87.png","language":"TypeScript","funding_links":["https://github.com/sponsors/Disane87"],"categories":[],"sub_categories":[],"readme":"[![Nuxt](https://img.shields.io/badge/Nuxt-4.3-00DC82?logo=nuxt.js\u0026logoColor=white)](https://nuxt.com/)\n[![Vue](https://img.shields.io/badge/Vue-3.5-4FC08D?logo=vue.js\u0026logoColor=white)](https://vuejs.org/)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?logo=typescript\u0026logoColor=white)](https://www.typescriptlang.org/)\n[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-v4-38B2AC?logo=tailwind-css\u0026logoColor=white)](https://tailwindcss.com/)\n[![pnpm](https://img.shields.io/badge/pnpm-package_manager-F69220?logo=pnpm\u0026logoColor=white)](https://pnpm.io/)\n[![License](https://img.shields.io/github/license/Disane87/tap-and-tell)](LICENSE)\n[![GitHub issues](https://img.shields.io/github/issues/Disane87/tap-and-tell?color=red)](https://github.com/Disane87/tap-and-tell/issues)\n# 🎯 Tap \u0026 Tell — NFC-Enabled Digital Guestbook\n\nHey there! 👋 **Tap \u0026 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! 📱✨\n\n\u003e Perfect for weddings 💍, birthday parties 🎂, corporate events 🏢, or any gathering where you want to capture memories digitally!\n\n\u003e [!NOTE]\n\u003e **🤖 AI-Aided Development (AIAD)**\n\u003e\n\u003e 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.\n\n---\n\n\u003cdetails open\u003e\n\u003csummary\u003e\u003ch2\u003e✨ What Can This Thing Do?\u003c/h2\u003e\u003c/summary\u003e\n\nGlad you asked! Here's the good stuff:\n\n- 📱 **NFC \u0026 QR Code Entry** — Guests tap an NFC tag or scan a QR code to open the guestbook instantly — no app download needed!\n- 🧙 **Multi-Step Wizard** — A beautiful 4-step form guides guests through leaving their entry (Basics → Favorites → Fun Facts → Message)\n- 📸 **Photo Upload with Compression** — Guests snap a selfie or upload a photo, automatically compressed client-side for fast uploads\n- 🎨 **Polaroid-Style Cards** — Entries are displayed as gorgeous polaroid-style cards with handwritten fonts\n- 🌙 **Dark Mode** — Full light/dark/system theme support with zero flash of unstyled content (FOUC)\n- 🌍 **Multilingual** — English and German out of the box with `@nuxtjs/i18n`\n- 🖥️ **Slideshow Mode** — Full-screen auto-advancing slideshow, perfect for displaying on a TV at your event\n- 📄 **PDF Export** — Download your entire guestbook as a beautifully formatted PDF\n- 🔐 **Admin Dashboard** — Password-protected admin panel for entry moderation (approve, reject, delete)\n- 📊 **Entry Moderation** — Three-state system: pending → approved / rejected — keep your guestbook clean!\n- 🔄 **Offline Support** — Entries are queued in IndexedDB when offline and synced when back online\n- 📱 **PWA Ready** — Install as a Progressive Web App on any device\n- 🐳 **Docker Support** — Ready-to-use Dockerfile and docker-compose for easy self-hosting\n\n\u003c/details\u003e\n\n---\n\n# 📱 How It Works\n\nThe magic is simple — here's the flow:\n\n```\n1. 📲 Guest taps NFC tag or scans QR code\n         ↓\n2. 🌐 Browser opens Tap \u0026 Tell (no app install!)\n         ↓\n3. 🧙 4-step wizard collects:\n      Step 1: Name + Photo (required)\n      Step 2: Favorites — color, food, movie, song, video (optional)\n      Step 3: Fun Facts — superpowers, hidden talents, preferences (optional)\n      Step 4: Personal Message (required)\n         ↓\n4. 💾 Entry saved with photo compression\n         ↓\n5. 🎉 Entry appears in the guestbook!\n```\n\n\u003e [!NOTE]\n\u003e 📝 Steps 1 (Basics) and 4 (Message) are required. Steps 2 (Favorites) and 3 (Fun Facts) are completely optional — guests can skip them!\n\n---\n\n# 🚀 Getting Started\n\nReady to set up your own digital guestbook? Let's go! 🎉\n\n## Prerequisites\n\n- **Node.js** 18+ installed\n- **pnpm** package manager (`npm install -g pnpm`)\n\n## Quick Start\n\n```bash\n# 1. Clone the repo\ngit clone https://github.com/Disane87/tap-and-tell.git\ncd tap-and-tell\n\n# 2. Install dependencies\npnpm install\n\n# 3. Start the dev server\npnpm dev\n```\n\nThat's it! Open `http://localhost:3000` and you're running! 🎉\n\n## Environment Variables\n\nCreate a `.env` file in the project root:\n\n```env\n# PostgreSQL connection string\nPOSTGRES_URL=postgresql://user:password@localhost:5432/tapandtell\n\n# JWT signing secret (CHANGE THIS in production!)\nJWT_SECRET=your-jwt-secret-here\n\n# CSRF token signing secret (CHANGE THIS in production!)\nCSRF_SECRET=your-csrf-secret-here\n\n# Master encryption key for photo encryption (64 hex chars, REQUIRED in production!)\nENCRYPTION_MASTER_KEY=\n\n# Storage directory for entries and photos\nDATA_DIR=.data\n```\n\n\u003e [!CAUTION]\n\u003e ⚠️ **Security First!** Always set secure values for `JWT_SECRET`, `CSRF_SECRET`, and `ENCRYPTION_MASTER_KEY` in production!\n\n---\n\n# 🐳 Docker Deployment\n\nPrefer containers? We've got you covered!\n\n### Docker Compose — Production (Recommended)\n\nThe `docker-compose.prod.yml` is a self-contained stack (app + PostgreSQL) ready for **Portainer** or any Docker host.\n\n**1. Generate secrets**\n\nAll secrets must be set before first start. Generate them with `openssl`:\n\n```bash\n# JWT signing secret (64-char hex)\nopenssl rand -hex 32\n\n# CSRF token secret (64-char hex)\nopenssl rand -hex 32\n\n# Photo encryption master key (64-char hex)\nopenssl rand -hex 32\n\n# PostgreSQL password (64-char hex)\nopenssl rand -hex 32\n\n# API token secret (base64)\nopenssl rand -base64 32\n```\n\n**2. Configure environment variables**\n\nSet the generated values in `docker-compose.prod.yml` or pass them as environment variables:\n\n| Variable | Format | Description |\n|---|---|---|\n| `POSTGRES_PASSWORD` | 64-char hex | PostgreSQL password (same in `postgres` and `app` services) |\n| `JWT_SECRET` | 64-char hex | JWT signing key for authentication |\n| `CSRF_SECRET` | 64-char hex | CSRF double-submit cookie secret |\n| `ENCRYPTION_MASTER_KEY` | 64-char hex | AES-256-GCM photo encryption key |\n| `TOKEN_SECRET` | base64 string | API token signing secret |\n| `DB_SSL` | `\"false\"` | Set to `\"false\"` for Docker-to-Docker connections (no SSL). Omit for external DBs (Neon, Supabase) where SSL is required. |\n\n\u003e [!CAUTION]\n\u003e Never commit secrets to version control. Use environment variables, Docker secrets, or Portainer's environment variable UI instead.\n\n**3. Deploy**\n\n```bash\n# Direct Docker Compose\ndocker compose -f docker-compose.prod.yml up -d\n```\n\nOr in **Portainer**: Stacks → Add Stack → paste the compose file → set environment variables in the UI.\n\n### Docker Compose — Development\n\n```bash\ndocker compose up -d\n```\n\n### Standalone Docker\n\n```bash\n# Build the image\ndocker build -t tap-and-tell .\n\n# Run the container\ndocker run -d \\\n  -p 3000:3000 \\\n  -e POSTGRES_URL=postgresql://user:password@host:5432/tapandtell \\\n  -e DB_SSL=false \\\n  -e JWT_SECRET=$(openssl rand -hex 32) \\\n  -e CSRF_SECRET=$(openssl rand -hex 32) \\\n  -e ENCRYPTION_MASTER_KEY=$(openssl rand -hex 32) \\\n  -e TOKEN_SECRET=$(openssl rand -base64 32) \\\n  -v tap-and-tell-data:/app/data \\\n  tap-and-tell\n```\n\n\u003e [!IMPORTANT]\n\u003e 📂 Mount a volume to `/app/data` to persist your guestbook entries and photos across container restarts!\n\n---\n\n# 📖 Pages \u0026 Features\n\nHere's a tour of everything Tap \u0026 Tell offers:\n\n## 🏠 Landing Page (`/`)\n\nThe main entry point for guests! Features:\n- 🎠 **Swipeable Carousel** — Intro slide followed by existing entry slides\n- 📝 **Bottom Sheet Wizard** — The 4-step form slides up from the bottom\n- ⌨️ **Keyboard \u0026 Swipe Navigation** — Navigate entries with arrow keys or swipe gestures\n- 📱 **NFC Context Detection** — Personalized welcome when entering via NFC tag\n- 🔵 **Pagination Dots** — Visual indicators for carousel position\n\n## 📖 Guestbook (`/guestbook`)\n\nBrowse all approved entries in a beautiful grid:\n- 🔍 **Search by Name** — Debounced search (300ms) for instant filtering\n- 🔄 **Sort Options** — Newest first or oldest first\n- 📄 **PDF Export** — Download the entire guestbook as a formatted PDF\n- 🖥️ **Slideshow Link** — Quick access to slideshow mode\n- 👆 **Detail View** — Click any card to see the full entry in a bottom sheet\n\n## 🖥️ Slideshow (`/slideshow`)\n\nPerfect for displaying on a TV at your event!\n- ▶️ **Auto-Advancing** — Configurable interval (3–30 seconds, default 8)\n- ⏸️ **Play/Pause Controls** — Take control when you want\n- 🖥️ **Fullscreen Mode** — True fullscreen for maximum impact\n- ⌨️ **Keyboard Controls** — Arrow keys, Space, P (pause), F (fullscreen), ESC (exit)\n- 👻 **Auto-Hide Controls** — Controls fade away during playback\n\n## 🔐 Admin Dashboard (`/admin`)\n\nManage your guestbook with a password-protected admin panel:\n- 📊 **Status Tabs** — Filter by All, Pending, Approved, Rejected\n- ✅ **Bulk Actions** — Approve or reject multiple entries at once\n- 🗑️ **Individual Management** — Delete or change status of single entries\n- 🔢 **Entry Counts** — See counts per status at a glance\n- 🚪 **Secure Logout** — Token-based session management\n\n## 📱 QR Code Generator (`/admin/qr`)\n\nGenerate QR codes for your event:\n- 🎯 **Custom Event Name** — Embed your event name in the URL\n- 📥 **Download Options** — Export as PNG or SVG\n- 📋 **Copy URL** — Quick copy to clipboard\n- 🔗 **NFC-Compatible URLs** — Generates `?source=nfc\u0026event=YourEvent` links\n\n---\n\n# 🏗️ Architecture\n\nLet's peek under the hood! Here's how Tap \u0026 Tell is built:\n\n## Tech Stack\n\n| Layer | Technology |\n|-------|-----------|\n| **Framework** | Nuxt 4.3 (SSR disabled — client-side SPA) |\n| **UI Library** | Vue 3.5 with Composition API |\n| **Styling** | Tailwind CSS v4 via `@tailwindcss/vite` |\n| **Components** | shadcn-vue (headless UI) |\n| **Language** | TypeScript 5.9 |\n| **Icons** | Lucide Vue Next |\n| **Database** | PostgreSQL 16+ with Row-Level Security (RLS) |\n| **ORM** | Drizzle ORM |\n| **Auth** | JWT (jose) + 2FA (TOTP / Email OTP) |\n| **Encryption** | AES-256-GCM per-tenant photo encryption |\n| **i18n** | @nuxtjs/i18n (EN + DE) |\n| **PDF Generation** | jsPDF |\n| **QR Codes** | qrcode |\n| **Utilities** | VueUse |\n| **Toasts** | vue-sonner |\n| **PWA** | @vite-pwa/nuxt |\n| **Package Manager** | pnpm |\n| **Deployment** | Docker (self-hosted) |\n\n## Project Structure\n\n```\ntap-and-tell/\n├── app/                          # 🖥️ Nuxt client application\n│   ├── pages/                    #    Route pages\n│   ├── components/               #    Vue components\n│   │   ├── form/                 #    Wizard form steps\n│   │   └── ui/                   #    shadcn-vue base components\n│   ├── composables/              #    Vue composables (state \u0026 logic)\n│   ├── types/                    #    TypeScript type definitions\n│   ├── plugins/                  #    Nuxt client plugins\n│   ├── layouts/                  #    Page layouts\n│   ├── lib/                      #    Utility functions\n│   └── assets/                   #    Static assets (CSS, images)\n│\n├── server/                       # ⚙️ Nitro server\n│   ├── routes/api/               #    API endpoints\n│   │   ├── g/                    #    Public guest endpoints (flat routes)\n│   │   ├── auth/                 #    Authentication + 2FA\n│   │   ├── tenants/              #    Tenant/guestbook management\n│   │   └── photos/               #    Photo serving (encrypted)\n│   ├── database/                 #    Schema + migrations (Drizzle ORM)\n│   ├── utils/                    #    Server utilities (crypto, auth, storage)\n│   └── plugins/                  #    Server startup plugins\n│\n├── i18n/                         # 🌍 Internationalization\n│   └── locales/                  #    EN + DE translation files\n│\n├── public/                       # 📂 Static public assets\n│   └── icons/                    #    PWA icons\n│\n├── plans/                        # 📋 Development plan documents\n├── nuxt.config.ts                # ⚙️ Nuxt configuration\n├── package.json                  # 📦 Dependencies\n└── tsconfig.json                 # 🔧 TypeScript config\n```\n\n## Storage Layer\n\nTap \u0026 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**.\n\n```\nPostgreSQL (via Drizzle ORM)\n├── users, sessions, user_two_factor     # Auth \u0026 2FA\n├── tenants, tenant_members              # Multi-tenancy\n├── guestbooks, entries                  # Core data (RLS-protected)\n└── audit_logs, api_apps, api_tokens     # Security \u0026 API access\n\n.data/photos/\n├── [guestbookId]/[entryId].[ext]        # AES-256-GCM encrypted photos\n└── ...\n```\n\n\u003e [!NOTE]\n\u003e Photo storage is configurable via `STORAGE_DRIVER` (`local`, `vercel-blob`, or `s3`) and `DATA_DIR` (default: `.data/`).\n\n---\n\n# 🔌 API Reference\n\nAll API endpoints at a glance. Authenticated endpoints use HTTP-only JWT cookies with CSRF protection.\n\n## Public — Guest Endpoints (No Auth)\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/api/g/[id]/info` | Guestbook info (name, settings, type) |\n| `GET` | `/api/g/[id]/entries` | Approved entries for a guestbook |\n| `POST` | `/api/g/[id]/entries` | Create a new guest entry (rate-limited) |\n| `GET` | `/api/photos/[tenantId]/[filename]` | Serve encrypted photo |\n| `GET` | `/api/health` | Health check endpoint |\n| `GET` | `/api/og` | Locale-aware OG image (`?lang=de\\|en`) |\n\n## Authentication \u0026 Profile\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `POST` | `/api/auth/login` | Login with email/password, set JWT cookies |\n| `POST` | `/api/auth/register` | Register a new account |\n| `POST` | `/api/auth/logout` | Clear auth cookies |\n| `POST` | `/api/auth/refresh` | Refresh access token |\n| `GET` | `/api/auth/me` | Get current user profile |\n| `PUT` | `/api/auth/me` | Update name and/or email |\n| `DELETE` | `/api/auth/me` | Delete account (requires password) |\n| `PUT` | `/api/auth/password` | Change password |\n| `GET` | `/api/auth/csrf` | Get CSRF token |\n| `POST` | `/api/auth/avatar` | Upload avatar (multipart, max 5 MB) |\n| `DELETE` | `/api/auth/avatar` | Delete avatar |\n| `GET` | `/api/auth/avatar/[userId]` | Serve avatar image (public) |\n\n## Two-Factor Authentication (2FA)\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `POST` | `/api/auth/2fa/setup` | Start 2FA setup (returns QR code) |\n| `POST` | `/api/auth/2fa/verify-setup` | Verify TOTP code to activate 2FA |\n| `GET` | `/api/auth/2fa/status` | Check if 2FA is enabled |\n| `POST` | `/api/auth/2fa/verify` | Verify 2FA code during login |\n| `POST` | `/api/auth/2fa/disable` | Disable 2FA |\n| `POST` | `/api/auth/2fa/resend` | Resend email OTP code |\n\n## Tenant Management (JWT Required)\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/api/tenants` | List user's tenants |\n| `POST` | `/api/tenants` | Create a new tenant |\n| `GET` | `/api/tenants/[uuid]` | Get tenant details |\n| `PUT` | `/api/tenants/[uuid]` | Update tenant settings |\n| `DELETE` | `/api/tenants/[uuid]` | Delete tenant |\n| `POST` | `/api/tenants/[uuid]/rotate-key` | Rotate encryption key |\n\n## Guestbook Management (JWT Required)\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/api/tenants/[uuid]/guestbooks` | List guestbooks with entry counts |\n| `POST` | `/api/tenants/[uuid]/guestbooks` | Create a new guestbook |\n| `GET` | `/api/tenants/[uuid]/guestbooks/[gbUuid]` | Get guestbook details |\n| `PUT` | `/api/tenants/[uuid]/guestbooks/[gbUuid]` | Update guestbook settings |\n| `DELETE` | `/api/tenants/[uuid]/guestbooks/[gbUuid]` | Delete guestbook (cascades entries) |\n| `POST` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/header` | Upload header image |\n| `DELETE` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/header` | Delete header image |\n| `POST` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/background` | Upload background image |\n| `DELETE` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/background` | Delete background image |\n\n## Entry Moderation (JWT Required)\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/entries` | All entries (admin view) |\n| `PATCH` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/entries/[id]` | Update entry status |\n| `DELETE` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/entries/[id]` | Delete an entry |\n| `POST` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/entries/bulk` | Bulk status update |\n\n## Team Members (JWT Required)\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/api/tenants/[uuid]/members` | List team members |\n| `POST` | `/api/tenants/[uuid]/members/invite` | Invite team member |\n| `GET` | `/api/tenants/[uuid]/members/invites` | List pending invites |\n| `DELETE` | `/api/tenants/[uuid]/members/invites/[id]` | Cancel invite |\n| `DELETE` | `/api/tenants/[uuid]/members/[userId]` | Remove team member |\n| `GET` | `/api/invites/[token]` | Get invite details (public) |\n| `POST` | `/api/invites/accept` | Accept team invite (public) |\n\n## API Apps \u0026 Tokens (JWT Required)\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `/api/tenants/[uuid]/apps` | List API apps |\n| `POST` | `/api/tenants/[uuid]/apps` | Create API app |\n| `GET` | `/api/tenants/[uuid]/apps/[appId]` | Get API app details |\n| `PUT` | `/api/tenants/[uuid]/apps/[appId]` | Update API app |\n| `DELETE` | `/api/tenants/[uuid]/apps/[appId]` | Delete API app |\n| `GET` | `/api/tenants/[uuid]/apps/[appId]/tokens` | List tokens |\n| `POST` | `/api/tenants/[uuid]/apps/[appId]/tokens` | Create token |\n| `DELETE` | `/api/tenants/[uuid]/apps/[appId]/tokens/[tokenId]` | Revoke token |\n| `GET` | `/api/scopes` | List available API scopes |\n\n## Analytics (JWT Required)\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `POST` | `/api/analytics/events` | Track analytics event |\n| `GET` | `/api/tenants/[uuid]/analytics/overview` | Dashboard overview |\n| `GET` | `/api/tenants/[uuid]/analytics/traffic` | Traffic analytics |\n| `GET` | `/api/tenants/[uuid]/analytics/sources` | Traffic sources |\n| `GET` | `/api/tenants/[uuid]/analytics/devices` | Device breakdown |\n| `GET` | `/api/tenants/[uuid]/analytics/funnel` | Conversion funnel |\n\n### Quick Start Example\n\n```bash\n# Login\ncurl -X POST /api/auth/login \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\": \"you@example.com\", \"password\": \"your-password\"}'\n# → Sets access_token and refresh_token cookies\n\n# Create a guest entry (public, no auth)\ncurl -X POST /api/g/your-guestbook-id/entries \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"Jane Doe\",\n    \"message\": \"What an amazing event!\",\n    \"photo\": \"data:image/jpeg;base64,...\"\n  }'\n```\n\n---\n\n# 🧩 Composables\n\nThe brain of Tap \u0026 Tell lives in these composables — each one handles a specific concern:\n\n| Composable | What It Does |\n|-----------|-------------|\n| `useAuth()` | 🔐 JWT cookie-based authentication (login, register, logout, profile) |\n| `useGuests()` | 📋 CRUD operations for guestbook entries. Module-level shared state across the app |\n| `useGuestForm()` | 🧙 4-step wizard state management with per-step validation |\n| `useGuestbook()` | 📖 Public guestbook operations using flat `/api/g/[id]` endpoints |\n| `useTenantAdmin()` | 👔 Admin entry operations (fetch all, delete, update status, bulk) |\n| `useTheme()` | 🌙 Light/dark/system theme with localStorage persistence \u0026 FOUC prevention |\n| `useNfc()` | 📱 Detects NFC context from URL query params (`?source=nfc\u0026event=...`) |\n| `useSlideshow()` | 🖥️ Auto-advancing slideshow with play/pause/fullscreen controls |\n| `useEntryFilters()` | 🔍 Debounced search \u0026 sort for the guestbook page |\n| `usePdfExport()` | 📄 Multi-page PDF generation with photos, favorites, and fun facts |\n| `useImageCompression()` | 📸 Client-side image compression (max 1920px, target 500KB) |\n| `useOfflineQueue()` | 🔄 IndexedDB-based offline entry queuing with auto-sync |\n\n---\n\n# 🎨 Theme System\n\nTap \u0026 Tell features a **3-layer theme initialization** to prevent any flash of unstyled content (FOUC):\n\n```\nLayer 1: Inline \u003cscript\u003e in \u003chead\u003e\n  └─ Runs BEFORE first paint\n  └─ Reads localStorage, applies `dark` class to \u003chtml\u003e\n  └─ Zero visual flash! ⚡\n\nLayer 2: Client Plugin (theme.client.ts)\n  └─ Syncs reactive Vue state with DOM\n  └─ Listens for system preference changes\n\nLayer 3: \u003cClientOnly\u003e Wrapper\n  └─ ThemeToggle component only renders on client\n  └─ Prevents SSR hydration mismatches\n```\n\nToggle between **Light** ☀️, **Dark** 🌙, and **System** 💻 modes with a single click.\n\n---\n\n# 🌍 Internationalization\n\nAll user-facing text is translatable — no hardcoded strings anywhere!\n\n| Feature | Details |\n|---------|---------|\n| **Languages** | 🇬🇧 English (default) + 🇩🇪 Deutsch |\n| **Strategy** | No URL prefix, browser detection |\n| **Persistence** | Cookie `i18n_locale` |\n| **Module** | `@nuxtjs/i18n` |\n\nTranslation files live in `i18n/locales/` covering all scopes: form, guestbook, admin, navigation, slideshow, toasts, and more.\n\n---\n\n# 📱 NFC \u0026 QR Code Setup\n\nSetting up NFC tags or QR codes for your event is easy!\n\n## NFC Tags\n\n1. Get writable NFC tags (NTAG215 or similar)\n2. Use any NFC writer app to write the URL:\n   ```\n   https://your-domain.com/?source=nfc\u0026event=YourEventName\n   ```\n3. Place tags at your event venue — guests tap and they're in! 📲\n\n## QR Codes\n\n1. Go to `/admin/qr` in your admin panel\n2. Enter your event name\n3. Download as PNG or SVG\n4. Print and display at your venue! 🖨️\n\n\u003e [!TIP]\n\u003e 💡 **Pro tip**: Use both NFC tags AND QR codes! NFC for quick access, QR as a fallback for phones without NFC support.\n\n---\n\n# 📄 Data Model\n\nHere's what a guest entry looks like under the hood:\n\n```typescript\ninterface GuestEntry {\n  id: string                    // UUID\n  name: string                  // Guest's name\n  message: string               // Personal message\n  photoUrl?: string             // Photo path (e.g., /api/photos/{id}.jpg)\n  answers?: GuestAnswers        // Optional form answers\n  createdAt: string             // ISO 8601 timestamp\n  status?: EntryStatus          // 'pending' | 'approved' | 'rejected'\n  rejectionReason?: string      // Why entry was rejected\n}\n\ninterface GuestAnswers {\n  // 🎨 Favorites\n  favoriteColor?: string\n  favoriteFood?: string\n  favoriteMovie?: string\n  favoriteSong?: { title: string; artist?: string; url?: string }\n  favoriteVideo?: { title: string; url?: string }\n\n  // 🎭 Fun Facts\n  superpower?: string\n  hiddenTalent?: string\n  desertIslandItems?: string\n  coffeeOrTea?: 'coffee' | 'tea'\n  nightOwlOrEarlyBird?: 'night_owl' | 'early_bird'\n  beachOrMountains?: 'beach' | 'mountains'\n\n  // 📖 Our Story\n  howWeMet?: string\n  bestMemory?: string\n}\n```\n\n---\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ch2\u003e⚡ Advanced Configuration\u003c/h2\u003e\u003c/summary\u003e\n\n### Image Compression Settings\n\nClient-side image compression is applied automatically before upload:\n\n| Setting | Value |\n|---------|-------|\n| Max dimension | 1920px |\n| Target file size | 500KB |\n| Initial JPEG quality | 0.8 |\n| Minimum JPEG quality | 0.3 (adaptive) |\n\n### PWA Configuration\n\nTap \u0026 Tell is a fully-configured Progressive Web App:\n\n| Setting | Value |\n|---------|-------|\n| Display mode | Standalone |\n| Orientation | Portrait |\n| Theme color | Dark |\n| Icon | SVG (any size, maskable) |\n| Font caching | Google Fonts (1-year CacheFirst) |\n| Offline | Navigate fallback to `/` |\n\n### Server-Side Validation\n\nEntries are validated server-side with these constraints:\n\n| Field | Constraint |\n|-------|-----------|\n| `name` | Required, 1–100 characters |\n| `message` | Required, 1–1000 characters |\n| `photo` | Optional, max 7MB (base64) |\n\n### Auth Token Details\n\n| Property | Value |\n|----------|-------|\n| Algorithm | HS256 (JWT via `jose`) |\n| Access Token | 15 minutes, HTTP-only cookie |\n| Refresh Token | 7 days, HTTP-only cookie, stored in DB |\n| CSRF | Double-submit cookie pattern |\n| 2FA | TOTP (RFC 6238) + Email OTP |\n\n\u003c/details\u003e\n\n---\n\n# 🛠️ Development\n\nWant to contribute or customize? Here's how to get the development environment running:\n\n## Commands\n\n```bash\npnpm install              # Install dependencies\npnpm dev                  # Start development server (https://localhost:3000)\npnpm build                # Build for production\npnpm preview              # Preview production build locally\npnpm exec nuxi typecheck  # Run TypeScript type checking\n```\n\n## Key Architectural Decisions\n\nHere are the \"why\"s behind the design:\n\n| Decision | Reasoning |\n|----------|-----------|\n| **SSR Disabled** | Client-side SPA avoids hydration mismatches with localStorage, NFC APIs, and browser-only features |\n| **Module-Level State** | Composables use module-level `ref()` instead of `useState()` to prevent SSR payload conflicts |\n| **PostgreSQL + RLS** | Multi-tenant isolation via Row-Level Security, per-tenant encryption for photos |\n| **JWT Cookies** | HTTP-only access (15min) + refresh (7d) tokens with CSRF protection |\n| **Client-Side Compression** | Reduces upload size and server load — images compressed before sending |\n| **IndexedDB Offline Queue** | Entries are never lost, even without internet — syncs automatically when back online |\n| **3-Layer Theme Init** | Prevents FOUC completely — no flash between page load and theme application |\n\n---\n\n# 🤝 Contributing\n\nWant to make Tap \u0026 Tell even better? That's awesome! 🎉\n\nHere's how to get started:\n\n1. 🍴 **Fork** the repository\n2. 🌿 **Create** a feature branch (`git checkout -b feature/amazing-feature`)\n3. 💻 **Make** your changes\n4. ✅ **Build** to verify (`pnpm build`)\n5. 📝 **Commit** with conventional commits (`feat: add amazing feature`)\n6. 🚀 **Push** and open a Pull Request\n\n### Guidelines\n\n- 🔤 **Code comments \u0026 JSDoc** in English\n- 🌍 **All user-facing text** must use i18n translation keys\n- 🎨 **Styling** with Tailwind CSS utility classes\n- 🧩 **UI components** follow shadcn-vue conventions\n- ♿ **Accessibility** (a11y) best practices\n- 📝 **TypeScript** — avoid `any` type\n- 🔒 **Security** — no hardcoded secrets, validate at boundaries\n\n### Commit Convention\n\nWe use [Conventional Commits](https://www.conventionalcommits.org/):\n\n```\nfeat: add new feature\nfix: resolve a bug\ndocs: update documentation\nrefactor: restructure code\nstyle: formatting changes\ntest: add or update tests\nchore: maintenance tasks\n```\n\n---\n\n# 🔒 Security Notes\n\n\u003e [!CAUTION]\n\u003e **Before going to production, make sure to:**\n\u003e - 🔑 Set a secure `JWT_SECRET` (not the default)\n\u003e - 🔑 Set a secure `CSRF_SECRET` (not the default)\n\u003e - 🔑 Generate a 64-character hex `ENCRYPTION_MASTER_KEY` for photo encryption\n\u003e - 🔐 All admin features require 2FA (TOTP or Email OTP)\n\n---\n\n# 💡 Use Case Ideas\n\nHere are some creative ways to use Tap \u0026 Tell:\n\n- 💍 **Weddings** — Let guests leave their wishes and photos for the couple\n- 🎂 **Birthday Parties** — Collect fun facts and memories from attendees\n- 🏢 **Corporate Events** — Gather feedback and networking connections\n- 🎓 **Graduations** — Classmates share their favorite memories\n- 🎄 **Holiday Parties** — Guests share their holiday traditions and wishes\n- 🏠 **Housewarming** — Visitors leave advice and well-wishes for the new home\n- 🎸 **Concerts \u0026 Festivals** — Fans share their experience and favorite moments\n\n---\n\n# 🎉 That's a Wrap!\n\nThanks for checking out **Tap \u0026 Tell**! If you find it useful, give it a ⭐ on GitHub — it really helps! 🙌\n\nGot 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! 🚀\n\n---\n\n\u003cp align=\"center\"\u003e\n  Made with ❤️ using \u003ca href=\"https://nuxt.com/\"\u003eNuxt\u003c/a\u003e, \u003ca href=\"https://vuejs.org/\"\u003eVue\u003c/a\u003e, and \u003ca href=\"https://tailwindcss.com/\"\u003eTailwind CSS\u003c/a\u003e\n\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdisane87%2Ftap-and-tell","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdisane87%2Ftap-and-tell","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdisane87%2Ftap-and-tell/lists"}