import { useMemo, useRef } from 'react'; import { Copy, Plus, Printer, ArrowDown } from 'lucide-react'; import { useProject } from './lib/use-project'; import { rectangle, nextId } from './lib/defaults'; import { buildGeometry } from './lib/geometry'; import { planFlooring } from './lib/flooring'; import { planSkirting } from './lib/skirting'; import { area, mm } from './lib/format'; import { isSkirtingCut } from './lib/cut-list'; import type { Project, Room } from './lib/types'; import { OutlineEditor } from './components/OutlineEditor'; import { MaterialsEditor } from './components/MaterialsEditor'; import { FloorPlan } from './components/FloorPlan'; import { CutList } from './components/CutList'; import { Issues } from './components/Fields'; const jump = (id: string) => document.getElementById(id)?.scrollIntoView({ block: 'start' }); export default function App() { const { state, error, update } = useProject(); const { project, view } = state; const copyStatus = useRef(null); const room = project.rooms.find((r) => r.id === view.roomId) ?? project.rooms[0]; const geometries = useMemo( () => project.rooms.map((r) => buildGeometry(r, project.tolerance)), [project.rooms, project.tolerance], ); const geometry = geometries[project.rooms.indexOf(room)]; const flooring = useMemo( () => planFlooring(project.rooms, project.flooring, project.tolerance), [project.rooms, project.flooring, project.tolerance], ); const skirting = useMemo( () => planSkirting(project.rooms, project.skirting, project.tolerance), [project.rooms, project.skirting, project.tolerance], ); const floorErrors = flooring.issues.filter((i) => i.severity === 'error'); const skirtErrors = skirting.issues.filter((i) => i.severity === 'error'); const roomErrors = [...floorErrors, ...skirtErrors].filter( (issue) => !issue.roomId || issue.roomId === room.id, ); const totalArea = geometries.reduce((sum, g) => sum + (g.valid ? g.area : 0), 0); const wholeBoards = flooring.pieces.filter( (p) => !p.cut && Math.abs(p.length - project.flooring.length) < 0.01 && Math.abs(p.width - project.flooring.width) < 0.01, ).length; const wholeSkirting = skirting.pieces.filter((p) => !isSkirtingCut(p, skirting)).length; const changeProject = (fn: (p: Project) => Project) => update((s) => ({ ...s, project: fn(s.project), view: { ...s.view, selectedPiece: null } })); const changeRoom = (fn: (r: Room) => Room) => changeProject((p) => ({ ...p, rooms: p.rooms.map((r) => (r.id === room.id ? fn(r) : r)) })); const select = (id: string | null, roomId?: string) => update((s) => ({ ...s, view: { ...s.view, roomId: roomId ?? s.view.roomId, selectedPiece: id }, })); const addRoom = () => update((s) => { const id = nextId( 'room', s.project.rooms.map((r) => r.id), ); return { ...s, project: { ...s.project, rooms: [...s.project.rooms, rectangle(id, `Room ${s.project.rooms.length + 1}`)], }, view: { ...s.view, roomId: id, selectedPiece: null }, }; }, true); const removeRoom = () => update((s) => { const rooms = s.project.rooms.filter((r) => r.id !== room.id); return { ...s, project: { ...s.project, rooms }, view: { ...s.view, roomId: rooms[0].id, selectedPiece: null }, }; }, true); const copyLink = async () => { try { await navigator.clipboard.writeText(window.location.href); if (copyStatus.current) copyStatus.current.textContent = 'Link copied'; } catch { if (copyStatus.current) copyStatus.current.textContent = 'Copy the address from your browser.'; } }; return ( <>
{error && (

{error}

)}
{project.rooms.map((r, index) => ( ))}
{area(totalArea)} m² total
1} />
select(id)} />

Materials to buy {project.rooms.length > 1 ? 'All rooms' : room.name}

); }