'use client'; import { ChangeEvent, FormEvent, useState } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { ArrowLeft, Bug, Image as ImageIcon, Loader2, MessageSquareQuote, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; type FeedbackCategory = 'BUG' | 'FEATURE' | 'OTHER'; type TabValue = 'feedback' | 'review'; interface FormStatus { type: 'success' | 'error'; message: string; } export default function FeedbackPage() { const [activeTab, setActiveTab] = useState('feedback'); const [status, setStatus] = useState(null); const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false); const [isSubmittingReview, setIsSubmittingReview] = useState(false); const [feedbackTitle, setFeedbackTitle] = useState(''); const [feedbackCategory, setFeedbackCategory] = useState('BUG'); const [feedbackMessage, setFeedbackMessage] = useState(''); const [feedbackScreenshotFiles, setFeedbackScreenshotFiles] = useState([]); const [feedbackScreenshotPreviewUrls, setFeedbackScreenshotPreviewUrls] = useState([]); const [reviewTitle, setReviewTitle] = useState(''); const [reviewMessage, setReviewMessage] = useState(''); const [reviewRating, setReviewRating] = useState('5'); const [allowShowcase, setAllowShowcase] = useState(false); const uploadFeedbackScreenshot = async (file: File): Promise => { const formData = new FormData(); formData.append('image', file); const response = await fetch('/api/feedback/upload', { method: 'POST', body: formData, }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to upload screenshot'); } return data.data.url as string; }; const handleFeedbackScreenshotChange = (event: ChangeEvent) => { const selectedFiles = Array.from(event.target.files ?? []); if (selectedFiles.length === 0) return; const remainingSlots = 5 - feedbackScreenshotFiles.length; if (remainingSlots <= 0) { setStatus({ type: 'error', message: 'You can upload up to 5 screenshots.' }); return; } const allowedFiles = selectedFiles.slice(0, remainingSlots); const nextFiles: File[] = []; const nextPreviewUrls: string[] = []; for (const file of allowedFiles) { const isImage = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'].includes(file.type); if (!isImage) { setStatus({ type: 'error', message: 'Unsupported screenshot format. Use JPG, PNG, WEBP, or GIF.', }); continue; } if (file.size > 10 * 1024 * 1024) { setStatus({ type: 'error', message: 'Each screenshot must be smaller than 10MB.' }); continue; } nextFiles.push(file); nextPreviewUrls.push(URL.createObjectURL(file)); } if (nextFiles.length > 0) { setFeedbackScreenshotFiles((prev) => [...prev, ...nextFiles]); setFeedbackScreenshotPreviewUrls((prev) => [...prev, ...nextPreviewUrls]); } event.target.value = ''; }; const removeFeedbackScreenshot = (index: number) => { const targetUrl = feedbackScreenshotPreviewUrls[index]; if (targetUrl) URL.revokeObjectURL(targetUrl); setFeedbackScreenshotFiles((prev) => prev.filter((_, currentIndex) => currentIndex !== index)); setFeedbackScreenshotPreviewUrls((prev) => prev.filter((_, currentIndex) => currentIndex !== index) ); }; const clearFeedbackScreenshots = () => { feedbackScreenshotPreviewUrls.forEach((url) => URL.revokeObjectURL(url)); setFeedbackScreenshotFiles([]); setFeedbackScreenshotPreviewUrls([]); }; const handleFeedbackSubmit = async (event: FormEvent) => { event.preventDefault(); setStatus(null); setIsSubmittingFeedback(true); try { const screenshotUrls = await Promise.all( feedbackScreenshotFiles.map((file) => uploadFeedbackScreenshot(file)) ); const response = await fetch('/api/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'FEEDBACK', category: feedbackCategory, title: feedbackTitle, message: feedbackMessage, screenshotUrls, }), }); const data = await response.json(); if (!response.ok) { setStatus({ type: 'error', message: data.error || 'Failed to submit feedback' }); return; } setFeedbackTitle(''); setFeedbackCategory('BUG'); setFeedbackMessage(''); clearFeedbackScreenshots(); setStatus({ type: 'success', message: 'Feedback submitted. Thank you.' }); } catch { setStatus({ type: 'error', message: 'Failed to submit feedback' }); } finally { setIsSubmittingFeedback(false); } }; const handleReviewSubmit = async (event: FormEvent) => { event.preventDefault(); setStatus(null); setIsSubmittingReview(true); try { const response = await fetch('/api/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'REVIEW', title: reviewTitle, message: reviewMessage, rating: Number(reviewRating), allowShowcase, }), }); const data = await response.json(); if (!response.ok) { setStatus({ type: 'error', message: data.error || 'Failed to submit review' }); return; } setReviewTitle(''); setReviewMessage(''); setReviewRating('5'); setAllowShowcase(false); setStatus({ type: 'success', message: 'Review submitted. Thank you for sharing your experience.', }); } catch { setStatus({ type: 'error', message: 'Failed to submit review' }); } finally { setIsSubmittingReview(false); } }; return (
Back to Dashboard Feedback & Review Send product feedback, report bugs, or share a review we can feature on the landing page. {status && (
{status.message}
)} setActiveTab(value as TabValue)} className="w-full" > Feedback Review
setFeedbackTitle(event.target.value)} placeholder="Short summary" minLength={3} maxLength={120} required disabled={isSubmittingFeedback} />