'use client'; import React, { useState, useEffect, useRef } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { QRCodeSVG } from 'qrcode.react'; import { toPng } from 'html-to-image'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Input } from '@/components/ui/Input'; import { Select } from '@/components/ui/Select'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; import { calculateContrast, cn } from '@/lib/utils'; import { useTranslation } from '@/hooks/useTranslation'; import { useCsrf } from '@/hooks/useCsrf'; import { showToast } from '@/components/ui/Toast'; import { Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload } from 'lucide-react'; // Tooltip component for form field help const Tooltip = ({ text }: { text: string }) => (
{text}
); // Content-type specific frame options const getFrameOptionsForContentType = (contentType: string) => { const baseOptions = [{ id: 'none', label: 'No Frame' }, { id: 'scanme', label: 'Scan Me' }]; switch (contentType) { case 'URL': return [...baseOptions, { id: 'website', label: 'Website' }, { id: 'visit', label: 'Visit' }]; case 'PHONE': return [...baseOptions, { id: 'callme', label: 'Call Me' }, { id: 'call', label: 'Call' }]; case 'GEO': return [...baseOptions, { id: 'findus', label: 'Find Us' }, { id: 'navigate', label: 'Navigate' }]; case 'VCARD': return [...baseOptions, { id: 'contact', label: 'Contact' }, { id: 'save', label: 'Save' }]; case 'SMS': return [...baseOptions, { id: 'textme', label: 'Text Me' }, { id: 'message', label: 'Message' }]; case 'WHATSAPP': return [...baseOptions, { id: 'chatme', label: 'Chat Me' }, { id: 'whatsapp', label: 'WhatsApp' }]; case 'TEXT': return [...baseOptions, { id: 'read', label: 'Read' }, { id: 'info', label: 'Info' }]; case 'PDF': return [...baseOptions, { id: 'download', label: 'Download' }, { id: 'view', label: 'View PDF' }]; case 'APP': return [...baseOptions, { id: 'getapp', label: 'Get App' }, { id: 'download', label: 'Download' }]; case 'COUPON': return [...baseOptions, { id: 'redeem', label: 'Redeem' }, { id: 'save', label: 'Save Offer' }]; case 'FEEDBACK': return [...baseOptions, { id: 'review', label: 'Review' }, { id: 'feedback', label: 'Feedback' }]; default: return [...baseOptions, { id: 'website', label: 'Website' }, { id: 'visit', label: 'Visit' }]; } }; export default function CreatePage() { const router = useRouter(); const { t } = useTranslation(); const { fetchWithCsrf } = useCsrf(); const [loading, setLoading] = useState(false); const [uploading, setUploading] = useState(false); const [userPlan, setUserPlan] = useState('FREE'); const qrRef = useRef(null); // Form state const [title, setTitle] = useState(''); const [contentType, setContentType] = useState('URL'); const [content, setContent] = useState({ url: '' }); const [isDynamic, setIsDynamic] = useState(true); // Style state const [foregroundColor, setForegroundColor] = useState('#000000'); const [backgroundColor, setBackgroundColor] = useState('#FFFFFF'); const [cornerStyle, setCornerStyle] = useState('square'); const [size, setSize] = useState(200); const [frameType, setFrameType] = useState('none'); // Get frame options for current content type const frameOptions = getFrameOptionsForContentType(contentType); // Reset frame type when content type changes (if current frame is not valid) useEffect(() => { const validIds = frameOptions.map(f => f.id); if (!validIds.includes(frameType)) { setFrameType('none'); } }, [contentType, frameOptions, frameType]); // Logo state const [logoUrl, setLogoUrl] = useState(''); const [logoSize, setLogoSize] = useState(24); const [excavate, setExcavate] = useState(true); // QR preview const [qrDataUrl, setQrDataUrl] = useState(''); // Check if user can customize colors (PRO+ only) const canCustomizeColors = userPlan === 'PRO' || userPlan === 'BUSINESS'; // Load user plan useEffect(() => { const fetchUserPlan = async () => { try { const response = await fetch('/api/user/plan'); if (response.ok) { const data = await response.json(); setUserPlan(data.plan || 'FREE'); } } catch (error) { console.error('Error fetching user plan:', error); } }; fetchUserPlan(); }, []); const contrast = calculateContrast(foregroundColor, backgroundColor); const hasGoodContrast = contrast >= 4.5; const contentTypes = [ { value: 'URL', label: 'URL / Website', icon: Globe }, { value: 'VCARD', label: 'Contact Card', icon: User }, { value: 'GEO', label: 'Location / Maps', icon: MapPin }, { value: 'PHONE', label: 'Phone Number', icon: Phone }, { value: 'PDF', label: 'PDF / File', icon: FileText }, { value: 'APP', label: 'App Download', icon: Smartphone }, { value: 'COUPON', label: 'Coupon / Discount', icon: Ticket }, { value: 'FEEDBACK', label: 'Feedback / Review', icon: Star }, ]; // Get QR content based on content type const getQRContent = () => { switch (contentType) { case 'URL': return content.url || 'https://example.com'; case 'PHONE': return `tel:${content.phone || '+1234567890'}`; case 'SMS': return `sms:${content.phone || '+1234567890'}${content.message ? `?body=${encodeURIComponent(content.message)}` : ''}`; case 'VCARD': return `BEGIN:VCARD\nVERSION:3.0\nFN:${content.firstName || 'John'} ${content.lastName || 'Doe'}\nORG:${content.organization || 'Company'}\nTITLE:${content.title || 'Position'}\nEMAIL:${content.email || 'email@example.com'}\nTEL:${content.phone || '+1234567890'}\nEND:VCARD`; case 'GEO': const lat = content.latitude || 37.7749; const lon = content.longitude || -122.4194; const label = content.label ? `?q=${encodeURIComponent(content.label)}` : ''; return `geo:${lat},${lon}${label}`; case 'TEXT': return content.text || 'Sample text'; case 'WHATSAPP': return `https://wa.me/${content.phone || '+1234567890'}${content.message ? `?text=${encodeURIComponent(content.message)}` : ''}`; case 'PDF': return content.fileUrl || 'https://example.com/file.pdf'; case 'APP': return content.fallbackUrl || content.iosUrl || content.androidUrl || 'https://example.com/app'; case 'COUPON': return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`; case 'FEEDBACK': return content.feedbackUrl || 'https://example.com/feedback'; default: return 'https://example.com'; } }; const qrContent = getQRContent(); const getFrameLabel = () => { const frame = frameOptions.find((f: { id: string; label: string }) => f.id === frameType); return frame?.id !== 'none' ? frame?.label : null; }; const downloadQR = async (format: 'svg' | 'png') => { if (!qrRef.current) return; try { if (format === 'png') { const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' }); const link = document.createElement('a'); link.download = `qrcode-${title || 'download'}.png`; link.href = dataUrl; link.click(); } else { // For SVG, we might still want to use the library or just toPng if SVG export of HTML is not needed // Simplest is to check if we can export the SVG element directly but that misses the frame HTML. // html-to-image can generate SVG too. // But usually for SVG users want the vector. Capturing HTML to SVG is possible but complex. // For now, let's just stick to the SVG code export if NO FRAME is selected, // otherwise warn or use toPng (as SVG). // Actually, the previous implementation was good for pure QR. // If frame is selected, we MUST use a raster export (PNG) or complex HTML-to-SVG. // Let's rely on toPng for consistency with frames. const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' }); // Wait, exporting HTML to valid vector SVG is hard. // Let's just offer PNG for frames for now to be safe, or just use the same PNG download for both buttons if frame is active? // No, let's try to grab the INNER SVG if no frame, else... if (frameType === 'none') { const svgElement = qrRef.current.querySelector('svg'); if (svgElement) { const svgData = new XMLSerializer().serializeToString(svgElement); const blob = new Blob([svgData], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `qrcode-${title || 'download'}.svg`; a.click(); URL.revokeObjectURL(url); } } else { showToast('SVG download not available with frames yet. Downloading PNG instead.', 'info'); const dataUrl = await toPng(qrRef.current, { cacheBust: true, pixelRatio: 3, backgroundColor: 'transparent' }); const link = document.createElement('a'); link.download = `qrcode-${title || 'download'}.png`; link.href = dataUrl; link.click(); } } } catch (err) { console.error('Error downloading QR code:', err); showToast('Error downloading QR code', 'error'); } }; const handleLogoUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { if (file.size > 10 * 1024 * 1024) { // 10MB limit (soft limit for upload, will be resized) showToast('Logo file size too large (max 10MB)', 'error'); return; } const reader = new FileReader(); reader.onload = (evt) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); const maxDimension = 500; // Resize to max 500px let width = img.width; let height = img.height; if (width > maxDimension || height > maxDimension) { if (width > height) { height = Math.round((height * maxDimension) / width); width = maxDimension; } else { width = Math.round((width * maxDimension) / height); height = maxDimension; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx?.drawImage(img, 0, 0, width, height); // Compress to JPEG/PNG with reduced quality to save space const dataUrl = canvas.toDataURL(file.type === 'image/png' ? 'image/png' : 'image/jpeg', 0.8); setLogoUrl(dataUrl); }; img.src = evt.target?.result as string; }; reader.readAsDataURL(file); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); try { const qrData = { title, contentType, content, isStatic: !isDynamic, tags: [], style: { // FREE users can only use black/white foregroundColor: canCustomizeColors ? foregroundColor : '#000000', backgroundColor: canCustomizeColors ? backgroundColor : '#FFFFFF', cornerStyle, size, imageSettings: (canCustomizeColors && logoUrl) ? { src: logoUrl, height: logoSize, width: logoSize, excavate, } : undefined, frameType, // Save frame type }, }; console.log('SENDING QR DATA:', qrData); const response = await fetchWithCsrf('/api/qrs', { method: 'POST', body: JSON.stringify(qrData), }); const responseData = await response.json(); console.log('RESPONSE DATA:', responseData); if (response.ok) { showToast(`QR Code "${title}" created successfully!`, 'success'); // Wait a moment so user sees the toast, then redirect setTimeout(() => { router.push('/dashboard'); router.refresh(); }, 1000); } else { console.error('Error creating QR code:', responseData); showToast(responseData.error || 'Error creating QR code', 'error'); } } catch (error) { console.error('Error creating QR code:', error); showToast('Error creating QR code. Please try again.', 'error'); } finally { setLoading(false); } }; const renderContentFields = () => { switch (contentType) { case 'URL': return ( setContent({ url: e.target.value })} placeholder="https://example.com" required /> ); case 'PHONE': return ( setContent({ phone: e.target.value })} placeholder="+1234567890" required /> ); case 'VCARD': return ( <> setContent({ ...content, firstName: e.target.value })} placeholder="John" required /> setContent({ ...content, lastName: e.target.value })} placeholder="Doe" required /> setContent({ ...content, email: e.target.value })} placeholder="john@example.com" /> setContent({ ...content, phone: e.target.value })} placeholder="+1234567890" /> setContent({ ...content, organization: e.target.value })} placeholder="Company Name" /> setContent({ ...content, title: e.target.value })} placeholder="CEO" /> ); case 'GEO': return ( <> setContent({ ...content, latitude: parseFloat(e.target.value) || 0 })} placeholder="37.7749" required /> setContent({ ...content, longitude: parseFloat(e.target.value) || 0 })} placeholder="-122.4194" required /> setContent({ ...content, label: e.target.value })} placeholder="Golden Gate Bridge" /> ); case 'TEXT': return (