import { useState, useEffect, useRef } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { X, Save, Loader2, Upload, Trash2 } from 'lucide-react'; import { api } from '../api/client'; import type { ExternalLink, ExternalLinkCreate, ExternalLinkUpdate } from '../api/client'; import { Button } from './Button'; import { IconPicker, getIconByName } from './IconPicker'; import { useTheme } from '../contexts/ThemeContext'; interface AddExternalLinkModalProps { link?: ExternalLink | null; onClose: () => void; } export function AddExternalLinkModal({ link, onClose }: AddExternalLinkModalProps) { const queryClient = useQueryClient(); const { mode } = useTheme(); const isEditing = !!link; const fileInputRef = useRef(null); const [name, setName] = useState(link?.name || ''); const [url, setUrl] = useState(link?.url || ''); const [icon, setIcon] = useState(link?.icon || 'link'); const [useCustomIcon, setUseCustomIcon] = useState(!!link?.custom_icon); const [customIconPreview, setCustomIconPreview] = useState( link?.custom_icon ? api.getExternalLinkIconUrl(link.id) : null ); const [pendingIconFile, setPendingIconFile] = useState(null); const [error, setError] = useState(null); // Close on Escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); // Create mutation const createMutation = useMutation({ mutationFn: async (data: ExternalLinkCreate) => { const created = await api.createExternalLink(data); // If there's a pending icon file, upload it if (pendingIconFile) { return await api.uploadExternalLinkIcon(created.id, pendingIconFile); } return created; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['external-links'] }); onClose(); }, onError: (err: Error) => { setError(err.message); }, }); // Update mutation const updateMutation = useMutation({ mutationFn: async (data: ExternalLinkUpdate) => { let updated = await api.updateExternalLink(link!.id, data); // Handle icon changes if (pendingIconFile) { // Upload new icon updated = await api.uploadExternalLinkIcon(link!.id, pendingIconFile); } else if (!useCustomIcon && link?.custom_icon) { // Remove custom icon if switching to preset updated = await api.deleteExternalLinkIcon(link!.id); } return updated; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['external-links'] }); onClose(); }, onError: (err: Error) => { setError(err.message); }, }); const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { // Validate file type const validTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/svg+xml', 'image/webp', 'image/x-icon']; if (!validTypes.includes(file.type)) { setError('Please select a valid image file (PNG, JPG, GIF, SVG, WebP, or ICO)'); return; } // Validate file size (max 1MB) if (file.size > 1024 * 1024) { setError('Image file must be less than 1MB'); return; } setPendingIconFile(file); setUseCustomIcon(true); // Create preview const reader = new FileReader(); reader.onload = (e) => { setCustomIconPreview(e.target?.result as string); }; reader.readAsDataURL(file); } }; const handleRemoveCustomIcon = () => { setPendingIconFile(null); setCustomIconPreview(null); setUseCustomIcon(false); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setError(null); if (!name.trim()) { setError('Name is required'); return; } if (!url.trim()) { setError('URL is required'); return; } // Validate URL if (!url.startsWith('http://') && !url.startsWith('https://')) { setError('URL must start with http:// or https://'); return; } const data = { name: name.trim(), url: url.trim(), icon: useCustomIcon ? icon : icon, // Keep preset icon as fallback }; if (isEditing) { updateMutation.mutate(data); } else { createMutation.mutate(data); } }; const isPending = createMutation.isPending || updateMutation.isPending; const PresetIcon = getIconByName(icon); return (
e.stopPropagation()} > {/* Header */}
{useCustomIcon && customIconPreview ? ( ) : ( )}

{isEditing ? 'Edit Link' : 'Add External Link'}

{/* Form */}
{error && (
{error}
)} {/* Name */}
setName(e.target.value)} placeholder="My Link" maxLength={50} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" />
{/* URL */}
setUrl(e.target.value)} placeholder="https://example.com" className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" />
{/* Icon Section */}
{/* Custom Icon Upload */}
Custom Icon {useCustomIcon && customIconPreview ? (
Custom icon
) : ( )}

PNG, JPG, GIF, SVG, WebP, or ICO. Max 1MB.

{/* Preset Icon Picker */} {!useCustomIcon && (
Or choose a preset icon
)}
{/* Actions */}
); }