import { useStore } from '@nanostores/react'; import { memo, useCallback, useEffect, useState, useRef } from 'react'; import { toast } from 'react-toastify'; import * as Tooltip from '@radix-ui/react-tooltip'; import { workbenchStore } from '~/lib/stores/workbench'; import { classNames } from '~/utils/classNames'; interface SaveAllButtonProps { className?: string; variant?: 'icon' | 'button'; showCount?: boolean; autoSave?: boolean; autoSaveInterval?: number; } export const SaveAllButton = memo( ({ className = '', variant = 'icon', showCount = true, autoSave = false, autoSaveInterval = 30000, }: SaveAllButtonProps) => { const unsavedFiles = useStore(workbenchStore.unsavedFiles); const [isSaving, setIsSaving] = useState(false); const [lastSaved, setLastSaved] = useState(null); const [timeUntilAutoSave, setTimeUntilAutoSave] = useState(null); const autoSaveTimerRef = useRef(null); const countdownTimerRef = useRef(null); const unsavedCount = unsavedFiles.size; const hasUnsavedFiles = unsavedCount > 0; // Log unsaved files state changes useEffect(() => { console.log('[SaveAllButton] Unsaved files changed:', { count: unsavedCount, files: Array.from(unsavedFiles), hasUnsavedFiles, }); }, [unsavedFiles, unsavedCount, hasUnsavedFiles]); // Auto-save logic useEffect(() => { if (!autoSave || !hasUnsavedFiles) { if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current); autoSaveTimerRef.current = null; } if (countdownTimerRef.current) { clearInterval(countdownTimerRef.current); countdownTimerRef.current = null; } setTimeUntilAutoSave(null); return; } // Set up auto-save timer console.log('[SaveAllButton] Setting up auto-save timer for', autoSaveInterval, 'ms'); autoSaveTimerRef.current = setTimeout(async () => { if (hasUnsavedFiles && !isSaving) { console.log('[SaveAllButton] Auto-save triggered'); await handleSaveAll(true); } }, autoSaveInterval); // Set up countdown timer const startTime = Date.now(); setTimeUntilAutoSave(Math.ceil(autoSaveInterval / 1000)); countdownTimerRef.current = setInterval(() => { const elapsed = Date.now() - startTime; const remaining = Math.max(0, autoSaveInterval - elapsed); setTimeUntilAutoSave(Math.ceil(remaining / 1000)); if (remaining <= 0 && countdownTimerRef.current) { clearInterval(countdownTimerRef.current); countdownTimerRef.current = null; } }, 1000); }, [autoSave, hasUnsavedFiles, autoSaveInterval, isSaving]); // Cleanup effect useEffect(() => { return () => { if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current); } if (countdownTimerRef.current) { clearInterval(countdownTimerRef.current); } }; }, []); const handleSaveAll = useCallback( async (isAutoSave = false) => { if (!hasUnsavedFiles || isSaving) { console.log('[SaveAllButton] Skipping save:', { hasUnsavedFiles, isSaving }); return; } console.log('[SaveAllButton] Starting save:', { unsavedCount, isAutoSave, files: Array.from(unsavedFiles), }); setIsSaving(true); const startTime = performance.now(); const savedFiles: string[] = []; const failedFiles: string[] = []; try { // Save each file individually with detailed logging for (const filePath of unsavedFiles) { try { console.log(`[SaveAllButton] Saving file: ${filePath}`); await workbenchStore.saveFile(filePath); savedFiles.push(filePath); console.log(`[SaveAllButton] Successfully saved: ${filePath}`); } catch (error) { console.error(`[SaveAllButton] Failed to save ${filePath}:`, error); failedFiles.push(filePath); } } const endTime = performance.now(); const duration = Math.round(endTime - startTime); setLastSaved(new Date()); // Check final state const remainingUnsaved = workbenchStore.unsavedFiles.get(); console.log('[SaveAllButton] Save complete:', { savedCount: savedFiles.length, failedCount: failedFiles.length, remainingUnsaved: Array.from(remainingUnsaved), duration, }); // Show appropriate feedback if (failedFiles.length === 0) { const message = isAutoSave ? `Auto-saved ${savedFiles.length} file${savedFiles.length > 1 ? 's' : ''}` : `Saved ${savedFiles.length} file${savedFiles.length > 1 ? 's' : ''}`; toast.success(message, { position: 'bottom-right', autoClose: 2000, }); } else { toast.warning(`Saved ${savedFiles.length} files, ${failedFiles.length} failed`, { position: 'bottom-right', autoClose: 3000, }); } } catch (error) { console.error('[SaveAllButton] Critical error during save:', error); toast.error('Failed to save files', { position: 'bottom-right', autoClose: 3000, }); } finally { setIsSaving(false); } }, [hasUnsavedFiles, isSaving, unsavedCount, unsavedFiles], ); // Keyboard shortcut useEffect(() => { const handleKeyPress = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 's') { e.preventDefault(); handleSaveAll(false); } }; window.addEventListener('keydown', handleKeyPress); return () => window.removeEventListener('keydown', handleKeyPress); }, [handleSaveAll]); const formatLastSaved = () => { if (!lastSaved) { return null; } const now = new Date(); const diff = Math.floor((now.getTime() - lastSaved.getTime()) / 1000); if (diff < 60) { return `${diff}s ago`; } if (diff < 3600) { return `${Math.floor(diff / 60)}m ago`; } return `${Math.floor(diff / 3600)}h ago`; }; // Icon-only variant for header if (variant === 'icon') { return (
{hasUnsavedFiles ? `${unsavedCount} unsaved file${unsavedCount > 1 ? 's' : ''}` : 'All files saved'}
{lastSaved &&
Last saved: {formatLastSaved()}
} {autoSave && hasUnsavedFiles && timeUntilAutoSave && (
Auto-save in: {timeUntilAutoSave}s
)}
Ctrl+Shift+S to save all
); } // Button variant return (
Ctrl+Shift+S to save all
); }, ); SaveAllButton.displayName = 'SaveAllButton';