This commit is contained in:
parent
7959fe85cc
commit
b9485e4097
32 changed files with 4692 additions and 65 deletions
491
src/components/LandingPage.tsx
Normal file
491
src/components/LandingPage.tsx
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
import {
|
||||
ArrowDown,
|
||||
ArrowRight,
|
||||
Check,
|
||||
ClipboardList,
|
||||
MoveUpRight,
|
||||
Recycle,
|
||||
Ruler,
|
||||
} from 'lucide-react';
|
||||
import '../landing.css';
|
||||
|
||||
const installationGuide = 'https://www.quick-step.co.uk/en-gb/laminate/installation';
|
||||
const finishingGuide = 'https://www.quick-step.co.uk/en-gb/laminate/finishing';
|
||||
|
||||
function PlannerLink({ compact = false }: { compact?: boolean }) {
|
||||
return (
|
||||
<a className={`landing-cta${compact ? ' landing-cta-small' : ''}`} href="?planner">
|
||||
{compact ? 'Open planner' : 'Plan my floor'}
|
||||
<ArrowRight size={compact ? 17 : 21} aria-hidden="true" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function PlanIllustration() {
|
||||
return (
|
||||
<figure className="landing-plan">
|
||||
<figcaption className="landing-plan-heading">
|
||||
<span>
|
||||
<span className="landing-status-dot" /> A little planning goes a long way.
|
||||
</span>
|
||||
<span className="landing-plan-index">FIG. 01</span>
|
||||
</figcaption>
|
||||
<div className="landing-plan-paper">
|
||||
<div className="landing-plan-room">
|
||||
<span>Living room</span>
|
||||
<span>
|
||||
4.8 × 3.6 m <span aria-hidden="true">/</span> 17.28 m²
|
||||
</span>
|
||||
</div>
|
||||
<svg viewBox="0 0 540 380" role="img" aria-labelledby="landing-plan-title">
|
||||
<title id="landing-plan-title">
|
||||
Illustrative floor plan with staggered floorboards, labelled cuts and skirting around a
|
||||
4.8 by 3.6 metre room.
|
||||
</title>
|
||||
<defs>
|
||||
<pattern id="landing-grid" width="20" height="20" patternUnits="userSpaceOnUse">
|
||||
<path d="M20 0H0V20" fill="none" stroke="#dfe4df" strokeWidth="0.7" />
|
||||
</pattern>
|
||||
<clipPath id="landing-room-clip">
|
||||
<rect x="91" y="68" width="358" height="264" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect width="540" height="380" fill="url(#landing-grid)" />
|
||||
<g stroke="#748279" strokeWidth="1" fill="none">
|
||||
<path d="M80 48V23M460 48V23M80 32H460M75 37L85 27M455 37L465 27" />
|
||||
<path d="M62 57H37M62 343H37M46 57V343M41 62L51 52M41 348L51 338" />
|
||||
</g>
|
||||
<g fill="#55685d" fontSize="12" fontFamily="monospace" textAnchor="middle">
|
||||
<rect x="224" y="21" width="92" height="22" fill="#f6f7f2" />
|
||||
<text x="270" y="36">
|
||||
4,800 mm
|
||||
</text>
|
||||
<text transform="translate(36 200) rotate(-90)">3,600 mm</text>
|
||||
</g>
|
||||
<rect
|
||||
x="80"
|
||||
y="57"
|
||||
width="380"
|
||||
height="286"
|
||||
fill="#fffdf7"
|
||||
stroke="#17263f"
|
||||
strokeWidth="3"
|
||||
/>
|
||||
<rect
|
||||
x="85"
|
||||
y="62"
|
||||
width="370"
|
||||
height="276"
|
||||
fill="none"
|
||||
stroke="#b74900"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<g clipPath="url(#landing-room-clip)">
|
||||
{Array.from({ length: 12 }, (_, row) =>
|
||||
Array.from({ length: 5 }, (_, col) => (
|
||||
<rect
|
||||
key={`${row}-${col}`}
|
||||
x={91 + col * 96 - (row % 2 === 0 ? 24 : 72)}
|
||||
y={68 + row * 22}
|
||||
width="96"
|
||||
height="22"
|
||||
fill={
|
||||
(row + col) % 3 === 0
|
||||
? '#ead9bc'
|
||||
: (row + col) % 3 === 1
|
||||
? '#f1e5d1'
|
||||
: '#e5d0ae'
|
||||
}
|
||||
stroke="#b49a72"
|
||||
strokeWidth="0.8"
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
<rect x="91" y="112" width="72" height="22" fill="#c66935" stroke="#a74617" />
|
||||
<rect x="403" y="178" width="46" height="22" fill="#c66935" stroke="#a74617" />
|
||||
</g>
|
||||
<g fontSize="11" fontFamily="monospace" fill="#fff" textAnchor="middle">
|
||||
<text x="127" y="127">
|
||||
F1
|
||||
</text>
|
||||
<text x="426" y="193">
|
||||
F2
|
||||
</text>
|
||||
</g>
|
||||
<rect x="226" y="177" width="114" height="48" rx="2" fill="#fffdf7" stroke="#b49a72" />
|
||||
<text x="283" y="198" fontSize="13" fontWeight="600" fill="#17263f" textAnchor="middle">
|
||||
Living room
|
||||
</text>
|
||||
<text x="283" y="214" fontSize="10" letterSpacing="1" fill="#58655e" textAnchor="middle">
|
||||
EXAMPLE LAYOUT
|
||||
</text>
|
||||
<path d="M79 257V317" stroke="#f6f7f2" strokeWidth="15" />
|
||||
<path
|
||||
d="M80 317H140M140 317A60 60 0 0 0 80 257"
|
||||
fill="none"
|
||||
stroke="#17263f"
|
||||
strokeWidth="1.2"
|
||||
/>
|
||||
<path d="M471 321H496M490 315L496 321L490 327" fill="none" stroke="#748279" />
|
||||
<text x="470" y="307" fontSize="10" fontFamily="monospace" fill="#55685d">
|
||||
LAY
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="landing-plan-legend">
|
||||
<span>
|
||||
<i className="legend-floor" /> Floorboards
|
||||
</span>
|
||||
<span>
|
||||
<i className="legend-trim" /> Skirting
|
||||
</span>
|
||||
<span>
|
||||
<i className="legend-cut" /> Labelled cuts
|
||||
</span>
|
||||
</div>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
function TrimIllustration({ scotia }: { scotia: boolean }) {
|
||||
const name = scotia ? 'scotia' : 'skirting';
|
||||
return (
|
||||
<svg
|
||||
className="landing-trim-diagram"
|
||||
viewBox="0 0 400 215"
|
||||
role="img"
|
||||
aria-labelledby={`${name}-title`}
|
||||
>
|
||||
<title id={`${name}-title`}>
|
||||
{scotia
|
||||
? 'Side view: scotia sits in front of existing skirting and covers the expansion gap above the floating floor.'
|
||||
: 'Side view: a new skirting board fixed to the wall covers the expansion gap, with the floating floor extending underneath.'}
|
||||
</title>
|
||||
<path d="M48 25H84V181H48" fill="#e3e4df" stroke="#9aaba0" />
|
||||
<path d="M48 181H353V193H48" fill="#dad8ce" />
|
||||
<path
|
||||
d={scotia ? 'M126 161H353V180H126Z' : 'M98 161H353V180H98Z'}
|
||||
fill="#e4c49c"
|
||||
stroke="#a78052"
|
||||
/>
|
||||
<path d="M203 162V180M303 162V180" stroke="#a78052" />
|
||||
{scotia ? (
|
||||
<>
|
||||
<path d="M84 63H100Q110 63 110 74V181H84Z" fill="#fffdf7" stroke="#7f8e84" />
|
||||
<path
|
||||
d="M110 129H116Q116 154 147 155V161H110Z"
|
||||
fill="#c66935"
|
||||
stroke="#9e4419"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path d="M142 143L173 113H225" fill="none" stroke="#a4471c" />
|
||||
<text x="233" y="117" fill="#a4471c" fontSize="13" fontWeight="600">
|
||||
Scotia bead
|
||||
</text>
|
||||
<path d="M102 85H225" stroke="#7f8e84" />
|
||||
<text x="233" y="89" fill="#526156" fontSize="12">
|
||||
Existing skirting
|
||||
</text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<path
|
||||
d="M84 54H98Q121 54 121 74V159H84Z"
|
||||
fill="#c66935"
|
||||
stroke="#9e4419"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path d="M113 85H225" stroke="#a4471c" />
|
||||
<text x="233" y="89" fill="#a4471c" fontSize="13" fontWeight="600">
|
||||
New skirting
|
||||
</text>
|
||||
<path d="M108 154L175 127H225" fill="none" stroke="#7f8e84" />
|
||||
<text x="233" y="131" fill="#526156" fontSize="12">
|
||||
Floor tucks under
|
||||
</text>
|
||||
</>
|
||||
)}
|
||||
<path
|
||||
d={scotia ? 'M118 172V206H189' : 'M91 172V206H189'}
|
||||
fill="none"
|
||||
stroke="#526156"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<text x="197" y="210" fill="#526156" fontSize="11">
|
||||
Expansion gap
|
||||
</text>
|
||||
<text x="48" y="15" fill="#526156" fontSize="10" letterSpacing="1">
|
||||
WALL
|
||||
</text>
|
||||
<text x="278" y="151" fill="#526156" fontSize="10" letterSpacing="1">
|
||||
FLOORING
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const benefits = [
|
||||
{
|
||||
icon: Ruler,
|
||||
title: 'See it before you lay it.',
|
||||
text: 'Add your room, doors and board sizes. See how the rows, edges and staggered joints fit together before the first cut.',
|
||||
},
|
||||
{
|
||||
icon: Recycle,
|
||||
title: 'Make more of every board.',
|
||||
text: 'Get quantities from an actual layout, with usable offcuts shared across rooms. Add your own allowance for mistakes and damage.',
|
||||
},
|
||||
{
|
||||
icon: ClipboardList,
|
||||
title: 'Take a plan to the floor.',
|
||||
text: 'Follow labelled floorboard and skirting cuts, tick off finished boards, and print your plan. Save or share the project by copying its link.',
|
||||
},
|
||||
];
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: 'Measure & make a plan',
|
||||
text: 'Measure each wall, recess and doorway in millimetres. Enter your board sizes, choose a laying direction, and check the first and last rows before buying.',
|
||||
},
|
||||
{
|
||||
title: 'Get the room ready',
|
||||
text: 'Acclimatise the packs as instructed. Check that the subfloor is dry, clean, flat and stable, then fit the specified underlay and moisture barrier where required.',
|
||||
},
|
||||
{
|
||||
title: 'Lay, check, repeat',
|
||||
text: 'Use spacers for the required expansion gap. Follow the click-system instructions, stagger the end joints, and measure each cut on site. Keep suitable offcuts for later rows.',
|
||||
},
|
||||
{
|
||||
title: 'Finish the edges',
|
||||
text: 'Remove the spacers. Fit skirting or scotia to cover the gap, fixing it to the wall or existing skirting so the floor can move. Fit doorway transitions as instructed.',
|
||||
},
|
||||
];
|
||||
|
||||
export function LandingPage() {
|
||||
return (
|
||||
<div className="landing-page">
|
||||
<a className="landing-skip" href="#main">
|
||||
Skip to content
|
||||
</a>
|
||||
<header className="landing-header landing-container">
|
||||
<a
|
||||
className="landing-brand"
|
||||
href={window.location.pathname}
|
||||
aria-label="Floor planner home"
|
||||
>
|
||||
<span className="square-mark" aria-hidden="true" />
|
||||
<span>
|
||||
Floor planner<span className="landing-brand-dot">.</span>
|
||||
</span>
|
||||
</a>
|
||||
<nav className="landing-nav" aria-label="Main navigation">
|
||||
<a href="#why">Why plan?</a>
|
||||
<a href="#guide">DIY guide</a>
|
||||
<a href="#finishing">The finishing touches</a>
|
||||
</nav>
|
||||
<PlannerLink compact />
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
<section className="landing-hero landing-container" aria-labelledby="landing-title">
|
||||
<div className="landing-hero-copy">
|
||||
<p className="landing-eyebrow">
|
||||
<span /> MEASURE. PLAN. LAY.
|
||||
</p>
|
||||
<h1 id="landing-title">
|
||||
A great floor
|
||||
<br />
|
||||
starts with
|
||||
<br />
|
||||
<em>a little planning.</em>
|
||||
</h1>
|
||||
<p className="landing-intro">
|
||||
From room measurements to the last cut. Plan your floorboards and skirting, make the
|
||||
most of your materials, and start your DIY project with confidence.
|
||||
</p>
|
||||
<div className="landing-hero-actions">
|
||||
<PlannerLink />
|
||||
<a className="landing-guide-link" href="#guide">
|
||||
First time fitting a floor? <ArrowDown size={16} aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
<p className="landing-reassurance">
|
||||
<Check size={14} aria-hidden="true" /> Free to use <span>·</span> No sign-up{' '}
|
||||
<span>·</span> Right in your browser
|
||||
</p>
|
||||
</div>
|
||||
<PlanIllustration />
|
||||
</section>
|
||||
|
||||
<section
|
||||
className="landing-benefits landing-container"
|
||||
id="why"
|
||||
aria-labelledby="benefits-title"
|
||||
>
|
||||
<div className="landing-section-kicker">
|
||||
<span className="landing-eyebrow">LESS GUESSWORK. MORE GETTING IT DONE.</span>
|
||||
<h2 id="benefits-title">Your tape measure’s new best friend.</h2>
|
||||
</div>
|
||||
<div className="landing-benefit-grid">
|
||||
{benefits.map(({ icon: Icon, title, text }, index) => (
|
||||
<article className="landing-benefit" key={title}>
|
||||
<div className="landing-benefit-top">
|
||||
<Icon size={24} strokeWidth={1.5} aria-hidden="true" />
|
||||
<span>0{index + 1}</span>
|
||||
</div>
|
||||
<h3>{title}</h3>
|
||||
<p>{text}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="landing-guide" id="guide" aria-labelledby="guide-title">
|
||||
<div className="landing-container landing-guide-layout">
|
||||
<div className="landing-guide-intro">
|
||||
<p className="landing-eyebrow">THE DIY STARTING POINT</p>
|
||||
<h2 id="guide-title">
|
||||
Your first floor?
|
||||
<br />
|
||||
You’ve got this.
|
||||
</h2>
|
||||
<p>
|
||||
A little patience, a good plan, and one row at a time. Here’s the journey for a
|
||||
floating click-fit laminate floor.
|
||||
</p>
|
||||
<a
|
||||
className="landing-text-link"
|
||||
href={installationGuide}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Read a manufacturer’s fitting guide <MoveUpRight size={15} aria-hidden="true" />
|
||||
<span className="landing-sr-only"> (opens in a new tab)</span>
|
||||
</a>
|
||||
<div className="landing-tool-note">
|
||||
<Ruler size={20} aria-hidden="true" />
|
||||
<p>
|
||||
<strong>Get your kit together</strong>Tape measure, pencil, square, spacers, a
|
||||
suitable saw, the recommended fitting tools, and eye, ear and dust protection.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ol className="landing-steps">
|
||||
{steps.map(({ title, text }, index) => (
|
||||
<li key={title}>
|
||||
<span className="landing-step-number">0{index + 1}</span>
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<p className="landing-guide-note">
|
||||
Your flooring’s instructions set the expansion gap, acclimatisation time, underlay
|
||||
and minimum joint spacing. Use those values in the planner and check every
|
||||
measurement before cutting.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
className="landing-finishing landing-container"
|
||||
id="finishing"
|
||||
aria-labelledby="finishing-title"
|
||||
>
|
||||
<div className="landing-finishing-heading">
|
||||
<div>
|
||||
<p className="landing-eyebrow">SMALL DETAILS. A PROPER FINISH.</p>
|
||||
<h2 id="finishing-title">Scotia or skirting board?</h2>
|
||||
</div>
|
||||
<p>
|
||||
Both can hide the expansion gap around a floating floor. The difference is what you’re
|
||||
keeping, and the finish you want.
|
||||
</p>
|
||||
</div>
|
||||
<div className="landing-trim-grid">
|
||||
<article className="landing-trim-card">
|
||||
<div className="landing-trim-label">
|
||||
<span>KEEP YOUR EXISTING SKIRTING</span>
|
||||
<span>01</span>
|
||||
</div>
|
||||
<TrimIllustration scotia />
|
||||
<div className="landing-trim-copy">
|
||||
<h3>
|
||||
Scotia <span>the small finishing bead</span>
|
||||
</h3>
|
||||
<p>
|
||||
A narrow, curved moulding fitted at the foot of your existing skirting. It covers
|
||||
the flooring’s expansion gap without removing the boards already on your wall.
|
||||
</p>
|
||||
<p className="landing-trim-choice">
|
||||
<Check size={16} aria-hidden="true" /> A practical choice when the skirting is
|
||||
staying.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
<article className="landing-trim-card">
|
||||
<div className="landing-trim-label">
|
||||
<span>REPLACE OR REFIT YOUR SKIRTING</span>
|
||||
<span>02</span>
|
||||
</div>
|
||||
<TrimIllustration scotia={false} />
|
||||
<div className="landing-trim-copy">
|
||||
<h3>
|
||||
Skirting board <span>also called a baseboard</span>
|
||||
</h3>
|
||||
<p>
|
||||
The taller board along the base of the wall. When fitted after the flooring, it
|
||||
can cover the expansion gap itself, giving you a clean edge without an extra bead.
|
||||
</p>
|
||||
<p className="landing-trim-choice">
|
||||
<Check size={16} aria-hidden="true" /> A neat finish when you’re renovating the
|
||||
whole room.
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div className="landing-finishing-note">
|
||||
<p>
|
||||
<strong>Leave room to move.</strong> Choose trim that covers the required gap, and
|
||||
never fix it to the floating floor. The app plans flooring and skirting cuts; scotia
|
||||
needs its own profile-specific fitting instructions.
|
||||
</p>
|
||||
<a className="landing-text-link" href={finishingGuide} target="_blank" rel="noreferrer">
|
||||
More on finishing <MoveUpRight size={15} aria-hidden="true" />
|
||||
<span className="landing-sr-only"> (opens in a new tab)</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="landing-final-cta landing-container" aria-labelledby="ready-title">
|
||||
<div>
|
||||
<p className="landing-eyebrow">YOUR NEXT PROJECT STARTS HERE</p>
|
||||
<h2 id="ready-title">
|
||||
Measure twice.
|
||||
<br />
|
||||
<em>Start with a plan.</em>
|
||||
</h2>
|
||||
<p>Bring your room measurements. We’ll help with the layout.</p>
|
||||
</div>
|
||||
<div className="landing-final-actions">
|
||||
<PlannerLink />
|
||||
<span>Open the planner. Make it your room.</span>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="landing-footer landing-container">
|
||||
<a className="landing-brand" href={window.location.pathname}>
|
||||
<span className="square-mark" aria-hidden="true" />
|
||||
<span>Floor planner.</span>
|
||||
</a>
|
||||
<p>A little preparation for a floor you’ll be proud of.</p>
|
||||
<a href="#main">Back to top ↑</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import { ArrowRight, ArrowDown } from 'lucide-react';
|
||||
import type { Project, Room, FlooringSettings, SkirtingSettings } from '../lib/types';
|
||||
import { NumberField } from './Fields';
|
||||
import type { FlooringOptions } from '../lib/flooring';
|
||||
import { mm } from '../lib/format';
|
||||
|
||||
interface Props {
|
||||
project: Project;
|
||||
room: Room;
|
||||
flooringOptions: FlooringOptions;
|
||||
changeProject: (fn: (p: Project) => Project) => void;
|
||||
changeRoom: (fn: (r: Room) => Room) => void;
|
||||
}
|
||||
|
|
@ -12,7 +15,13 @@ interface Props {
|
|||
const kerfHelp =
|
||||
'Width of material removed by the saw blade. A 3 mm blade turns a 1,000 mm board into 600 mm + 397 mm after one cut.';
|
||||
|
||||
export function MaterialsEditor({ project, room, changeProject, changeRoom }: Props) {
|
||||
export function MaterialsEditor({
|
||||
project,
|
||||
room,
|
||||
flooringOptions,
|
||||
changeProject,
|
||||
changeRoom,
|
||||
}: Props) {
|
||||
const f = project.flooring;
|
||||
const s = project.skirting;
|
||||
const floor = (key: keyof FlooringSettings, value: number | string) =>
|
||||
|
|
@ -63,6 +72,34 @@ export function MaterialsEditor({ project, room, changeProject, changeRoom }: Pr
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{f.pattern === 'random' && (
|
||||
<div className="field">
|
||||
<span>Optimise random flooring · all rooms</span>
|
||||
<div className="optimisation-options" role="group" aria-label="Flooring optimisation">
|
||||
{(['cuts', 'random'] as const).map((priority) => {
|
||||
const plan = flooringOptions[priority];
|
||||
const valid = !plan.issues.some((issue) => issue.severity === 'error');
|
||||
const selected = (f.optimisation ?? 'cuts') === priority;
|
||||
return (
|
||||
<button
|
||||
key={priority}
|
||||
type="button"
|
||||
className={selected ? 'active' : ''}
|
||||
aria-pressed={selected}
|
||||
onClick={() => floor('optimisation', priority)}
|
||||
>
|
||||
<span>{priority === 'cuts' ? 'Minimise cuts' : 'Maximise randomness'}</span>
|
||||
<small>
|
||||
{valid
|
||||
? `${plan.cuts} cuts · ${mm(plan.wastePercent)}% waste`
|
||||
: 'Check measurements'}
|
||||
</small>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="field">
|
||||
<span>Board direction · {room.name}</span>
|
||||
<div className="segmented" role="group" aria-label="Board direction">
|
||||
|
|
|
|||
Loading…
Reference in a new issue