"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useDebouncedValue } from "@/hooks/useDebouncedValue"; import { fetchProject, patchProjectSceneData } from "@/lib/project-api"; import { isDevProjectId } from "@/lib/project-ids"; import { isImageSceneDataEmpty } from "@/lib/image-scene-data"; import { useImageEditorStore } from "@/lib/image-editor-store"; import { PROJECT_SAVE_DEBOUNCE_MS, PROJECT_SAVED_DISPLAY_MS, type ProjectSaveStatus, } from "@/lib/project-save-status"; export interface UseImageProjectPersistenceResult { projectName: string; setProjectName: (name: string) => void; saveStatus: ProjectSaveStatus; retrySave: () => void; } export function useImageProjectPersistence( projectId: string | undefined ): UseImageProjectPersistenceResult { const layers = useImageEditorStore((state) => state.layers); const canvasWidth = useImageEditorStore((state) => state.canvasWidth); const canvasHeight = useImageEditorStore((state) => state.canvasHeight); const adjustments = useImageEditorStore((state) => state.adjustments); const activeFilterPreset = useImageEditorStore( (state) => state.activeFilterPreset ); const hydrateFromSceneData = useImageEditorStore( (state) => state.hydrateFromSceneData ); const getSceneDataForSave = useImageEditorStore( (state) => state.getSceneDataForSave ); const [projectName, setProjectName] = useState("Untitled image"); const [saveStatus, setSaveStatus] = useState("idle"); const skipSaveRef = useRef(true); const lastSavedPayloadRef = useRef(null); const savedTimerRef = useRef | null>(null); const persistPayload = useMemo( () => JSON.stringify(getSceneDataForSave()), [layers, canvasWidth, canvasHeight, adjustments, activeFilterPreset] ); const debouncedPayload = useDebouncedValue( persistPayload, PROJECT_SAVE_DEBOUNCE_MS ); const clearSavedTimer = useCallback(() => { if (savedTimerRef.current) { clearTimeout(savedTimerRef.current); savedTimerRef.current = null; } }, []); const performSave = useCallback( async (payloadJson: string) => { if (!projectId || isDevProjectId(projectId)) return; setSaveStatus("saving"); try { const scene_data = JSON.parse(payloadJson) as Record; await patchProjectSceneData(projectId, scene_data); lastSavedPayloadRef.current = payloadJson; setSaveStatus("saved"); clearSavedTimer(); savedTimerRef.current = setTimeout(() => { setSaveStatus((current) => (current === "saved" ? "idle" : current)); }, PROJECT_SAVED_DISPLAY_MS); } catch { setSaveStatus("error"); } }, [projectId, clearSavedTimer] ); const retrySave = useCallback(() => { void performSave(persistPayload); }, [performSave, persistPayload]); useEffect(() => { if (!projectId || isDevProjectId(projectId)) return; let cancelled = false; skipSaveRef.current = true; void (async () => { try { const { project } = await fetchProject(projectId); if (cancelled) return; setProjectName(project.name); if (!isImageSceneDataEmpty(project.scene_data)) { hydrateFromSceneData(project.scene_data); } lastSavedPayloadRef.current = JSON.stringify( useImageEditorStore.getState().getSceneDataForSave() ); } catch { if (!cancelled) { setSaveStatus("error"); } } finally { if (!cancelled) { skipSaveRef.current = false; } } })(); return () => { cancelled = true; }; }, [projectId, hydrateFromSceneData]); useEffect(() => { if (!projectId || isDevProjectId(projectId) || skipSaveRef.current) return; if (persistPayload === lastSavedPayloadRef.current) return; setSaveStatus("pending"); }, [projectId, persistPayload]); useEffect(() => { if (!projectId || isDevProjectId(projectId) || skipSaveRef.current) return; if (debouncedPayload === lastSavedPayloadRef.current) return; void performSave(debouncedPayload); }, [projectId, debouncedPayload, performSave]); useEffect(() => clearSavedTimer, [clearSavedTimer]); return { projectName, setProjectName, saveStatus, retrySave }; }