Here is Claude's plan: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ Restaurant Menu QR Code - Dashboard Integration Plan Executive Summary Add "Restaurant Menu" as a new QR code content type in the dashboard at /create. Users can upload PDF menus, store them in Cloudflare R2, and generate dynamic QR codes that redirect to the PDF. This integrates with the existing QRCode model and tracking system. Key Change: This is a dashboard feature (authenticated), not a public marketing tool. Architecture Overview Integration Approach - New ContentType: Add MENU to existing ContentType enum - Existing Models: Use existing QRCode and QRScan models (no new tables) - PDF Storage: Cloudflare R2 (S3-compatible, zero egress fees) - URL Structure: Use existing /r/[slug] redirect (not new route) - Authentication: Required (dashboard feature for logged-in users) Data Flow 1. User logs in → Goes to /create → Selects "Restaurant Menu" type 2. Uploads PDF → Validate → Upload to R2 → Get public URL 3. Creates QR code with content: { pdfUrl: "...", restaurantName: "...", menuTitle: "..." } 4. QR code redirects to: /r/[slug] → Redirect to PDF URL 5. Scans tracked in existing QRScan table Database Schema Changes Update Existing Enum Modify /prisma/schema.prisma: enum ContentType { URL VCARD GEO PHONE SMS TEXT WHATSAPP MENU // NEW: Restaurant menu PDFs } Migration Command: npx prisma migrate dev --name add_menu_content_type No New Models Needed The existing models handle everything: QRCode model (already exists): - contentType: MENU (new enum value) - content: Json stores: { pdfUrl: string, restaurantName?: string, menuTitle?: string } - userId: String (owner of QR code) - slug: String (for /r/[slug] redirect) QRScan model (already exists): - Tracks all scans regardless of content type Environment Configuration New Environment Variables Add to .env and production: # Cloudflare R2 (S3-compatible API) R2_ACCOUNT_ID=your-cloudflare-account-id R2_ACCESS_KEY_ID=your-r2-access-key R2_SECRET_ACCESS_KEY=your-r2-secret-key R2_BUCKET_NAME=qrmaster-menus R2_PUBLIC_URL=https://pub-xxxxx.r2.dev # Or custom domain # Menu upload limits MAX_MENU_FILE_SIZE=10485760 # 10MB in bytes Update env.ts Add to /src/lib/env.ts schema: const envSchema = z.object({ // ... existing fields ... R2_ACCOUNT_ID: z.string().optional(), R2_ACCESS_KEY_ID: z.string().optional(), R2_SECRET_ACCESS_KEY: z.string().optional(), R2_BUCKET_NAME: z.string().default('qrmaster-menus'), R2_PUBLIC_URL: z.string().optional(), MAX_MENU_FILE_SIZE: z.string().default('10485760'), }); Critical Files to Modify/Create 1. R2 Client Library File: /src/lib/r2.ts (NEW) Purpose: Handle PDF uploads to Cloudflare R2 import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; import { env } from './env'; const r2Client = new S3Client({ region: 'auto', endpoint: `https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, credentials: { accessKeyId: env.R2_ACCESS_KEY_ID!, secretAccessKey: env.R2_SECRET_ACCESS_KEY!, }, }); export async function uploadMenuToR2( file: Buffer, filename: string, shortId: string ): Promise { const key = `menus/${shortId}.pdf`; await r2Client.send( new PutObjectCommand({ Bucket: env.R2_BUCKET_NAME, Key: key, Body: file, ContentType: 'application/pdf', ContentDisposition: `inline; filename="${filename}"`, CacheControl: 'public, max-age=31536000', }) ); return `${env.R2_PUBLIC_URL}/${key}`; } export async function deleteMenuFromR2(r2Key: string): Promise { await r2Client.send( new DeleteObjectCommand({ Bucket: env.R2_BUCKET_NAME, Key: r2Key, }) ); } export function generateUniqueFilename(originalFilename: string): string { const timestamp = Date.now(); const random = crypto.randomBytes(4).toString('hex'); const ext = originalFilename.split('.').pop(); return `menu_${timestamp}_${random}.${ext}`; } 2. Upload API Endpoint File: /src/app/api/menu/upload/route.ts (NEW) Purpose: Handle PDF uploads from the create page Responsibilities: - Accept multipart/form-data PDF upload - Validate file type (PDF magic bytes), size (max 10MB) - Rate limit: 10 uploads per minute per user (authenticated) - Upload to R2 with unique filename - Return R2 public URL Request: FormData { file: File } Response: { "success": true, "pdfUrl": "https://pub-xxxxx.r2.dev/menus/menu_1234567890_abcd.pdf" } Key Implementation Details: - Use request.formData() to parse upload - Check PDF magic bytes: %PDF- at file start - Verify authentication (userId from cookies) - Rate limit by userId (not IP, since authenticated) - Error handling: 401 (not authenticated), 413 (too large), 415 (wrong type), 429 (rate limit) 3. Update Redirect Route File: /src/app/r/[slug]/route.ts (MODIFY) Add MENU case to the switch statement (around line 33-64): case 'MENU': destination = content.pdfUrl || 'https://example.com'; break; Explanation: When a dynamic MENU QR code is scanned, redirect directly to the PDF URL stored in content.pdfUrl 4. Update Validation Schema File: /src/lib/validationSchemas.ts (MODIFY) Line 28: Update contentType enum to include MENU: contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT', 'MENU'], { errorMap: () => ({ message: 'Invalid content type' }) }), Line 63: Update bulk QR schema as well: contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT', 'MENU']), 5. Update Create Page - Add MENU Type File: /src/app/(app)/create/page.tsx (MODIFY) Multiple changes needed: A. Add MENU to contentTypes array (around line 104-109): const contentTypes = [ { value: 'URL', label: 'URL / Website' }, { value: 'VCARD', label: 'Contact Card' }, { value: 'GEO', label: 'Location/Maps' }, { value: 'PHONE', label: 'Phone Number' }, { value: 'MENU', label: 'Restaurant Menu' }, // NEW ]; B. Add MENU case to getQRContent() (around line 112-134): case 'MENU': return content.pdfUrl || 'https://example.com/menu.pdf'; C. Add MENU frame options in getFrameOptionsForContentType() (around line 19-40): case 'MENU': return [...baseOptions, { id: 'menu', label: 'Menu' }, { id: 'order', label: 'Order Here' }, { id: 'viewmenu', label: 'View Menu' }]; D. Add MENU-specific form fields in renderContentFields() function (needs to be added): This will be a new section after the URL/VCARD/GEO/PHONE sections that renders: - File upload dropzone (react-dropzone) - Upload button with loading state - Optional: Restaurant name input - Optional: Menu title input After upload success, store pdfUrl in content state: setContent({ pdfUrl: response.pdfUrl, restaurantName: '', menuTitle: '' }); 6. Update Rate Limiting File: /src/lib/rateLimit.ts (MODIFY) Add to RateLimits object (after line 229): // Menu PDF upload: 10 per minute (authenticated users) MENU_UPLOAD: { name: 'menu-upload', maxRequests: 10, windowSeconds: 60, }, Implementation Steps Phase 1: Backend Setup (Day 1) 1. Install Dependencies npm install @aws-sdk/client-s3 react-dropzone 2. Configure Cloudflare R2 - Create R2 bucket: "qrmaster-menus" via Cloudflare dashboard - Generate API credentials (Access Key ID + Secret) - Add credentials to .env and production environment - Set bucket to public (for PDF access) 3. Database Migration - Add MENU to ContentType enum in prisma/schema.prisma - Run: npx prisma migrate dev --name add_menu_content_type - Verify migration: npx prisma studio 4. Environment Configuration - Update src/lib/env.ts with R2 variables - Update src/lib/rateLimit.ts with MENU_UPLOAD config 5. Create R2 Client - Create src/lib/r2.ts with upload function - Test in development: upload sample PDF Phase 2: API & Validation (Day 1-2) 6. Update Validation Schema (/src/lib/validationSchemas.ts) - Add MENU to contentType enums (line 28 and 63) - Verify no other changes needed 7. Create Upload API (/src/app/api/menu/upload/route.ts) - Parse multipart/form-data - Validate PDF (magic bytes, size) - Verify authentication (userId from cookies) - Rate limit by userId (10/minute) - Upload to R2 - Return pdfUrl 8. Update Redirect Route (/src/app/r/[slug]/route.ts) - Add MENU case to switch statement (line 33-64) - Redirect to content.pdfUrl Phase 3: Dashboard Integration (Day 2-3) 9. Update Create Page (/src/app/(app)/create/page.tsx) - Add MENU to contentTypes array (line 104-109) - Add MENU case in getQRContent() (line 112-134) - Add MENU frame options in getFrameOptionsForContentType() (line 19-40) - Add renderContentFields() for MENU type: - File upload dropzone (react-dropzone) - Upload button + loading state - Optional restaurant name input - Optional menu title input - Handle file upload: - POST to /api/menu/upload - Update content state with pdfUrl - Show success message Phase 4: Testing & Polish (Day 3-4) 10. Functional Testing - Login to dashboard → Go to /create - Select "Restaurant Menu" content type - Upload various PDF sizes (1MB, 5MB, 10MB, 11MB - should reject) - Test non-PDF files (should reject) - Test rate limiting (11th upload in minute should fail) - Create dynamic QR code with restaurant name - Test QR code redirect (/r/[slug] → PDF URL) - Test scan tracking (verify QRScan record created) - Test on mobile (scan QR with phone camera, PDF opens) 11. Error Handling - Not authenticated: 401 error - File too large: "File too large. Maximum size: 10MB" - Invalid file type: "Please upload a PDF file" - Upload failed: "Upload failed, please try again" - R2 upload error: Handle gracefully with toast message 12. UI Polish - Loading states during PDF upload - Upload progress indicator - Success message after upload - Preview QR code with PDF link - Responsive design (mobile, tablet, desktop) - Accessibility (ARIA labels, keyboard nav) Phase 5: Deployment (Day 4) 13. Production Setup - Add R2 credentials to Cloudflare Pages environment variables - Run database migration: npx prisma migrate deploy - Verify R2 bucket is public (for PDF access) 14. Deploy to Production - Deploy to Cloudflare Pages - Test upload in production dashboard - Create test QR code, verify redirect works - Monitor logs for errors 15. Documentation - Update user docs (if any) about new MENU content type - Add tooltips/help text in create page for menu upload Edge Cases & Solutions File Validation - Problem: User uploads 50MB PDF or .exe file - Solution: - Client-side validation (check file.size and file.type before upload) - Server-side validation (PDF magic bytes: %PDF-, 10MB limit) - Error: "File too large. Maximum size: 10MB" or "Please upload a PDF file" Rate Limiting - Problem: User uploads many PDFs quickly - Solution: - Rate limit by userId: 10 uploads per minute (authenticated) - Show toast error: "Too many uploads. Please wait a moment." - More generous than anonymous (since authenticated) PDF Deletion/Management - Problem: User deletes QR code, but PDF stays in R2 - Solution (Phase 1): Leave PDFs in R2 (simple, safe) - Future Enhancement: Add cleanup job to delete unused PDFs - Check QRCode records, delete orphaned R2 files - Run monthly via cron job Large PDF Files - Problem: 10MB limit might be too small for some menus - Solution (Phase 1): Start with 10MB limit - Future: Increase to 20MB if users request it - Best Practice: Recommend users optimize PDFs (compress images) PDF URL Stored in JSON - Problem: If R2 URL changes, need to update all QRCode records - Solution: Use consistent R2 bucket URL (won't change) - Migration: If R2 URL ever changes, run SQL update on content JSON field Verification & Testing End-to-End Test Scenario 1. Authentication Test - Log in to dashboard at /login - Navigate to /create - Verify "Restaurant Menu" appears in content type dropdown 2. Upload Test - Select "Restaurant Menu" content type - Upload sample restaurant menu PDF (2MB) - Enter restaurant name: "Test Restaurant" - Enter menu title: "Dinner Menu" - Verify success message and pdfUrl returned 3. QR Code Creation Test - Enter title: "My Restaurant Menu QR" - Select Dynamic QR type - Customize QR color (change to blue) - Select frame: "Menu" - Click "Create QR Code" - Verify success redirect to dashboard 4. Scan Test - From dashboard, copy QR code URL: qrmaster.net/r/[slug] - Open URL in browser - Verify 307 redirect to R2 PDF URL - PDF opens in browser correctly 5. Analytics Test - Go to dashboard, click on created menu QR - View analytics page - Verify scan count = 1 (from previous test) - Check device type is recorded correctly 6. Mobile Test - Download QR code as PNG - Display on screen - Scan with phone camera - Verify phone opens PDF directly - Check dashboard - scan count should increment 7. Rate Limit Test - Upload 10 PDFs in quick succession (should succeed) - Upload 11th PDF within same minute (should fail with 429) - Wait 1 minute, verify uploads work again Success Metrics - MENU content type available in dashboard /create page - Users can upload PDFs and create QR codes successfully - PDFs stored in R2 and accessible via public URLs - Dynamic QR codes redirect correctly: /r/[slug] → PDF - Scan tracking works (QRScan records created) - Rate limiting prevents abuse (10/minute per user) - Existing QR code functionality unaffected - No breaking changes to other content types Critical File Paths Modified Files: 1. /prisma/schema.prisma - Add MENU to ContentType enum 2. /src/lib/validationSchemas.ts - Add MENU to contentType enums (lines 28, 63) 3. /src/app/(app)/create/page.tsx - Add MENU UI and logic 4. /src/app/r/[slug]/route.ts - Add MENU redirect case 5. /src/lib/env.ts - Add R2 environment variables 6. /src/lib/rateLimit.ts - Add MENU_UPLOAD rate limit New Files: 7. /src/lib/r2.ts - R2 client library for PDF uploads 8. /src/app/api/menu/upload/route.ts - PDF upload API endpoint