Simplify
Some checks failed
Deploy to Pages / build (push) Failing after 10m45s

This commit is contained in:
Andras Schmelczer 2026-09-11 23:27:29 +01:00
commit 8bf3e3b705
40 changed files with 1443 additions and 1497 deletions

View file

@ -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 };
}