import { useState, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { ticketsApi } from '../api/tickets' import { infoApi } from '../api/info' import { logger } from '../utils/logger' import { checkRateLimit, getRateLimitResetTime, RATE_LIMIT_KEYS } from '../utils/rateLimit' import type { TicketDetail, TicketMessage } from '../types' const log = logger.createLogger('Support') const PlusIcon = () => ( ) const SendIcon = () => ( ) const ImageIcon = () => ( ) const CloseIcon = () => ( ) // Media attachment state interface MediaAttachment { file: File preview: string uploading: boolean fileId?: string error?: string } // Message media display component function MessageMedia({ message, t }: { message: TicketMessage; t: (key: string) => string }) { const [imageLoaded, setImageLoaded] = useState(false) const [imageError, setImageError] = useState(false) const [showFullImage, setShowFullImage] = useState(false) if (!message.has_media || !message.media_file_id) { return null } const mediaUrl = ticketsApi.getMediaUrl(message.media_file_id) if (message.media_type === 'photo') { return ( <>
{!imageLoaded && !imageError && (
)} {imageError ? (
{t('support.imageLoadFailed')}
) : ( {message.media_caption setImageLoaded(true)} onError={() => setImageError(true)} onClick={() => setShowFullImage(true)} /> )} {message.media_caption && (

{message.media_caption}

)}
{/* Full image modal */} {showFullImage && (
setShowFullImage(false)} > {message.media_caption
)} ) } // For documents/videos - show download link return (
{message.media_caption || `Download ${message.media_type}`}
) } export default function Support() { log.debug('Component loaded') const { t } = useTranslation() const queryClient = useQueryClient() const [selectedTicket, setSelectedTicket] = useState(null) const [showCreateForm, setShowCreateForm] = useState(false) const [newTitle, setNewTitle] = useState('') const [newMessage, setNewMessage] = useState('') const [replyMessage, setReplyMessage] = useState('') const [rateLimitError, setRateLimitError] = useState(null) // Media attachment states const [createAttachment, setCreateAttachment] = useState(null) const [replyAttachment, setReplyAttachment] = useState(null) const createFileInputRef = useRef(null) const replyFileInputRef = useRef(null) // Get support configuration const { data: supportConfig, isLoading: configLoading } = useQuery({ queryKey: ['support-config'], queryFn: infoApi.getSupportConfig, }) const { data: tickets, isLoading } = useQuery({ queryKey: ['tickets'], queryFn: () => ticketsApi.getTickets({ per_page: 20 }), enabled: supportConfig?.tickets_enabled === true, }) const { data: ticketDetail, isLoading: detailLoading } = useQuery({ queryKey: ['ticket', selectedTicket?.id], queryFn: () => ticketsApi.getTicket(selectedTicket!.id), enabled: !!selectedTicket, }) // Handle file selection const handleFileSelect = async ( file: File, setAttachment: (a: MediaAttachment | null) => void ) => { // Validate file type const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'] if (!allowedTypes.includes(file.type)) { setAttachment({ file, preview: '', uploading: false, error: t('support.invalidFileType') || 'Invalid file type. Use JPEG, PNG, GIF, or WebP.', }) return } // Validate file size (10MB) if (file.size > 10 * 1024 * 1024) { setAttachment({ file, preview: '', uploading: false, error: t('support.fileTooLarge') || 'File is too large. Maximum size is 10MB.', }) return } // Create preview const preview = URL.createObjectURL(file) setAttachment({ file, preview, uploading: true }) try { const result = await ticketsApi.uploadMedia(file, 'photo') setAttachment({ file, preview, uploading: false, fileId: result.file_id, }) } catch (error) { setAttachment({ file, preview, uploading: false, error: t('support.uploadFailed') || 'Failed to upload image', }) } } const createMutation = useMutation({ mutationFn: async () => { const media = createAttachment?.fileId ? { media_type: 'photo', media_file_id: createAttachment.fileId, } : undefined return ticketsApi.createTicket(newTitle, newMessage, media) }, onSuccess: (ticket) => { queryClient.invalidateQueries({ queryKey: ['tickets'] }) setShowCreateForm(false) setNewTitle('') setNewMessage('') setCreateAttachment(null) setSelectedTicket(ticket) }, }) const replyMutation = useMutation({ mutationFn: async () => { const media = replyAttachment?.fileId ? { media_type: 'photo', media_file_id: replyAttachment.fileId, } : undefined return ticketsApi.addMessage(selectedTicket!.id, replyMessage, media) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ticket', selectedTicket?.id] }) setReplyMessage('') setReplyAttachment(null) }, }) const getStatusBadge = (status: string) => { switch (status) { case 'open': return 'badge-info' case 'answered': return 'badge-success' case 'pending': return 'badge-warning' case 'closed': return 'badge-neutral' default: return 'badge-neutral' } } const getStatusLabel = (status: string) => { return t(`support.status.${status}`) || status } // Show loading while checking configuration if (configLoading) { return (
) } // If tickets are disabled, show redirect message if (supportConfig && !supportConfig.tickets_enabled) { log.debug('Tickets disabled, config:', supportConfig) const getSupportMessage = () => { log.debug('Getting support message for type:', supportConfig.support_type) if (supportConfig.support_type === 'profile') { const supportUsername = supportConfig.support_username || '@support' log.debug('Opening profile:', supportUsername) return { title: t('support.ticketsDisabled'), message: t('support.contactSupport', { username: supportUsername }), buttonText: t('support.contactUs'), buttonAction: () => { log.debug('Button clicked, opening:', supportUsername) const webApp = window.Telegram?.WebApp // Extract username without @ const username = supportUsername.startsWith('@') ? supportUsername.slice(1) : supportUsername const webUrl = `https://t.me/${username}` log.debug('Web URL:', webUrl, 'WebApp methods:', { openTelegramLink: !!webApp?.openTelegramLink, openLink: !!webApp?.openLink, }) // Try openTelegramLink first (for tg:// links) if (webApp?.openTelegramLink) { log.debug('Using openTelegramLink with web URL') try { webApp.openTelegramLink(webUrl) return } catch (e) { log.error('openTelegramLink failed:', e) } } // Fallback to openLink if (webApp?.openLink) { log.debug('Using openLink') try { webApp.openLink(webUrl, { try_browser: true }) return } catch (e) { log.error('openLink failed:', e) } } // Last resort - window.open log.debug('Using window.open') window.open(webUrl, '_blank') }, } } if (supportConfig.support_type === 'url' && supportConfig.support_url) { return { title: t('support.ticketsDisabled'), message: t('support.useExternalLink'), buttonText: t('support.openSupport'), buttonAction: () => { const webApp = window.Telegram?.WebApp if (webApp?.openLink) { webApp.openLink(supportConfig.support_url!, { try_browser: true }) } else { window.open(supportConfig.support_url!, '_blank') } }, } } // Fallback: contact support (should not normally happen if config is correct) const supportUsername = supportConfig.support_username || '@support' log.debug('Fallback: Opening profile:', supportUsername) return { title: t('support.ticketsDisabled'), message: t('support.contactSupport', { username: supportUsername }), buttonText: t('support.contactUs'), buttonAction: () => { log.debug('Fallback button clicked, opening:', supportUsername) const webApp = window.Telegram?.WebApp // Extract username without @ const username = supportUsername.startsWith('@') ? supportUsername.slice(1) : supportUsername const webUrl = `https://t.me/${username}` log.debug('Fallback opening URL:', webUrl) if (webApp?.openTelegramLink) { log.debug('Fallback using openTelegramLink') webApp.openTelegramLink(webUrl) } else if (webApp?.openLink) { log.debug('Fallback using openLink') webApp.openLink(webUrl, { try_browser: true }) } else { log.debug('Fallback using window.open') window.open(webUrl, '_blank') } }, } } const supportMessage = getSupportMessage() return (

