/** * FeedbackModal — send feedback to the BoostMedia centralized backend. * * Adapted from BoostBot's FeedbackModal, restyled to BMAI's bc-* tailwind tokens * and routed through `api.upload('feedback', formData)` (which proxies to * `POST /wp-json/bmai/v1/feedback`, which proxies to `/api/v1/feedback` on the backend). * * @package BoostMedia_AI * @license GPL-2.0-or-later */ import { useState, useCallback } from 'react' import { MessageSquare, X, Upload, Image as ImageIcon, CheckCircle, Bug, Lightbulb, HelpCircle, MoreHorizontal, AlertCircle, } from 'lucide-react' import { Button } from './Button' import { api } from '../../api/client' import { t } from '../../lib/i18n' interface FeedbackModalProps { isOpen: boolean onClose: () => void } type FeedbackType = 'bug' | 'feature' | 'question' | 'other' function getFeedbackTypes(): { value: FeedbackType; label: string; icon: typeof Bug }[] { return [ { value: 'bug', label: t('Bug Report'), icon: Bug }, { value: 'feature', label: t('Feature Request'), icon: Lightbulb }, { value: 'question', label: t('Question'), icon: HelpCircle }, { value: 'other', label: t('Other'), icon: MoreHorizontal }, ] } export function FeedbackModal({ isOpen, onClose }: FeedbackModalProps) { const [type, setType] = useState('question') const [customType, setCustomType] = useState('') const [message, setMessage] = useState('') const [screenshot, setScreenshot] = useState(null) const [screenshotPreview, setScreenshotPreview] = useState(null) const [isSubmitting, setIsSubmitting] = useState(false) const [success, setSuccess] = useState(false) const [error, setError] = useState(null) const [isDragging, setIsDragging] = useState(false) const handleScreenshotSelect = useCallback((file: File) => { if (!file.type.startsWith('image/')) { setError(t('Please upload an image file only')) return } if (file.size > 5 * 1024 * 1024) { setError(t('Maximum file size is 5MB')) return } setScreenshot(file) setError(null) const reader = new FileReader() reader.onloadend = () => setScreenshotPreview(reader.result as string) reader.readAsDataURL(file) }, []) const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(true) }, []) const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false) }, []) const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false) const file = e.dataTransfer.files[0] if (file) handleScreenshotSelect(file) }, [handleScreenshotSelect]) const handleFileChange = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) handleScreenshotSelect(file) }, [handleScreenshotSelect]) const removeScreenshot = useCallback(() => { setScreenshot(null); setScreenshotPreview(null) }, []) const handleSubmit = async () => { if (message.trim().length < 10) { setError(t('Message must contain at least 10 characters')) return } setIsSubmitting(true); setError(null) try { const formData = new FormData() const feedbackType = type === 'other' && customType.trim() ? `other:${customType.trim()}` : type formData.append('type', feedbackType) formData.append('message', message) formData.append('browser_info', navigator.userAgent) if (screenshot) formData.append('screenshot', screenshot) await api.upload('feedback', formData) setSuccess(true) setTimeout(() => handleClose(), 2000) } catch (err) { console.error('Failed to send feedback:', err) setError(t('An error occurred while sending feedback. Please try again later.')) } finally { setIsSubmitting(false) } } const handleClose = useCallback(() => { setType('question'); setCustomType(''); setMessage('') setScreenshot(null); setScreenshotPreview(null) setSuccess(false); setError(null); setIsSubmitting(false) onClose() }, [onClose]) if (!isOpen) return null return (
e.stopPropagation()} > {/* Header */}

{t('Send Us Feedback')}

{/* Content */}
{success ? (

{t('Thank you for your feedback!')}

{t('We will get back to you as soon as possible')}

) : ( <> {error && (

{error}

)} {/* Feedback Type */}
{getFeedbackTypes().map(({ value, label, icon: Icon }) => ( ))}
{type === 'other' && (
setCustomType(e.target.value)} placeholder={t('Describe the type of inquiry...')} maxLength={50} className="w-full px-4 py-2.5 border border-bc-gray-200 rounded-bc text-sm focus:outline-none focus:ring-2 focus:ring-bc-primary focus:border-transparent placeholder:text-bc-gray-400" />
)}
{/* Message */}