'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 }) => (
);
// 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 (
);
case 'PDF':
const handleFileUpload = async (e: React.ChangeEvent) => {
const file = e.target.files?.[0];
if (!file) return;
// 10MB limit
if (file.size > 10 * 1024 * 1024) {
showToast('File size too large (max 10MB)', 'error');
return;
}
setUploading(true);
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
const data = await response.json();
if (response.ok) {
setContent({ ...content, fileUrl: data.url, fileName: data.filename });
showToast('File uploaded successfully!', 'success');
} else {
showToast(data.error || 'Upload failed', 'error');
}
} catch (error) {
console.error('Upload error:', error);
showToast('Error uploading file', 'error');
} finally {
setUploading(false);
}
};
return (
<>
{uploading ? (
) : content.fileUrl ? (
) : (
<>
PDF, PNG, JPG up to 10MB
>
)}
{content.fileUrl && (
setContent({ ...content, fileName: e.target.value })}
placeholder="Product Catalog 2026"
/>
)}
>
);
case 'APP':
return (
<>
>
);
case 'COUPON':
return (
<>
setContent({ ...content, code: e.target.value })}
placeholder="SUMMER20"
required
/>
setContent({ ...content, discount: e.target.value })}
placeholder="20% OFF"
required
/>
setContent({ ...content, title: e.target.value })}
placeholder="Summer Sale 2026"
/>
setContent({ ...content, description: e.target.value })}
placeholder="Valid on all products"
/>
setContent({ ...content, expiryDate: e.target.value })}
/>
setContent({ ...content, redeemUrl: e.target.value })}
placeholder="https://shop.example.com?coupon=SUMMER20"
/>
>
);
case 'FEEDBACK':
return (
<>
setContent({ ...content, businessName: e.target.value })}
placeholder="Your Restaurant Name"
required
/>
setContent({ ...content, thankYouMessage: e.target.value })}
placeholder="Thanks for your feedback!"
/>
>
);
default:
return null;
}
};
return (
{t('create.title')}
{t('create.subtitle')}
);
}