import { useState, useRef, useCallback, useEffect } from 'react'; import { createLogger } from '@automaker/utils/logger'; import { ImageIcon, Upload, Trash2 } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; const logger = createLogger('BoardBackgroundModal'); import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from '@/components/ui/sheet'; import { Button } from '@/components/ui/button'; import { Slider } from '@/components/ui/slider'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { cn } from '@/lib/utils'; import { useAppStore, defaultBackgroundSettings } from '@/store/app-store'; import { getHttpApiClient } from '@/lib/http-api-client'; import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; import { useBoardBackgroundSettings } from '@/hooks/use-board-background-settings'; import { toast } from 'sonner'; import { fileToBase64, validateImageFile, ACCEPTED_IMAGE_TYPES, DEFAULT_MAX_FILE_SIZE, } from '@/lib/image-utils'; interface BoardBackgroundModalProps { open: boolean; onOpenChange: (open: boolean) => void; } export function BoardBackgroundModal({ open, onOpenChange }: BoardBackgroundModalProps) { const { currentProject, boardBackgroundByProject } = useAppStore(); const { setBoardBackground, setCardOpacity, setColumnOpacity, setColumnBorderEnabled, setCardGlassmorphism, setCardBorderEnabled, setCardBorderOpacity, setHideScrollbar, clearBoardBackground, persistSettings, getCurrentSettings, } = useBoardBackgroundSettings(); const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const fileInputRef = useRef(null); const [previewImage, setPreviewImage] = useState(null); // Get current background settings (live from store) const backgroundSettings = (currentProject && boardBackgroundByProject[currentProject.path]) || defaultBackgroundSettings; // Local state for sliders during dragging (avoids store updates during drag) const [localCardOpacity, setLocalCardOpacity] = useState(backgroundSettings.cardOpacity); const [localColumnOpacity, setLocalColumnOpacity] = useState(backgroundSettings.columnOpacity); const [localCardBorderOpacity, setLocalCardBorderOpacity] = useState( backgroundSettings.cardBorderOpacity ); const [isDragging, setIsDragging] = useState(false); // Sync local state with store when not dragging (e.g., on modal open or external changes) useEffect(() => { if (!isDragging) { setLocalCardOpacity(backgroundSettings.cardOpacity); setLocalColumnOpacity(backgroundSettings.columnOpacity); setLocalCardBorderOpacity(backgroundSettings.cardBorderOpacity); } }, [ isDragging, backgroundSettings.cardOpacity, backgroundSettings.columnOpacity, backgroundSettings.cardBorderOpacity, ]); const columnBorderEnabled = backgroundSettings.columnBorderEnabled; const cardGlassmorphism = backgroundSettings.cardGlassmorphism; const cardBorderEnabled = backgroundSettings.cardBorderEnabled; const hideScrollbar = backgroundSettings.hideScrollbar; const imageVersion = backgroundSettings.imageVersion; // Update preview image when background settings change useEffect(() => { if (currentProject && backgroundSettings.imagePath) { // Add cache-busting query parameter to force browser to reload image const cacheBuster = imageVersion ?? Date.now().toString(); const imagePath = getAuthenticatedImageUrl( backgroundSettings.imagePath, currentProject.path, cacheBuster ); setPreviewImage(imagePath); } else { setPreviewImage(null); } }, [currentProject, backgroundSettings.imagePath, imageVersion]); const processFile = useCallback( async (file: File) => { if (!currentProject) { toast.error('No project selected'); return; } // Validate file const validation = validateImageFile(file, DEFAULT_MAX_FILE_SIZE); if (!validation.isValid) { toast.error(validation.error); return; } setIsProcessing(true); try { const base64 = await fileToBase64(file); // Set preview immediately setPreviewImage(base64); // Save to server const httpClient = getHttpApiClient(); const result = await httpClient.saveBoardBackground( base64, file.name, file.type, currentProject.path ); if (result.success && result.path) { // Update store and persist to server await setBoardBackground(currentProject.path, result.path); toast.success('Background image saved'); } else { toast.error(result.error || 'Failed to save background image'); setPreviewImage(null); } } catch (error) { logger.error('Failed to process image:', error); toast.error('Failed to process image'); setPreviewImage(null); } finally { setIsProcessing(false); } }, [currentProject, setBoardBackground] ); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); const files = e.dataTransfer.files; if (files.length > 0) { processFile(files[0]); } }, [processFile] ); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); }, []); const handleFileSelect = useCallback( (e: React.ChangeEvent) => { const files = e.target.files; if (files && files.length > 0) { processFile(files[0]); } // Reset the input so the same file can be selected again if (fileInputRef.current) { fileInputRef.current.value = ''; } }, [processFile] ); const handleBrowseClick = useCallback(() => { if (fileInputRef.current) { fileInputRef.current.click(); } }, []); const handleClear = useCallback(async () => { if (!currentProject) return; try { setIsProcessing(true); const httpClient = getHttpApiClient(); const result = await httpClient.deleteBoardBackground(currentProject.path); if (result.success) { await clearBoardBackground(currentProject.path); setPreviewImage(null); toast.success('Background image cleared'); } else { toast.error(result.error || 'Failed to clear background image'); } } catch (error) { logger.error('Failed to clear background:', error); toast.error('Failed to clear background'); } finally { setIsProcessing(false); } }, [currentProject, clearBoardBackground]); // Live update local state during drag (modal-only, no store update) const handleCardOpacityChange = useCallback((value: number[]) => { setIsDragging(true); setLocalCardOpacity(value[0]); }, []); // Update store and persist when slider is released const handleCardOpacityCommit = useCallback( (value: number[]) => { if (!currentProject) return; setIsDragging(false); setCardOpacity(currentProject.path, value[0]); const current = getCurrentSettings(currentProject.path); persistSettings(currentProject.path, { ...current, cardOpacity: value[0] }); }, [currentProject, setCardOpacity, getCurrentSettings, persistSettings] ); // Live update local state during drag (modal-only, no store update) const handleColumnOpacityChange = useCallback((value: number[]) => { setIsDragging(true); setLocalColumnOpacity(value[0]); }, []); // Update store and persist when slider is released const handleColumnOpacityCommit = useCallback( (value: number[]) => { if (!currentProject) return; setIsDragging(false); setColumnOpacity(currentProject.path, value[0]); const current = getCurrentSettings(currentProject.path); persistSettings(currentProject.path, { ...current, columnOpacity: value[0] }); }, [currentProject, setColumnOpacity, getCurrentSettings, persistSettings] ); const handleColumnBorderToggle = useCallback( async (checked: boolean) => { if (!currentProject) return; await setColumnBorderEnabled(currentProject.path, checked); }, [currentProject, setColumnBorderEnabled] ); const handleCardGlassmorphismToggle = useCallback( async (checked: boolean) => { if (!currentProject) return; await setCardGlassmorphism(currentProject.path, checked); }, [currentProject, setCardGlassmorphism] ); const handleCardBorderToggle = useCallback( async (checked: boolean) => { if (!currentProject) return; await setCardBorderEnabled(currentProject.path, checked); }, [currentProject, setCardBorderEnabled] ); // Live update local state during drag (modal-only, no store update) const handleCardBorderOpacityChange = useCallback((value: number[]) => { setIsDragging(true); setLocalCardBorderOpacity(value[0]); }, []); // Update store and persist when slider is released const handleCardBorderOpacityCommit = useCallback( (value: number[]) => { if (!currentProject) return; setIsDragging(false); setCardBorderOpacity(currentProject.path, value[0]); const current = getCurrentSettings(currentProject.path); persistSettings(currentProject.path, { ...current, cardBorderOpacity: value[0] }); }, [currentProject, setCardBorderOpacity, getCurrentSettings, persistSettings] ); const handleHideScrollbarToggle = useCallback( async (checked: boolean) => { if (!currentProject) return; await setHideScrollbar(currentProject.path, checked); }, [currentProject, setHideScrollbar] ); if (!currentProject) { return null; } return ( Board Background Settings Set a custom background image for your kanban board and adjust card/column opacity
{/* Image Upload Section */}
{/* Hidden file input */} {/* Drop zone */}
{previewImage ? (
Background preview {isProcessing && (
)}
) : (
{isProcessing ? ( ) : ( )}

{isDragOver && !isProcessing ? 'Drop image here' : 'Click to upload or drag and drop'}

JPG, PNG, GIF, or WebP (max {Math.round(DEFAULT_MAX_FILE_SIZE / (1024 * 1024))} MB)

)}
{/* Opacity Controls */}
{localCardOpacity}%
{localColumnOpacity}%
{/* Column Border Toggle */}
{/* Card Glassmorphism Toggle */}
{/* Card Border Toggle */}
{/* Card Border Opacity - only show when border is enabled */} {cardBorderEnabled && (
{localCardBorderOpacity}%
)} {/* Hide Scrollbar Toggle */}
); }