64 lines
2.2 KiB
TypeScript
64 lines
2.2 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { decodeState, encodeState } from './url-state';
|
|
import { reduceProject } from './project-state';
|
|
import type { ProjectAction } from './project-state';
|
|
|
|
export const PROJECT_STORAGE_KEY = 'floor-planner.project';
|
|
|
|
function readLocation() {
|
|
const link = window.location.search;
|
|
if (link && link !== '?' && link !== '?planner') return decodeState(link);
|
|
try {
|
|
const saved = decodeState(window.localStorage.getItem(PROJECT_STORAGE_KEY) ?? '');
|
|
if (!saved.error) return saved;
|
|
} catch {
|
|
/* Storage may be unavailable. */
|
|
}
|
|
return decodeState('');
|
|
}
|
|
|
|
/** React owns live state. Persistence is an output between navigations. */
|
|
export function useProject() {
|
|
const [loaded, setLoaded] = useState(() => ({ ...readLocation(), milestone: false }));
|
|
useEffect(() => {
|
|
const restore = () => setLoaded({ ...readLocation(), milestone: false });
|
|
window.addEventListener('popstate', restore);
|
|
return () => window.removeEventListener('popstate', restore);
|
|
}, []);
|
|
useEffect(() => {
|
|
if (loaded.error) return;
|
|
try {
|
|
const query = encodeState(loaded.state);
|
|
if (query !== window.location.search || window.location.hash)
|
|
window.history[loaded.milestone ? 'pushState' : 'replaceState'](
|
|
null,
|
|
'',
|
|
`${window.location.pathname}${query}`,
|
|
);
|
|
try {
|
|
window.localStorage.setItem(PROJECT_STORAGE_KEY, query);
|
|
} catch {
|
|
/* The URL remains available if storage is blocked or full. */
|
|
}
|
|
} catch (error) {
|
|
// Keep edits usable even if the browser cannot save this link.
|
|
setLoaded((current) =>
|
|
current === loaded
|
|
? {
|
|
...current,
|
|
error: error instanceof Error ? error.message : 'This plan could not be saved.',
|
|
}
|
|
: current,
|
|
);
|
|
}
|
|
}, [loaded]);
|
|
const dispatch = useCallback((action: ProjectAction) => {
|
|
setLoaded((current) => ({
|
|
state: reduceProject(current.state, action),
|
|
error: null,
|
|
milestone:
|
|
action.type === 'addRoom' || action.type === 'removeRoom' || action.type === 'visitRoom',
|
|
}));
|
|
}, []);
|
|
return { ...loaded, dispatch };
|
|
}
|