'use client' import { useRef, useState } from 'react' import { useTranslations } from 'next-intl' import { cn } from '@/lib/utils' import { FileText, Loader2, Mail, Paperclip, Send, X } from 'lucide-react' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { useToast } from '@/components/ui/use-toast' import { submitFeedback } from '@/lib/support/submit-feedback' import { useCompanyOptional } from '@/contexts/CompanyContext' import { SUPPORT_ATTACHMENT_ACCEPT, SUPPORT_MAX_ATTACHMENTS, SUPPORT_MAX_ATTACHMENT_TOTAL_BYTES, SUPPORT_MAX_ATTACHMENT_TOTAL_MB, isSupportedAttachmentType, } from '@/lib/support/attachments' import { shrinkImageForUpload } from '@/lib/documents/shrink-image' import { isShrinkableImage } from '@/lib/documents/upload-size' interface SupportLinkProps { variant?: 'inline' | 'muted' subject?: string children?: React.ReactNode className?: string } type AttachmentError = 'unsupported' | 'too_many' | 'too_large' function totalBytes(files: File[]): number { return files.reduce((sum, file) => sum + file.size, 0) } export function SupportLink({ variant = 'inline', subject, children, className, }: SupportLinkProps) { const t = useTranslations('support_link') const [open, setOpen] = useState(false) const [message, setMessage] = useState('') const [attachments, setAttachments] = useState([]) const [isPreparing, setIsPreparing] = useState(false) const [isSending, setIsSending] = useState(false) const [sent, setSent] = useState(false) const fileInputRef = useRef(null) const attachmentGenerationRef = useRef(0) const { toast } = useToast() const companyCtx = useCompanyOptional() if (companyCtx?.isSandbox) return null function showAttachmentError(error: AttachmentError) { if (error === 'unsupported') { toast({ title: t('attach_unsupported'), variant: 'destructive' }) return } if (error === 'too_many') { toast({ title: t('attach_too_many', { count: SUPPORT_MAX_ATTACHMENTS }), variant: 'destructive', }) return } toast({ title: t('attach_too_large', { limit: SUPPORT_MAX_ATTACHMENT_TOTAL_MB }), variant: 'destructive', }) } async function addFiles(incoming: File[]) { if (!incoming.length || isPreparing || isSending) return const generation = attachmentGenerationRef.current setIsPreparing(true) try { const next = [...attachments] let firstError: AttachmentError | null = null for (const original of incoming) { if (!isSupportedAttachmentType(original.type)) { firstError ??= 'unsupported' continue } if (next.length >= SUPPORT_MAX_ATTACHMENTS) { firstError ??= 'too_many' break } const remaining = SUPPORT_MAX_ATTACHMENT_TOTAL_BYTES - totalBytes(next) const file = original.size > remaining && isShrinkableImage(original.type) ? await shrinkImageForUpload(original, remaining) : original if (file.size > remaining) { firstError ??= 'too_large' continue } next.push(file) } if (generation === attachmentGenerationRef.current) { setAttachments(next) if (firstError) showAttachmentError(firstError) } } finally { setIsPreparing(false) } } function resetAttachments() { attachmentGenerationRef.current += 1 setAttachments([]) if (fileInputRef.current) fileInputRef.current.value = '' } async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (message.trim().length < 5) return setIsSending(true) const result = await submitFeedback({ subject, message: message.trim(), files: attachments, }) setIsSending(false) if (result.ok) { setSent(true) setTimeout(() => { setOpen(false) setSent(false) setMessage('') resetAttachments() }, 2000) } else { toast({ title: t('send_failed_title'), description: result.error || t('send_failed_fallback'), variant: 'destructive', }) } } function handleOpenChange(next: boolean) { setOpen(next) if (!next) { setSent(false) setMessage('') resetAttachments() } } const trigger = variant === 'muted' ? ( ) : ( ) return ( {trigger} {t('dialog_title')} {t('dialog_description')} {sent ? (

{t('thanks')}

) : (