464 lines
15 KiB
Markdown
464 lines
15 KiB
Markdown
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<string> {
|
|
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<void> {
|
|
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 |