diff --git a/src/app/(app)/create/page.tsx b/src/app/(app)/create/page.tsx index 96ccf6f..888bf09 100644 --- a/src/app/(app)/create/page.tsx +++ b/src/app/(app)/create/page.tsx @@ -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('FREE'); const qrRef = useRef(null); diff --git a/src/app/(app)/qr/[id]/edit/page.tsx b/src/app/(app)/qr/[id]/edit/page.tsx index 48e6806..d2a2fae 100644 --- a/src/app/(app)/qr/[id]/edit/page.tsx +++ b/src/app/(app)/qr/[id]/edit/page.tsx @@ -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 }) => ( +
+ +
+ {text} +
+
+
+); 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(null); const [title, setTitle] = useState(''); const [content, setContent] = useState({}); @@ -45,6 +58,41 @@ export default function EditQRPage() { fetchQRCode(); }, [qrId, router]); + 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); + } + }; + const handleSave = async () => { setSaving(true); @@ -244,19 +292,58 @@ export default function EditQRPage() { {qrCode.contentType === 'PDF' && ( <> - setContent({ ...content, fileUrl: e.target.value })} - placeholder="https://drive.google.com/file/d/.../view" - required - /> - setContent({ ...content, fileName: e.target.value })} - placeholder="Product Catalog 2026" - /> +
+
+ + +
+ +
+
+ {uploading ? ( +
+
+

Uploading...

+
+ ) : content.fileUrl ? ( +
+
+ +
+

Upload Complete!

+ + {content.fileName || 'View File'} + + +
+ ) : ( + <> + +
+ +

or drag and drop

+
+

PDF, PNG, JPG up to 10MB

+ + )} +
+
+
+ + {content.fileUrl && ( + setContent({ ...content, fileName: e.target.value })} + placeholder="Product Catalog 2026" + /> + )} )} diff --git a/src/app/api/qrs/route.ts b/src/app/api/qrs/route.ts index 4b61b24..0a97fde 100644 --- a/src/app/api/qrs/route.ts +++ b/src/app/api/qrs/route.ts @@ -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 diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 856202d..4336754 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -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 }); } diff --git a/src/app/coupon/[slug]/page.tsx b/src/app/coupon/[slug]/page.tsx index 39194fd..a334b3b 100644 --- a/src/app/coupon/[slug]/page.tsx +++ b/src/app/coupon/[slug]/page.tsx @@ -69,30 +69,30 @@ export default function CouponPage() { const isExpired = coupon.expiryDate && new Date(coupon.expiryDate) < new Date(); - return ( -
-
- {/* Card */} -
- {/* Colorful Header */} -
- {/* Decorative circles */} -
-
+ return (
+
+ {/* Card */} +
+ {/* Colorful Header */} +
+ {/* Decorative circles */} +
+
-
-
- -
-

{coupon.title || 'Special Offer'}

-

{coupon.discount}

+
+
+
+

{coupon.title || 'Special Offer'}

+

{coupon.discount}

+
- {/* Dotted line with circles */} + {/* Dotted line with circles */} +
-
-
+
+
@@ -103,8 +103,8 @@ export default function CouponPage() { )} {/* Code Box */} -
-

Your Code

+
+

Your Code

{coupon.code} @@ -112,8 +112,8 @@ export default function CouponPage() {
{/* Footer */} -

+

Powered by QR Master

+
); } diff --git a/src/app/feedback/[slug]/page.tsx b/src/app/feedback/[slug]/page.tsx index 098044c..61b476f 100644 --- a/src/app/feedback/[slug]/page.tsx +++ b/src/app/feedback/[slug]/page.tsx @@ -68,7 +68,7 @@ export default function FeedbackPage() { // Loading if (loading) { return ( -
+
); @@ -77,7 +77,7 @@ export default function FeedbackPage() { // Not found if (!feedback) { return ( -
+

This feedback form is not available.

@@ -88,9 +88,9 @@ export default function FeedbackPage() { // Success if (submitted) { return ( -
+
-
+
@@ -114,17 +114,17 @@ export default function FeedbackPage() { // Rating Form return ( -
+
{/* Card */}
{/* Colored Header */} -
+
-

How was your experience?

-

{feedback.businessName}

+

How was your experience?

+

{feedback.businessName}

{/* Content */} @@ -142,8 +142,8 @@ export default function FeedbackPage() { > @@ -175,8 +175,8 @@ export default function FeedbackPage() { onClick={handleSubmit} 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-gray-100 text-gray-400 cursor-not-allowed' + : 'bg-gradient-to-r from-[#4C5F4E] to-[#0C342C] text-white hover:from-[#5a705c] hover:to-[#0E4036] shadow-lg shadow-emerald-200' }`} > @@ -186,7 +186,7 @@ export default function FeedbackPage() {
{/* Footer */} -

+

Powered by QR Master

diff --git a/src/lib/db.ts b/src/lib/db.ts index fe5ae7e..b521586 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -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; \ No newline at end of file