"use client"; import { useEffect, useEffectEvent, useRef, useState } from "react"; import { toast } from "sonner"; import { saveWithConflictRetry } from "@/lib/editor/autosave"; import { parseLocalDraft as parseStoredDraft, serializeLocalDraft } from "@/lib/editor/local-draft"; export type AutosaveState = "saved" | "unsaved" | "saving" | "incomplete" | "error"; interface SaveResult { status: string; message?: string; version?: number; currentVersion?: number; } export function useAutosave({ value, valid, initialVersion, persist, delay = 900, storageKey, localDraft, parseLocalDraft, restoreLocalDraft, }: { value: Value; valid: boolean; initialVersion: number; persist: (value: Value, expectedVersion: number) => Promise; delay?: number; storageKey?: string; localDraft?: Draft; parseLocalDraft?: (value: unknown) => Draft | null; restoreLocalDraft?: (draft: Draft) => void; }) { const fingerprint = JSON.stringify(value); const draftFingerprint = JSON.stringify(localDraft ?? value); const [savedFingerprint, setSavedFingerprint] = useState(fingerprint); const [savedDraftFingerprint, setSavedDraftFingerprint] = useState(draftFingerprint); const [errorFingerprint, setErrorFingerprint] = useState(null); const [saving, setSaving] = useState(false); const [version, setVersion] = useState(initialVersion); const [hydrated, setHydrated] = useState(!storageKey); const [manualRequest, setManualRequest] = useState(0); const handledManualRequest = useRef(0); const storageWarningShown = useRef(false); const restore = useEffectEvent((serialized: string) => { if (!parseLocalDraft || !restoreLocalDraft) return false; const stored = parseStoredDraft(serialized, parseLocalDraft); if (!stored) return false; restoreLocalDraft(stored.draft); return true; }); useEffect(() => { if (!storageKey) return; const timer = window.setTimeout(() => { let restored = false; try { const serialized = window.localStorage.getItem(storageKey); if (serialized) restored = restore(serialized); if (serialized && !restored) window.localStorage.removeItem(storageKey); } catch { if (!storageWarningShown.current) { storageWarningShown.current = true; toast.error("ไม่สามารถเปิด Local draft ได้"); } } setHydrated(true); if (restored) toast.info("กู้คืน Local draft แล้ว ระบบจะบันทึกอัตโนมัติเมื่อข้อมูลครบ"); }, 0); return () => window.clearTimeout(timer); }, [storageKey]); useEffect(() => { if (!hydrated || !storageKey || localDraft === undefined) return; try { if (draftFingerprint === savedDraftFingerprint) { window.localStorage.removeItem(storageKey); return; } window.localStorage.setItem(storageKey, serializeLocalDraft(localDraft, version)); } catch { if (!storageWarningShown.current) { storageWarningShown.current = true; toast.error("บันทึก Local draft ไม่สำเร็จ กรุณาตรวจสอบพื้นที่ของเบราว์เซอร์"); } } }, [draftFingerprint, hydrated, localDraft, savedDraftFingerprint, storageKey, version]); const save = useEffectEvent(async (snapshot: Value, snapshotFingerprint: string, snapshotDraftFingerprint: string) => { setSaving(true); setErrorFingerprint(null); try { const attempt = await saveWithConflictRetry(persist, snapshot, version); const result = attempt.response; if (attempt.retried) setVersion(attempt.version); if (result.status === "error" || result.status === "conflict") { if (result.currentVersion) setVersion(result.currentVersion); setErrorFingerprint(snapshotFingerprint); toast.error(result.message ?? "บันทึกอัตโนมัติไม่สำเร็จ"); return; } if (result.version) setVersion(result.version); setSavedFingerprint(snapshotFingerprint); setSavedDraftFingerprint(snapshotDraftFingerprint); } catch (cause) { setErrorFingerprint(snapshotFingerprint); toast.error(cause instanceof Error ? cause.message : "บันทึกอัตโนมัติไม่สำเร็จ"); } finally { setSaving(false); } }); useEffect(() => { if (!hydrated || !valid || saving) return; const manual = manualRequest !== handledManualRequest.current; if (!manual && (fingerprint === savedFingerprint || fingerprint === errorFingerprint)) return; if (manual) handledManualRequest.current = manualRequest; const timer = window.setTimeout(() => { void save(JSON.parse(fingerprint) as Value, fingerprint, draftFingerprint); }, manual ? 0 : delay); return () => window.clearTimeout(timer); }, [delay, draftFingerprint, errorFingerprint, fingerprint, hydrated, manualRequest, savedFingerprint, saving, valid]); let state: AutosaveState; if (!valid) state = "incomplete"; else if (saving) state = "saving"; else if (fingerprint === errorFingerprint) state = "error"; else if (fingerprint !== savedFingerprint) state = "unsaved"; else state = "saved"; useEffect(() => { const browserWindow = window as Window & { __guideHasUnsavedChanges?: boolean }; browserWindow.__guideHasUnsavedChanges = state !== "saved"; if (state === "saved") return () => { browserWindow.__guideHasUnsavedChanges = false; }; const warnBeforeLeaving = (event: BeforeUnloadEvent) => { event.preventDefault(); event.returnValue = ""; }; window.addEventListener("beforeunload", warnBeforeLeaving); return () => { window.removeEventListener("beforeunload", warnBeforeLeaving); browserWindow.__guideHasUnsavedChanges = false; }; }, [state]); return { state, saveNow: () => { if (!valid || saving) return; setErrorFingerprint(null); setManualRequest((current) => current + 1); }, }; }