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 { useCsrf } from '@/hooks/useCsrf';
|
||||||
import { showToast } from '@/components/ui/Toast';
|
import { showToast } from '@/components/ui/Toast';
|
||||||
import {
|
import {
|
||||||
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle
|
Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// Tooltip component for form field help
|
// Tooltip component for form field help
|
||||||
|
|
@ -66,6 +66,7 @@ export default function CreatePage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { fetchWithCsrf } = useCsrf();
|
const { fetchWithCsrf } = useCsrf();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
const [userPlan, setUserPlan] = useState<string>('FREE');
|
const [userPlan, setUserPlan] = useState<string>('FREE');
|
||||||
const qrRef = useRef<HTMLDivElement>(null);
|
const qrRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,18 @@ import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { showToast } from '@/components/ui/Toast';
|
import { showToast } from '@/components/ui/Toast';
|
||||||
import { useCsrf } from '@/hooks/useCsrf';
|
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() {
|
export default function EditQRPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -16,6 +28,7 @@ export default function EditQRPage() {
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
const [qrCode, setQrCode] = useState<any>(null);
|
const [qrCode, setQrCode] = useState<any>(null);
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [content, setContent] = useState<any>({});
|
const [content, setContent] = useState<any>({});
|
||||||
|
|
@ -45,6 +58,41 @@ export default function EditQRPage() {
|
||||||
fetchQRCode();
|
fetchQRCode();
|
||||||
}, [qrId, router]);
|
}, [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 () => {
|
const handleSave = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
|
||||||
|
|
@ -244,19 +292,58 @@ export default function EditQRPage() {
|
||||||
|
|
||||||
{qrCode.contentType === 'PDF' && (
|
{qrCode.contentType === 'PDF' && (
|
||||||
<>
|
<>
|
||||||
<Input
|
<div>
|
||||||
label="PDF/File URL"
|
<div className="flex items-center mb-1">
|
||||||
value={content.fileUrl || ''}
|
<label className="block text-sm font-medium text-gray-700">Upload Menu / PDF</label>
|
||||||
onChange={(e) => setContent({ ...content, fileUrl: e.target.value })}
|
<Tooltip text="Upload your menu PDF (Max 10MB). Hosted securely." />
|
||||||
placeholder="https://drive.google.com/file/d/.../view"
|
</div>
|
||||||
required
|
|
||||||
/>
|
<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">
|
||||||
<Input
|
<div className="space-y-1 text-center">
|
||||||
label="File Name (optional)"
|
{uploading ? (
|
||||||
value={content.fileName || ''}
|
<div className="flex flex-col items-center">
|
||||||
onChange={(e) => setContent({ ...content, fileName: e.target.value })}
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-500 mb-2"></div>
|
||||||
placeholder="Product Catalog 2026"
|
<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="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;
|
const userId = cookies().get('userId')?.value;
|
||||||
console.log('POST /api/qrs - userId from cookie:', userId);
|
|
||||||
|
|
||||||
// Rate Limiting (user-based)
|
// Rate Limiting (user-based)
|
||||||
const clientId = userId || getClientIdentifier(request);
|
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 });
|
return NextResponse.json({ error: 'Unauthorized - no userId cookie' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user exists and get their plan
|
|
||||||
const user = await db.user.findUnique({
|
const user = await db.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
select: { plan: true },
|
select: { plan: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('User exists:', !!user);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return NextResponse.json({ error: `User not found: ${userId}` }, { status: 404 });
|
return NextResponse.json({ error: `User not found: ${userId}` }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
console.log('Request body:', body);
|
|
||||||
|
|
||||||
// Validate request body with Zod (only for non-static QRs or simplified validation)
|
// 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
|
// 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 { authOptions } from '@/lib/auth';
|
||||||
import { uploadFileToR2 } from '@/lib/r2';
|
import { uploadFileToR2 } from '@/lib/r2';
|
||||||
import { env } from '@/lib/env';
|
import { env } from '@/lib/env';
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// 1. Authentication Check
|
// 1. Authentication Check
|
||||||
const session = await getServerSession(authOptions);
|
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 });
|
return new NextResponse('Unauthorized', { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,30 +69,30 @@ export default function CouponPage() {
|
||||||
|
|
||||||
const isExpired = coupon.expiryDate && new Date(coupon.expiryDate) < new Date();
|
const isExpired = coupon.expiryDate && new Date(coupon.expiryDate) < new Date();
|
||||||
|
|
||||||
return (
|
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="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">
|
<div className="max-w-sm w-full">
|
||||||
<div className="max-w-sm w-full">
|
{/* Card */}
|
||||||
{/* Card */}
|
<div className="bg-white rounded-3xl shadow-xl overflow-hidden">
|
||||||
<div className="bg-white rounded-3xl shadow-xl overflow-hidden">
|
{/* Colorful Header */}
|
||||||
{/* Colorful Header */}
|
<div className="bg-gradient-to-br from-[#DB5375] to-[#B3FFB3] text-gray-900 p-8 text-center relative overflow-hidden">
|
||||||
<div className="bg-gradient-to-r from-rose-500 to-orange-500 text-white p-8 text-center relative overflow-hidden">
|
{/* Decorative circles */}
|
||||||
{/* 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 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="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="relative">
|
||||||
<div className="w-14 h-14 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
<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-white" />
|
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Dotted line with circles */}
|
{/* Dotted line with circles */}
|
||||||
|
<div className="relative py-4">
|
||||||
<div className="relative py-4">
|
<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 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-rose-50 via-pink-50 to-orange-50 rounded-l-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 className="border-t-2 border-dashed border-gray-200 mx-8"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -103,8 +103,8 @@ export default function CouponPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Code Box */}
|
{/* Code Box */}
|
||||||
<div className="bg-gradient-to-r from-rose-50 to-orange-50 rounded-2xl p-5 mb-4 border border-rose-100">
|
<div className="bg-gray-50 rounded-2xl p-5 mb-4 border border-emerald-100">
|
||||||
<p className="text-xs text-rose-400 text-center mb-2 font-medium uppercase tracking-wider">Your Code</p>
|
<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">
|
<div className="flex items-center justify-center gap-3">
|
||||||
<code className="text-2xl font-mono font-bold text-gray-900 tracking-wider">
|
<code className="text-2xl font-mono font-bold text-gray-900 tracking-wider">
|
||||||
{coupon.code}
|
{coupon.code}
|
||||||
|
|
@ -112,8 +112,8 @@ export default function CouponPage() {
|
||||||
<button
|
<button
|
||||||
onClick={copyCode}
|
onClick={copyCode}
|
||||||
className={`p-2.5 rounded-xl transition-all ${copied
|
className={`p-2.5 rounded-xl transition-all ${copied
|
||||||
? 'bg-emerald-100 text-emerald-600'
|
? 'bg-emerald-100 text-emerald-600'
|
||||||
: 'bg-white text-gray-500 hover:text-rose-500 shadow-sm hover:shadow'
|
: 'bg-white text-gray-500 hover:text-rose-500 shadow-sm hover:shadow'
|
||||||
}`}
|
}`}
|
||||||
aria-label="Copy code"
|
aria-label="Copy code"
|
||||||
>
|
>
|
||||||
|
|
@ -145,7 +145,7 @@ export default function CouponPage() {
|
||||||
href={coupon.redeemUrl}
|
href={coupon.redeemUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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">
|
<span className="flex items-center justify-center gap-2">
|
||||||
Redeem Now
|
Redeem Now
|
||||||
|
|
@ -157,10 +157,11 @@ export default function CouponPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* 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
|
Powered by QR Master
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export default function FeedbackPage() {
|
||||||
// Loading
|
// Loading
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
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 className="w-10 h-10 border-3 border-indigo-200 border-t-indigo-600 rounded-full animate-spin"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -77,7 +77,7 @@ export default function FeedbackPage() {
|
||||||
// Not found
|
// Not found
|
||||||
if (!feedback) {
|
if (!feedback) {
|
||||||
return (
|
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">
|
<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>
|
<p className="text-gray-500 text-lg">This feedback form is not available.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -88,9 +88,9 @@ export default function FeedbackPage() {
|
||||||
// Success
|
// Success
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
return (
|
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="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} />
|
<Check className="w-10 h-10 text-white" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -114,17 +114,17 @@ export default function FeedbackPage() {
|
||||||
|
|
||||||
// Rating Form
|
// Rating Form
|
||||||
return (
|
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">
|
<div className="max-w-md w-full">
|
||||||
{/* Card */}
|
{/* Card */}
|
||||||
<div className="bg-white rounded-3xl shadow-xl overflow-hidden">
|
<div className="bg-white rounded-3xl shadow-xl overflow-hidden">
|
||||||
{/* Colored Header */}
|
{/* 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">
|
<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" />
|
<Star className="w-7 h-7 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold mb-1">How was your experience?</h1>
|
<h1 className="text-2xl font-bold mb-1 text-gray-900">How was your experience?</h1>
|
||||||
<p className="text-white/80">{feedback.businessName}</p>
|
<p className="text-gray-700">{feedback.businessName}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
|
|
@ -142,8 +142,8 @@ export default function FeedbackPage() {
|
||||||
>
|
>
|
||||||
<Star
|
<Star
|
||||||
className={`w-11 h-11 transition-all ${star <= (hoverRating || rating)
|
className={`w-11 h-11 transition-all ${star <= (hoverRating || rating)
|
||||||
? 'text-amber-400 fill-amber-400 drop-shadow-sm'
|
? 'text-amber-400 fill-amber-400 drop-shadow-sm'
|
||||||
: 'text-gray-200'
|
: 'text-gray-200'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -175,8 +175,8 @@ export default function FeedbackPage() {
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={rating === 0 || submitting}
|
disabled={rating === 0 || submitting}
|
||||||
className={`w-full py-4 rounded-xl font-semibold flex items-center justify-center gap-2 transition-all ${rating === 0
|
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-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" />
|
<Send className="w-4 h-4" />
|
||||||
|
|
@ -186,7 +186,7 @@ export default function FeedbackPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* 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
|
Powered by QR Master
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ const globalForPrisma = globalThis as unknown as {
|
||||||
|
|
||||||
export const db =
|
export const db =
|
||||||
globalForPrisma.prisma ??
|
globalForPrisma.prisma ??
|
||||||
new PrismaClient({
|
new PrismaClient();
|
||||||
log: ['query'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
|
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
|
||||||
Loading…
Reference in New Issue