"use client" import { AlertCircle, CheckCircle2, Download, Loader2 } from 'lucide-react'; import * as React from 'react'; import { Button, type ButtonProps } from '../../forms/button'; import { cn } from '../../../lib'; import { useLocalStorage } from '../../../hooks'; // Token key used by the API client const TOKEN_KEY = "auth_token" export interface DownloadButtonProps extends Omit { /** * URL to download from */ url: string /** * Optional filename override. If not provided, will try to extract from Content-Disposition header */ filename?: string /** * Optional callback when download starts */ onDownloadStart?: () => void /** * Optional callback when download completes */ onDownloadComplete?: (filename: string) => void /** * Optional callback when download fails */ onDownloadError?: (error: Error) => void /** * Use fetch API for download (allows progress tracking and auth headers) * Default: true */ useFetch?: boolean /** * Show download status icons (loading, success, error) * Default: true */ showStatus?: boolean /** * Auto-reset status after success/error (in ms) * Default: 2000 */ statusResetDelay?: number /** * Method for download (GET or POST) * Default: GET */ method?: 'GET' | 'POST' /** * Optional request body for POST requests */ body?: any } type DownloadStatus = 'idle' | 'downloading' | 'success' | 'error' const DownloadButton = React.forwardRef( ( { url, filename, onDownloadStart, onDownloadComplete, onDownloadError, useFetch = true, showStatus = true, statusResetDelay = 2000, method = 'GET', body, children, disabled, className, ...props }, ref ) => { const [status, setStatus] = React.useState('idle') const resetTimeoutRef = React.useRef(undefined) // Get auth token from localStorage const [token] = useLocalStorage(TOKEN_KEY, '') // Clean up timeout on unmount React.useEffect(() => { return () => { if (resetTimeoutRef.current) { clearTimeout(resetTimeoutRef.current) } } }, []) /** * Extract filename from Content-Disposition header */ const extractFilename = (headers: Headers): string | null => { const disposition = headers.get('Content-Disposition') if (!disposition) return null // Try to match: filename*=UTF-8''example.txt or filename="example.txt" const filenameMatch = disposition.match(/filename\*?=(?:UTF-8'')?["']?([^"';]+)["']?/i) if (filenameMatch && filenameMatch[1]) { return decodeURIComponent(filenameMatch[1]) } return null } /** * Trigger browser download for blob */ const triggerDownload = (blob: Blob, downloadFilename: string) => { const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = downloadFilename document.body.appendChild(a) a.click() document.body.removeChild(a) window.URL.revokeObjectURL(url) } /** * Reset status after delay */ const scheduleStatusReset = () => { if (resetTimeoutRef.current) { clearTimeout(resetTimeoutRef.current) } resetTimeoutRef.current = setTimeout(() => { setStatus('idle') }, statusResetDelay) } /** * Download using fetch API (supports auth, progress, error handling) */ const downloadWithFetch = async () => { try { setStatus('downloading') onDownloadStart?.() const headers: HeadersInit = {} // Add authorization header if token is available if (token) { headers['Authorization'] = `Bearer ${token}` } const options: RequestInit = { method, headers, } if (method === 'POST' && body) { options.body = JSON.stringify(body) options.headers = { ...options.headers, 'Content-Type': 'application/json', } } const response = await fetch(url, options) if (!response.ok) { throw new Error(`Download failed: ${response.status} ${response.statusText}`) } const blob = await response.blob() // Determine filename const downloadFilename = filename || extractFilename(response.headers) || `download-${Date.now()}.bin` // Trigger download triggerDownload(blob, downloadFilename) setStatus('success') onDownloadComplete?.(downloadFilename) scheduleStatusReset() } catch (error) { console.error('Download error:', error) setStatus('error') onDownloadError?.(error as Error) scheduleStatusReset() } } /** * Simple download using window.open (fallback) */ const downloadWithWindowOpen = () => { try { setStatus('downloading') onDownloadStart?.() window.open(url, '_blank') // We can't really track success/failure with window.open // So just assume success after a short delay setTimeout(() => { setStatus('success') onDownloadComplete?.(filename || 'file') scheduleStatusReset() }, 500) } catch (error) { console.error('Download error:', error) setStatus('error') onDownloadError?.(error as Error) scheduleStatusReset() } } /** * Handle download click */ const handleDownload = () => { if (status === 'downloading') return if (useFetch) { downloadWithFetch() } else { downloadWithWindowOpen() } } /** * Render status icon */ const renderStatusIcon = () => { if (!showStatus) return null switch (status) { case 'downloading': return case 'success': return case 'error': return default: return } } return ( ) } ) DownloadButton.displayName = "DownloadButton" export { DownloadButton }