app/lib/autoSaveService.ts (106 lines of code) (raw):

// Auto-save utilities for localStorage and configuration export class AutoSaveService { private static saveQueue = new Map<string, NodeJS.Timeout>(); private static readonly DEFAULT_DELAY = 300; // Much faster - 300ms for localStorage /** * Auto-save to localStorage with debouncing */ static saveToLocalStorage( key: string, value: any, delay = this.DEFAULT_DELAY ) { // Clear existing timeout for this key const existingTimeout = this.saveQueue.get(key); if (existingTimeout) { clearTimeout(existingTimeout); } // Set new timeout const timeout = setTimeout(() => { try { const serializedValue = typeof value === "string" ? value : JSON.stringify(value); localStorage.setItem(key, serializedValue); console.log(`Auto-saved to localStorage: ${key}`); this.saveQueue.delete(key); } catch (error) { console.error(`Failed to auto-save to localStorage (${key}):`, error); } }, delay); this.saveQueue.set(key, timeout); } /** * Show a very subtle save indicator */ static showSaveIndicator( message: string = "Saving...", type: "saving" | "saved" | "error" = "saving" ) { // Only show error messages, skip success indicators for subtlety if (type !== "error") { return; } // Remove existing indicator const existing = document.querySelector("[data-autosave-indicator]"); if (existing) { existing.remove(); } const indicator = document.createElement("div"); indicator.setAttribute("data-autosave-indicator", "true"); // Much more subtle styling indicator.className = `fixed bottom-4 right-4 bg-red-500 text-white px-3 py-1.5 rounded text-xs shadow-lg z-50 transition-all duration-200`; indicator.innerHTML = ` <div class="flex items-center gap-1"> <div class="w-1 h-1 bg-white rounded-full"></div> <span>${message}</span> </div> `; document.body.appendChild(indicator); // Auto-remove quickly setTimeout(() => { if (indicator.parentNode) { indicator.style.opacity = "0"; indicator.style.transform = "translateY(10px)"; setTimeout(() => { if (indicator.parentNode) { indicator.remove(); } }, 200); } }, 3000); } /** * Clear all pending saves (useful for cleanup) */ static clearAllPending() { this.saveQueue.forEach((timeout) => clearTimeout(timeout)); this.saveQueue.clear(); } /** * Force save all pending items immediately */ static async forceSaveAll() { const promises: Promise<void>[] = []; this.saveQueue.forEach((timeout, key) => { clearTimeout(timeout); // Force immediate save by calling the timeout function promises.push(Promise.resolve()); }); this.saveQueue.clear(); await Promise.all(promises); } } // Storage keys used throughout the application export const STORAGE_KEYS = { dockerConfig: "hugex_docker_config", localSecrets: "hugex_local_secrets", templateText: "hugex_template_text", selectedRepo: "hugex_selected_repo", selectedBranch: "hugex_selected_branch", recentRepos: "hugex_recent_repositories", recentBranches: "hugex_recent_branches", welcomeSeen: "hugex_welcome_seen", } as const; // Helper function to safely parse JSON from localStorage export function getFromLocalStorage<T>(key: string, defaultValue: T): T { try { if (typeof window === "undefined") return defaultValue; const item = localStorage.getItem(key); if (item === null) return defaultValue; return JSON.parse(item); } catch { return defaultValue; } } // Helper function to safely set JSON to localStorage with auto-save export function setToLocalStorage(key: string, value: any, autoSave = true) { try { if (typeof window === "undefined") return; if (autoSave) { AutoSaveService.saveToLocalStorage(key, value); } else { const serializedValue = typeof value === "string" ? value : JSON.stringify(value); localStorage.setItem(key, serializedValue); } } catch (error) { console.error(`Failed to save to localStorage (${key}):`, error); } }