67 lines
2 KiB
TypeScript
67 lines
2 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
|
|
/**
|
|
* Autosave a long form so it isn't lost on navigation or reload.
|
|
*
|
|
* - Persists `snapshot` to localStorage under `<prefix><key>` (content-debounced:
|
|
* the serialized string only changes when the draft actually changes).
|
|
* - A saved draft is only offered as **resumable** when it *differs* from the
|
|
* current form state — so merely opening an existing record (whose prefill
|
|
* matches the saved draft) never shows a spurious "resume" banner, and a
|
|
* draft can never silently clobber a freshly loaded record.
|
|
* - Call `clear()` after a successful publish.
|
|
*
|
|
* Used by the Lab profile forms (`useLabDraft`) and the create-spot form.
|
|
*/
|
|
export function useFormDraft<T>(
|
|
prefix: string,
|
|
key: string,
|
|
snapshot: T,
|
|
apply: (draft: T) => void,
|
|
) {
|
|
const storageKey = prefix + key;
|
|
const [loaded, setLoaded] = useState<T | null>(null);
|
|
const firstRun = useRef(true);
|
|
const serialized = JSON.stringify(snapshot);
|
|
|
|
// Load any saved draft once.
|
|
useEffect(() => {
|
|
try {
|
|
const raw = localStorage.getItem(storageKey);
|
|
if (raw) setLoaded(JSON.parse(raw) as T);
|
|
} catch {
|
|
// ignore malformed/blocked storage
|
|
}
|
|
}, [storageKey]);
|
|
|
|
// Persist on change (skip the initial empty paint).
|
|
useEffect(() => {
|
|
if (firstRun.current) {
|
|
firstRun.current = false;
|
|
return;
|
|
}
|
|
try {
|
|
localStorage.setItem(storageKey, serialized);
|
|
} catch {
|
|
// storage may be full/blocked — a lost draft is not fatal
|
|
}
|
|
}, [serialized, storageKey]);
|
|
|
|
const resume = () => {
|
|
if (!loaded) return;
|
|
apply(loaded);
|
|
setLoaded(null);
|
|
};
|
|
|
|
const discard = () => {
|
|
setLoaded(null);
|
|
try { localStorage.removeItem(storageKey); } catch { /* ignore */ }
|
|
};
|
|
|
|
const clear = () => {
|
|
try { localStorage.removeItem(storageKey); } catch { /* ignore */ }
|
|
};
|
|
|
|
const resumable = loaded !== null && JSON.stringify(loaded) !== serialized;
|
|
return { resumable, resume, discard, clear };
|
|
}
|