{supportMessage.title}

{supportMessage.message}

) } // Attachment preview component const AttachmentPreview = ({ attachment, onRemove, }: { attachment: MediaAttachment onRemove: () => void }) => (
{attachment.preview && ( Attachment preview )} {attachment.uploading && (
)} {attachment.error && (
{attachment.error}
)}
) return (

{t('support.title')}

{/* Tickets List */}

{t('support.yourTickets')}

{isLoading ? (
) : tickets?.items && tickets.items.length > 0 ? (
{tickets.items.map((ticket) => ( ))}
) : (
{t('support.noTickets')}
)}
{/* Ticket Detail / Create Form */}
{showCreateForm ? (

{t('support.createTicket')}

{ e.preventDefault() setRateLimitError(null) // Rate limit: max 3 tickets per 60 seconds if (!checkRateLimit(RATE_LIMIT_KEYS.TICKET_CREATE, 3, 60000)) { const resetTime = getRateLimitResetTime(RATE_LIMIT_KEYS.TICKET_CREATE) setRateLimitError(t('support.tooManyRequests', { seconds: resetTime }) || `Слишком много запросов. Подождите ${resetTime} сек.`) return } createMutation.mutate() }} className="space-y-4" >
setNewTitle(e.target.value)} required minLength={3} maxLength={255} />