floor-planner/src/lib/project-state.ts
Andras Schmelczer 8bf3e3b705
Some checks failed
Deploy to Pages / build (push) Failing after 10m45s
Simplify
2026-09-11 23:27:29 +01:00

162 lines
5 KiB
TypeScript

import { nextId, rectangle } from './defaults';
import { PROJECT_LIMITS } from './limits';
import type {
FlooringSettings,
PlanFocus,
PlanMaterial,
PlannerSection,
Project,
Room,
SkirtingSettings,
UrlState,
} from './types';
export type ProjectAction =
| { type: 'room'; id: string; change: (room: Room) => Room }
| { type: 'flooring'; patch: Partial<FlooringSettings> }
| { type: 'skirting'; patch: Partial<SkirtingSettings> }
| { type: 'tolerance'; value: number }
| { type: 'addRoom' }
| { type: 'removeRoom'; id: string }
| { type: 'visitRoom'; id: string }
| { type: 'material'; material: PlanMaterial }
| { type: 'select'; material: PlanMaterial; id: string; roomId?: string }
| { type: 'closePreview' }
| { type: 'search'; text: string; roomId: string; material: PlanMaterial }
| { type: 'focus'; focus: PlanFocus; roomId: string }
| { type: 'toggleBoard'; key: string }
| { type: 'toggleSection'; section: PlannerSection };
const toggle = <T>(items: T[], value: T) =>
items.includes(value) ? items.filter((item) => item !== value) : [...items, value];
/** Allocation changes invalidate progress and locations together. Renames leave them intact. */
function changedPlan(state: UrlState, project: Project): UrlState {
return {
project: { ...project, completedBoards: [] },
view: { ...state.view, selectedPiece: null, search: '', focus: null },
};
}
export function reduceProject(state: UrlState, action: ProjectAction): UrlState {
const { project, view } = state;
switch (action.type) {
case 'room': {
const room = project.rooms.find((room) => room.id === action.id);
if (!room) return state;
const next = action.change(room);
if (next === room) return state;
const updated = {
...project,
rooms: project.rooms.map((item) => (item === room ? next : item)),
};
return next.walls === room.walls && next.direction === room.direction
? { ...state, project: updated }
: changedPlan(state, updated);
}
case 'flooring':
case 'skirting': {
if (
Object.entries(action.patch).every(
([key, value]) => Reflect.get(project[action.type], key) === value,
)
)
return state;
return changedPlan(state, {
...project,
[action.type]: { ...project[action.type], ...action.patch },
});
}
case 'tolerance':
return action.value === project.tolerance
? state
: changedPlan(state, { ...project, tolerance: action.value });
case 'addRoom': {
if (project.rooms.length >= PROJECT_LIMITS.rooms) return state;
const id = nextId(
'room',
project.rooms.map((room) => room.id),
);
const next = changedPlan(state, {
...project,
rooms: [...project.rooms, rectangle(id, `Room ${project.rooms.length + 1}`)],
});
return {
...next,
view: {
...next.view,
roomId: id,
collapsedSections: view.collapsedSections.filter((section) => section !== 'measurements'),
},
};
}
case 'removeRoom': {
if (project.rooms.length <= 1) return state;
const rooms = project.rooms.filter((room) => room.id !== action.id);
if (rooms.length === project.rooms.length) return state;
const next = changedPlan(state, { ...project, rooms });
return {
...next,
view: {
...next.view,
roomId: rooms.some((room) => room.id === view.roomId) ? view.roomId : rooms[0].id,
},
};
}
case 'visitRoom':
return project.rooms.some((room) => room.id === action.id)
? { ...state, view: { ...view, roomId: action.id, selectedPiece: null } }
: state;
case 'material':
return {
...state,
view: { ...view, material: action.material, selectedPiece: null, search: '', focus: null },
};
case 'select':
return {
...state,
view: {
...view,
selectedPiece: `${action.material}:${action.id}`,
material: action.material,
roomId: action.roomId ?? view.roomId,
},
};
case 'closePreview':
return { ...state, view: { ...view, selectedPiece: null } };
case 'search':
return {
...state,
view: {
...view,
search: action.text,
roomId: action.roomId,
material: action.material,
focus: null,
selectedPiece: null,
},
};
case 'focus':
return {
...state,
view: {
...view,
focus: action.focus,
material: action.focus.material,
roomId: action.roomId,
search: '',
selectedPiece: null,
},
};
case 'toggleBoard':
return {
...state,
project: { ...project, completedBoards: toggle(project.completedBoards, action.key) },
};
case 'toggleSection':
return {
...state,
view: { ...view, collapsedSections: toggle(view.collapsedSections, action.section) },
};
}
}