'use client' import { useScene } from '@pascal-app/core' import { ImageIcon, MessageSquare, X } from 'lucide-react' import { useParams } from 'next/navigation' import { useCallback, useRef, useState } from 'react' import { Button } from '@/components/ui/primitives/button' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/primitives/dialog' import { createImageUploadUrls, submitFeedback } from '@/features/community/lib/feedback/actions' const MAX_IMAGES = 5 const MAX_IMAGE_SIZE = 5 * 1024 * 1024 type ImagePreview = { file: File; url: string } export function FeedbackDialog({ projectId: projectIdProp }: { projectId?: string }) { const params = useParams() const projectId = projectIdProp ?? (params?.projectId as string | undefined) const [open, setOpen] = useState(false) const [message, setMessage] = useState('') const [images, setImages] = useState([]) const [isDragging, setIsDragging] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const [error, setError] = useState(null) const [sent, setSent] = useState(false) const fileInputRef = useRef(null) const dragCounter = useRef(0) const handleOpen = () => { setOpen(true) setSent(false) setError(null) setMessage('') setImages([]) setIsDragging(false) dragCounter.current = 0 } const handleClose = () => { if (isSubmitting) return setOpen(false) images.forEach((img) => { URL.revokeObjectURL(img.url) }) } const addFiles = useCallback((files: FileList | File[]) => { const incoming = Array.from(files).filter( (f) => f.type.startsWith('image/') && f.size <= MAX_IMAGE_SIZE, ) setImages((prev) => { const remaining = MAX_IMAGES - prev.length const added = incoming.slice(0, remaining).map((file) => ({ file, url: URL.createObjectURL(file), })) return [...prev, ...added] }) }, []) const removeImage = (index: number) => { setImages((prev) => { const img = prev[index] if (img) URL.revokeObjectURL(img.url) return prev.filter((_, i) => i !== index) }) } // ── Drag handlers (on the entire dialog content) ── const onDragEnter = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() dragCounter.current++ if (e.dataTransfer.types.includes('Files')) { setIsDragging(true) } } const onDragLeave = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() dragCounter.current-- if (dragCounter.current === 0) { setIsDragging(false) } } const onDragOver = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() } const onDrop = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() dragCounter.current = 0 setIsDragging(false) if (e.dataTransfer.files.length > 0) { addFiles(e.dataTransfer.files) } } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError(null) setIsSubmitting(true) try { // Capture scene graph snapshot let sceneGraph: unknown = null try { const { nodes, rootNodeIds } = useScene.getState() sceneGraph = { nodes, rootNodeIds } } catch { // Scene store may not be available (e.g. on non-editor pages) } // Upload images directly to Supabase Storage via signed URLs let imagePaths: string[] = [] if (images.length > 0) { const urlResult = await createImageUploadUrls( images.map((img) => ({ name: img.file.name, type: img.file.type })), ) if (!urlResult.success) { setError(urlResult.error) return } // Upload each file directly to Supabase (bypasses Vercel size limit) const uploadResults = await Promise.allSettled( urlResult.uploads.map(async ({ path, signedUrl }, i) => { const file = images[i]?.file if (!file) return null const res = await fetch(signedUrl, { method: 'PUT', headers: { 'Content-Type': file.type }, body: file, }) if (!res.ok) { console.error(`Upload failed for ${file.name}: ${res.status}`) return null } return path }), ) imagePaths = uploadResults .filter( (r): r is PromiseFulfilledResult => r.status === 'fulfilled' && r.value !== null, ) .map((r) => r.value) } const result = await submitFeedback({ message, projectId, sceneGraph, imagePaths, }) if (result.success) { setSent(true) setTimeout(() => setOpen(false), 1500) } else { setError(result.error) } } finally { setIsSubmitting(false) } } return ( <> {/* Drag overlay — only visible when dragging files over the dialog */} {isDragging && (

Drop images here

)} Send Feedback We'd love to hear your thoughts {sent ? (

Thanks for your feedback!

) : (