This commit is contained in:
parent
171fbf1927
commit
8bf3e3b705
40 changed files with 1443 additions and 1497 deletions
|
|
@ -1,85 +1,64 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { UrlState } from './types';
|
||||
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';
|
||||
|
||||
const readLocation = () => {
|
||||
// The landing-page CTA opens the last project; it is not a project override.
|
||||
const link = window.location.search === '?planner' ? '' : window.location.search;
|
||||
if (link && link !== '?') return decodeState(link);
|
||||
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 {
|
||||
// Browser privacy settings can make localStorage unavailable.
|
||||
/* Storage may be unavailable. */
|
||||
}
|
||||
return decodeState('');
|
||||
};
|
||||
|
||||
const saveLocal = (query: string) => {
|
||||
try {
|
||||
window.localStorage.setItem(PROJECT_STORAGE_KEY, query);
|
||||
} catch {
|
||||
// Keep URL persistence working if storage is blocked or full.
|
||||
}
|
||||
};
|
||||
|
||||
const queryUrl = (query: string) => `${window.location.pathname}${query}`;
|
||||
|
||||
/** Persist every parameter in the URL and locally; explicit links take precedence. */
|
||||
export function useProject() {
|
||||
const [loaded, setLoaded] = useState(readLocation);
|
||||
useEffect(() => {
|
||||
const restore = () => {
|
||||
const next = readLocation();
|
||||
if (!next.error) {
|
||||
try {
|
||||
// Canonicalize editable queries without creating an extra history entry.
|
||||
const query = encodeState(next.state);
|
||||
if (query !== window.location.search || window.location.hash)
|
||||
window.history.replaceState(null, '', queryUrl(query));
|
||||
saveLocal(query);
|
||||
} catch (error) {
|
||||
next.error = error instanceof Error ? error.message : 'This plan could not be saved.';
|
||||
}
|
||||
}
|
||||
setLoaded(next);
|
||||
};
|
||||
window.addEventListener('popstate', restore);
|
||||
restore();
|
||||
return () => {
|
||||
window.removeEventListener('popstate', restore);
|
||||
};
|
||||
}, []);
|
||||
const update = useCallback((transform: (current: UrlState) => UrlState, milestone = false) => {
|
||||
setLoaded((current) => {
|
||||
const next = transform(current.state);
|
||||
try {
|
||||
const query = encodeState(next);
|
||||
if (query !== window.location.search || window.location.hash)
|
||||
window.history[milestone ? 'pushState' : 'replaceState'](null, '', queryUrl(query));
|
||||
saveLocal(query);
|
||||
const canonical = decodeState(query).state;
|
||||
if (next.project === current.state.project) canonical.project = current.state.project;
|
||||
else {
|
||||
// Progress changes should not rerun geometry and stock allocation.
|
||||
if (next.project.rooms === current.state.project.rooms)
|
||||
canonical.project.rooms = current.state.project.rooms;
|
||||
if (next.project.flooring === current.state.project.flooring)
|
||||
canonical.project.flooring = current.state.project.flooring;
|
||||
if (next.project.skirting === current.state.project.skirting)
|
||||
canonical.project.skirting = current.state.project.skirting;
|
||||
}
|
||||
return { state: canonical, error: null };
|
||||
} catch (error) {
|
||||
return {
|
||||
...current,
|
||||
error:
|
||||
error instanceof Error ? error.message : 'This project could not be saved in the URL.',
|
||||
};
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
return { ...loaded, update };
|
||||
}
|
||||
|
||||
/** 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 };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue