Final
This commit is contained in:
parent
1251584b13
commit
3682673852
|
|
@ -15,7 +15,7 @@ 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
|
||||
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload
|
||||
} from 'lucide-react';
|
||||
|
||||
// Tooltip component for form field help
|
||||
|
|
@ -66,6 +66,7 @@ export default function CreatePage() {
|
|||
const { t } = useTranslation();
|
||||
const { fetchWithCsrf } = useCsrf();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [userPlan, setUserPlan] = useState<string>('FREE');
|
||||
const qrRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,18 @@ import { Button } from '@/components/ui/Button';
|
|||
import { Input } from '@/components/ui/Input';
|
||||
import { showToast } from '@/components/ui/Toast';
|
||||
import { useCsrf } from '@/hooks/useCsrf';
|
||||
import { Upload, FileText, HelpCircle } from 'lucide-react';
|
||||
|
||||
// Tooltip component for form field help
|
||||
const Tooltip = ({ text }: { text: string }) => (
|
||||
<div className="group relative inline-block ml-1">
|
||||
<HelpCircle className="w-4 h-4 text-gray-400 cursor-help" />
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 text-white text-xs rounded-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50 w-48 text-center">
|
||||
{text}
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function EditQRPage() {
|
||||
const router = useRouter();
|
||||
|
|
@ -16,6 +28,7 @@ export default function EditQRPage() {
|
|||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [qrCode, setQrCode] = useState<any>(null);
|
||||
const [title, setTitle] = useState('');
|
||||
const [content, setContent] = useState<any>({});
|
||||
|
|
@ -45,6 +58,41 @@ export default function EditQRPage() {
|
|||
fetchQRCode();
|
||||
}, [qrId, router]);
|
||||
|
||||
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
|
||||
|
|
@ -244,19 +292,58 @@ export default function EditQRPage() {
|
|||
|
||||
{qrCode.contentType === 'PDF' && (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex items-center mb-1">
|
||||
<label className="block text-sm font-medium text-gray-700">Upload Menu / PDF</label>
|
||||
<Tooltip text="Upload your menu PDF (Max 10MB). Hosted securely." />
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-lg hover:bg-gray-50 transition-colors relative">
|
||||
<div className="space-y-1 text-center">
|
||||
{uploading ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-500 mb-2"></div>
|
||||
<p className="text-sm text-gray-500">Uploading...</p>
|
||||
</div>
|
||||
) : content.fileUrl ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mx-auto h-12 w-12 text-primary-500 bg-primary-50 rounded-full flex items-center justify-center mb-2">
|
||||
<FileText className="h-6 w-6" />
|
||||
</div>
|
||||
<p className="text-sm text-green-600 font-medium mb-1">Upload Complete!</p>
|
||||
<a href={content.fileUrl} target="_blank" rel="noopener noreferrer" className="text-xs text-primary-500 hover:underline break-all max-w-xs mb-3 block">
|
||||
{content.fileName || 'View File'}
|
||||
</a>
|
||||
<label htmlFor="file-upload" className="cursor-pointer bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
|
||||
<span>Replace File</span>
|
||||
<input id="file-upload" name="file-upload" type="file" className="sr-only" accept=".pdf,image/*" onChange={handleFileUpload} />
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Upload className="mx-auto h-12 w-12 text-gray-400" />
|
||||
<div className="flex text-sm text-gray-600 justify-center">
|
||||
<label htmlFor="file-upload" className="relative cursor-pointer bg-white rounded-md font-medium text-primary-600 hover:text-primary-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-primary-500">
|
||||
<span>Upload a file</span>
|
||||
<input id="file-upload" name="file-upload" type="file" className="sr-only" accept=".pdf,image/*" onChange={handleFileUpload} />
|
||||
</label>
|
||||
<p className="pl-1">or drag and drop</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">PDF, PNG, JPG up to 10MB</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{content.fileUrl && (
|
||||
<Input
|
||||
label="PDF/File URL"
|
||||
value={content.fileUrl || ''}
|
||||
onChange={(e) => setContent({ ...content, fileUrl: e.target.value })}
|
||||
placeholder="https://drive.google.com/file/d/.../view"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="File Name (optional)"
|
||||
label="File Name / Menu Title"
|
||||
value={content.fileName || ''}
|
||||
onChange={(e) => setContent({ ...content, fileName: e.target.value })}
|
||||
placeholder="Product Catalog 2026"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ export async function POST(request: NextRequest) {
|
|||
}
|
||||
|
||||
const userId = cookies().get('userId')?.value;
|
||||
console.log('POST /api/qrs - userId from cookie:', userId);
|
||||
|
||||
// Rate Limiting (user-based)
|
||||
const clientId = userId || getClientIdentifier(request);
|
||||
|
|
@ -90,20 +89,16 @@ export async function POST(request: NextRequest) {
|
|||
return NextResponse.json({ error: 'Unauthorized - no userId cookie' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Check if user exists and get their plan
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { plan: true },
|
||||
});
|
||||
|
||||
console.log('User exists:', !!user);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: `User not found: ${userId}` }, { status: 404 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
console.log('Request body:', body);
|
||||
|
||||
// Validate request body with Zod (only for non-static QRs or simplified validation)
|
||||
// Note: Static QRs have complex nested content structure, so we do basic validation
|
||||
|
|
|
|||
|
|
@ -3,12 +3,30 @@ import { getServerSession } from 'next-auth';
|
|||
import { authOptions } from '@/lib/auth';
|
||||
import { uploadFileToR2 } from '@/lib/r2';
|
||||
import { env } from '@/lib/env';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// 1. Authentication Check
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session || !session.user) {
|
||||
let userId = session?.user?.id;
|
||||
|
||||
// Fallback: Check for simple-login cookie if no NextAuth session
|
||||
if (!userId) {
|
||||
const cookieUserId = request.cookies.get('userId')?.value;
|
||||
if (cookieUserId) {
|
||||
// Verify user exists
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: cookieUserId },
|
||||
select: { id: true }
|
||||
});
|
||||
if (user) {
|
||||
userId = user.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
return new NextResponse('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,30 +69,30 @@ export default function CouponPage() {
|
|||
|
||||
const isExpired = coupon.expiryDate && new Date(coupon.expiryDate) < new Date();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-rose-50 via-pink-50 to-orange-50 px-6 py-12">
|
||||
return (<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#FAF8F5] via-[#C6C0B3] to-[#4C5F4E] px-6 py-12">
|
||||
<div className="max-w-sm w-full">
|
||||
{/* Card */}
|
||||
<div className="bg-white rounded-3xl shadow-xl overflow-hidden">
|
||||
{/* Colorful Header */}
|
||||
<div className="bg-gradient-to-r from-rose-500 to-orange-500 text-white p-8 text-center relative overflow-hidden">
|
||||
<div className="bg-gradient-to-br from-[#DB5375] to-[#B3FFB3] text-gray-900 p-8 text-center relative overflow-hidden">
|
||||
{/* Decorative circles */}
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/2"></div>
|
||||
<div className="absolute bottom-0 left-0 w-24 h-24 bg-white/10 rounded-full translate-y-1/2 -translate-x-1/2"></div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="w-14 h-14 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<Gift className="w-7 h-7 text-white" />
|
||||
<div className="w-14 h-14 bg-[#DB5375]/10 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<Gift className="w-7 h-7 text-[#DB5375]" />
|
||||
</div>
|
||||
<p className="text-white/80 text-sm mb-1">{coupon.title || 'Special Offer'}</p>
|
||||
<p className="text-4xl font-bold tracking-tight">{coupon.discount}</p>
|
||||
<p className="text-gray-700 text-sm mb-1">{coupon.title || 'Special Offer'}</p>
|
||||
<p className="text-4xl font-bold tracking-tight text-gray-900">{coupon.discount}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dotted line with circles */}
|
||||
<div className="relative py-4">
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-5 h-10 bg-gradient-to-br from-rose-50 via-pink-50 to-orange-50 rounded-r-full"></div>
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-5 h-10 bg-gradient-to-br from-rose-50 via-pink-50 to-orange-50 rounded-l-full"></div>
|
||||
<div className="relative py-4">
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-5 h-10 bg-gradient-to-br from-[#FAF8F5] via-[#C6C0B3] to-[#4C5F4E] rounded-r-full"></div>
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-5 h-10 bg-gradient-to-br from-[#FAF8F5] via-[#C6C0B3] to-[#4C5F4E] rounded-l-full"></div>
|
||||
<div className="border-t-2 border-dashed border-gray-200 mx-8"></div>
|
||||
</div>
|
||||
|
||||
|
|
@ -103,8 +103,8 @@ export default function CouponPage() {
|
|||
)}
|
||||
|
||||
{/* Code Box */}
|
||||
<div className="bg-gradient-to-r from-rose-50 to-orange-50 rounded-2xl p-5 mb-4 border border-rose-100">
|
||||
<p className="text-xs text-rose-400 text-center mb-2 font-medium uppercase tracking-wider">Your Code</p>
|
||||
<div className="bg-gray-50 rounded-2xl p-5 mb-4 border border-emerald-100">
|
||||
<p className="text-xs text-emerald-600 text-center mb-2 font-medium uppercase tracking-wider">Your Code</p>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<code className="text-2xl font-mono font-bold text-gray-900 tracking-wider">
|
||||
{coupon.code}
|
||||
|
|
@ -145,7 +145,7 @@ export default function CouponPage() {
|
|||
href={coupon.redeemUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full py-4 rounded-xl font-semibold text-center bg-gradient-to-r from-rose-500 to-orange-500 text-white hover:from-rose-600 hover:to-orange-600 transition-all shadow-lg shadow-rose-200"
|
||||
className="block w-full py-4 rounded-xl font-semibold text-center bg-gradient-to-r from-[#076653] to-[#0C342C] text-white hover:from-[#087861] hover:to-[#0E4036] transition-all shadow-lg shadow-emerald-200"
|
||||
>
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
Redeem Now
|
||||
|
|
@ -157,10 +157,11 @@ export default function CouponPage() {
|
|||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<p className="text-center text-sm text-rose-300 mt-6">
|
||||
<p className="text-center text-sm text-white/60 mt-6">
|
||||
Powered by QR Master
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export default function FeedbackPage() {
|
|||
// Loading
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#FAF8F5] via-[#C6C0B3] to-[#4C5F4E]">
|
||||
<div className="w-10 h-10 border-3 border-indigo-200 border-t-indigo-600 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -77,7 +77,7 @@ export default function FeedbackPage() {
|
|||
// Not found
|
||||
if (!feedback) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-6">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#FAF8F5] via-[#C6C0B3] to-[#4C5F4E] px-6">
|
||||
<div className="text-center bg-white rounded-2xl p-8 shadow-lg">
|
||||
<p className="text-gray-500 text-lg">This feedback form is not available.</p>
|
||||
</div>
|
||||
|
|
@ -88,9 +88,9 @@ export default function FeedbackPage() {
|
|||
// Success
|
||||
if (submitted) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-emerald-50 to-teal-100 px-6">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#FAF8F5] via-[#C6C0B3] to-[#4C5F4E] px-6">
|
||||
<div className="max-w-sm w-full bg-white rounded-3xl shadow-xl p-10 text-center">
|
||||
<div className="w-20 h-20 bg-gradient-to-br from-emerald-400 to-teal-500 rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg">
|
||||
<div className="w-20 h-20 bg-gradient-to-br from-[#4C5F4E] to-[#FAF8F5] rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg">
|
||||
<Check className="w-10 h-10 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
|
||||
|
|
@ -114,17 +114,17 @@ export default function FeedbackPage() {
|
|||
|
||||
// Rating Form
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 px-6 py-12">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#FAF8F5] via-[#C6C0B3] to-[#4C5F4E] px-6 py-12">
|
||||
<div className="max-w-md w-full">
|
||||
{/* Card */}
|
||||
<div className="bg-white rounded-3xl shadow-xl overflow-hidden">
|
||||
{/* Colored Header */}
|
||||
<div className="bg-gradient-to-r from-indigo-500 to-purple-600 text-white p-8 text-center">
|
||||
<div className="bg-gradient-to-r from-[#4C5F4E] via-[#C6C0B3] to-[#FAF8F5] p-8 text-center">
|
||||
<div className="w-14 h-14 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<Star className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold mb-1">How was your experience?</h1>
|
||||
<p className="text-white/80">{feedback.businessName}</p>
|
||||
<h1 className="text-2xl font-bold mb-1 text-gray-900">How was your experience?</h1>
|
||||
<p className="text-gray-700">{feedback.businessName}</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
|
@ -176,7 +176,7 @@ export default function FeedbackPage() {
|
|||
disabled={rating === 0 || submitting}
|
||||
className={`w-full py-4 rounded-xl font-semibold flex items-center justify-center gap-2 transition-all ${rating === 0
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-gradient-to-r from-indigo-500 to-purple-600 text-white hover:from-indigo-600 hover:to-purple-700 shadow-lg shadow-indigo-200'
|
||||
: 'bg-gradient-to-r from-[#4C5F4E] to-[#0C342C] text-white hover:from-[#5a705c] hover:to-[#0E4036] shadow-lg shadow-emerald-200'
|
||||
}`}
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
|
|
@ -186,7 +186,7 @@ export default function FeedbackPage() {
|
|||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<p className="text-center text-sm text-indigo-300 mt-6">
|
||||
<p className="text-center text-sm text-white/60 mt-6">
|
||||
Powered by QR Master
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ const globalForPrisma = globalThis as unknown as {
|
|||
|
||||
export const db =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: ['query'],
|
||||
});
|
||||
new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
|
||||
Loading…
Reference in New Issue