162 lines
5 KiB
TypeScript
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) },
|
|
};
|
|
}
|
|
}
|