diff --git a/CALC-SPEC-HOME-ENERGY-DATE.md b/CALC-SPEC-HOME-ENERGY-DATE.md new file mode 100644 index 0000000..e21b864 --- /dev/null +++ b/CALC-SPEC-HOME-ENERGY-DATE.md @@ -0,0 +1,88 @@ +# Calculator Cohort Spec: Home / Energy (traffic-growth episode) — REVISED v2 + +> Episode `01KSRA0R8FEVMVYRJXNAC8NY22`. 5 new calculators chosen for RANKABILITY +> (low-competition, high-intent, simple-correct math), affiliate-friendly. +> v2 changes (plan-design-critic + plan-eng-critic round 1): +> - DROPPED "Days Between Dates" + "Business Days" — duplicates of the existing +> DateDifferenceCalculator (days/weeks/months/years + "Show business days" toggle). +> - UI pattern switched to the SHARED UI KIT (the Home siblings Tile/Paint/Mulch use it), +> NOT the legacy calc-card pattern. +> - EV cost units pinned to GBP; BTU heating multiplier pinned to a single constant. +> - Added input affordances, invalid-state UX, and result hierarchy per calc. + +## Why these (rankability rationale) +GSC: site is indexed but ranks pos 56-89 on saturated head terms (0 clicks). These 5 target lower-competition, specific-intent long-tail where page-1 is achievable, are affiliate-friendly (home improvement / EV), and use simple geometry/arithmetic so the math is verifiably correct. "Home" category is thin (8). + +## CANONICAL LAYOUT — USE THE SHARED UI KIT (match the Home siblings) +The Home-category siblings (TileCalculator, PaintCalculator, MulchCalculator) all use the shared design-system kit via the `useCalculatorBase` hook. The new calcs MUST match them, NOT the legacy `calc-card`/`class` pattern. + +REFERENCE TO COPY: `src/components/calculators/TileCalculator/` (types.ts + calculations.ts + TileCalculator.tsx + index.ts) and `src/components/calculators/MulchCalculator/`. + +Each calculator = these files: +1. `src/components/calculators/[Name]/types.ts` — export the `[Name]Inputs` interface, `[Name]Result` interface, any selector enums, and `getDefaultInputs(): [Name]Inputs`. Include `currency: Currency` (from `../../../lib/regions`) where money is shown. +2. `src/components/calculators/[Name]/calculations.ts` — pure `calculate[Name](inputs): [Name]Result`. NaN-safe: `const safe = (v) => Number.isFinite(v) ? Math.max(0, v) : 0;` on every numeric input (Math.max(0, NaN) is NaN). No JSX. +3. `src/components/calculators/[Name]/[Name].tsx` — `import { useCalculatorBase } from '../../../hooks/useCalculatorBase';` then `const { inputs, result, updateInput } = useCalculatorBase({ name, slug: 'calc-[slug]-inputs', defaults: getDefaultInputs, compute: calculate[Name] });`. Build the UI from the kit (`import { ThemeProvider, Card, CalculatorHeader, Label, Input, Select, ButtonGroup, Toggle, Grid, Divider, ResultCard, MetricCard, Alert } from '../../ui';`). Use `className` (kit convention), NOT `class`. Live update (no submit button). Default export. Wrap in `...`. +4. `src/components/calculators/[Name]/index.ts` — `export { default as [Name] } from './[Name]'; export * from './calculations'; export * from './types';` +5. `src/pages/calculators/[slug].astro` — CalculatorLayout + SEOHead + HeroSection + ContentSection + FAQSection; component via folder named import `{ [Name] }`; `client:load`; 6+ accurate FAQs; 600+ words across 3 h2 sections ("How to use", "How it is calculated" with the formula in words, "Understanding your results"); `related` 3-5 from the safe pool below. +6. `tests/calculations/[slug-without-calculator-suffix].test.ts` — vitest; import `calculate[Name]` from the calc file; 4+ hand-computed assertions incl an edge case (zero/NaN, unit conversion, or a guarded degenerate state). This location is REQUIRED (vitest only scans `tests/**`). + +### UI affordances (use the kit; do not hand-roll) +- Numeric fields: `Input` (with `variant="currency"` for money). +- Unit toggles (m/ft), shape selectors, sun-exposure, charging mode: `ButtonGroup` or `Select`. +- Booleans (kitchen, include-spare): `Toggle`. +- Primary result: ONE `ResultCard` (the headline number). Secondary figures: `MetricCard` in a `Grid`. +- Invalid/degenerate state: render an `Alert variant="warning"` instead of a misleading number (see each spec). Never show NaN. + +### Hard constraints +NO emojis. NO em dashes in frontend strings. NO fake stats. Math MUST be correct. Currency `Intl.NumberFormat('en-GB',...maximumFractionDigits:0)` via the kit's currency formatting; UK-first. Support metric + imperial where relevant. + +### Safe related-slug pool (all exist; pick 3-5, domain-appropriate) +`tile-calculator`, `paint-calculator`, `flooring-calculator`, `mulch-calculator`, `square-footage-calculator`, `electricity-cost-calculator`, `currency-converter`. + +--- + +## SPECS (5) + +### 1. Concrete Calculator +- Name `ConcreteCalculator`, slug `concrete-calculator`, category "Home", icon `cube`, color `amber`. +- Inputs: shape (Select: 'slab'|'footing'|'column'), length, width, depth (for slab/footing) OR diameter+height (for column — reuse fields: column uses diameter=width, height=depth), unit (ButtonGroup: 'm'|'ft'), wastePct (default 10), bagYield m3/bag (default 0.011 = 25kg bag), optional bagPrice + currency. +- Slab/footing volume = L*W*D (ft->m: *0.3048 each dim). Column = Math.PI*(diameter/2)**2*height. volumeWithWaste = volume*(1+wastePct/100). bags = volume>0 ? Math.ceil(volumeWithWaste / bagYield) : 0. +- PRIMARY ResultCard: bags needed. Secondary MetricCards: volume m3, cubic yards (m3*1.30795), optional cost (bags*bagPrice). +- Invalid state: any dimension <= 0 -> Alert "Enter all dimensions to see how much concrete you need." (result 0, no NaN). FAQ: bag-yield assumption + ready-mix sold by m3 + worked example (4x3x0.1m slab = 1.2m3, +10% = 1.32m3, 120 bags). + +### 2. Gravel Calculator +- Name `GravelCalculator`, slug `gravel-calculator`, category "Home", icon `cube`, color `ocean`. +- Inputs: length, width, depth, unit (ButtonGroup m/ft), density t/m3 (default 1.5; Select common types or numeric), wastePct (default 5), optional pricePerTonne + currency. +- volume = L*W*depth (unit converted). tonnes = volume*density*(1+waste/100). bulkBags (0.85t each) = volume>0 ? Math.ceil(tonnes/0.85) : 0. +- PRIMARY: tonnes needed. Secondary: volume m3, bulk bags, optional cost. Invalid: dims<=0 -> Alert. FAQ: gravel density varies 1.4-1.7 t/m3. + +### 3. Wallpaper Calculator +- Name `WallpaperCalculator`, slug `wallpaper-calculator`, category "Home", icon `layers`, color `violet`. +- Inputs: roomPerimeter (or length+width -> perimeter=2*(L+W)), wallHeight, rollLength (default 10.05 m), rollWidth (default 0.53 m), patternRepeat (default 0), unit (m/ft). +- dropsNeeded = rollWidth>0 ? Math.ceil(perimeter / rollWidth) : 0. effectiveDropLength = wallHeight + patternRepeat. dropsPerRoll = effectiveDropLength>0 ? Math.floor(rollLength / effectiveDropLength) : 0. rolls = dropsPerRoll>0 ? Math.ceil(dropsNeeded / dropsPerRoll) : 0. +- PRIMARY: rolls needed (suggest +1 spare in copy). Secondary: drops needed, drops per roll. Invalid: if dropsPerRoll===0 (pattern repeat + height exceed roll length) -> Alert "Your wall height plus pattern repeat is taller than one roll; choose a longer roll." If perimeter/height<=0 -> Alert prompt. FAQ: explain drops + pattern-repeat waste. Worked example (16m perimeter, 2.4m height, no repeat -> 31 drops, 4 drops/roll, 8 rolls). + +### 4. BTU Calculator (room air-con / heating sizing) +- Name `BTUCalculator`, slug `btu-calculator`, category "Home", icon `fire`, color `coral`. +- Inputs: roomLength, roomWidth, unit (m/ft), ceilingHeight (default 2.4m / 8ft), sunExposure (ButtonGroup: 'shaded'|'normal'|'sunny'), occupants (default 2), kitchen (Toggle). +- areaSqFt = L*W converted to sq ft (m->ft: *10.7639 on area, or convert each dim *3.28084). COOLING_BTU_PER_SQFT = 20. HEATING_BTU_PER_SQFT = 25 (PINNED constant; state in FAQ). base_cooling = areaSqFt*20; base_heating = areaSqFt*25. ceiling adjustment: multiply by (ceilingHeightFt/8). sun: shaded *0.9, sunny *1.1, normal *1.0 (cooling only). occupants: + (max(0, occupants-2))*600. kitchen: +4000 (cooling). +- PRIMARY: recommended cooling BTU. Secondary: heating BTU, cooling kW (BTU/3412), heating kW. Invalid: area<=0 -> Alert. FAQ: rule-of-thumb (20 cooling / 25 heating BTU per sq ft), recommend a pro for exact sizing. Worked example: 12ft x 12ft = 144 sqft x 20 = 2880 base cooling BTU. + +### 5. EV Charging Cost Calculator +- Name `EVChargingCostCalculator`, slug `ev-charging-cost-calculator`, country 'UK', category "Automotive", icon `bolt`, color `green`. +- Inputs: batterySize kWh, currentCharge % (default 20), targetCharge % (default 80), ratePence p/kWh (default 28; Input), chargingEfficiency % (default 90), milesPerKwh (default 3.5). +- energyNeeded = batterySize*(target-current)/100 (only if target>current). energyDrawn = energyNeeded / (chargingEfficiency/100). UNITS: rate is PENCE/kWh -> costGBP = energyDrawn * (ratePence/100). costPerMileGBP = (ratePence/100) / milesPerKwh. fullChargeCostGBP = (batterySize/(efficiency/100)) * (ratePence/100). +- PRIMARY: cost for this charge (GBP, e.g. 60kWh 20->80% at 28p/90% eff = £11.20). Secondary: cost per mile, full-charge cost, energy added kWh. Invalid: targetCharge <= currentCharge -> Alert "Target charge must be higher than current charge." FAQ: home vs public rates, charging losses, rates vary; state the 28p default is illustrative. +- TEST MUST assert the GBP value (11.20), not a 100x pence value. + +--- + +## CENTRAL MERGE (orchestrator, after execute) +Append 5 entries to the `calculators` array in `src/lib/calculators.ts`. FULL entry shape (per CalculatorEntry): `{ title, description, href: '/calculators/[slug]', icon, color, category, country?, mostUsed: false }`. No em dashes, no trailing slashes. icons cube/cube/layers/fire/bolt; colors amber/ocean/violet/coral/green; categories Home/Home/Home/Home/Automotive; EV country 'UK'. Then build + full vitest + lighthouse. + +## VERIFY CHECKLIST (orchestrator) +1. `npm run build` -> 0 errors, 5 new pages in dist. +2. `npx vitest run --no-coverage` -> all pass incl 5 new suites (4+ hand-computed assertions each). +3. Spot-check: concrete 4x3x0.1m +10% = 1.32m3 = 120 bags; gravel 5x3x0.05m=0.75m3*1.5=1.125t; wallpaper 16m/0.53=31 drops, floor(10.05/2.4)=4/roll -> 8 rolls; BTU 144 sqft*20=2880 cooling; EV 60kWh 20->80%=36kWh/0.9=40kWh*0.28=GBP 11.20 (NOT 1120). +4. No emojis / em dashes. NaN-safe inputs. UI-kit pattern (useCalculatorBase). Registry 162 -> 167. +5. Lighthouse on 1-2 new pages: strong perf/seo/best-practices/a11y, 0 console errors. diff --git a/scripts/workflows/build-home-energy-calculators.mjs b/scripts/workflows/build-home-energy-calculators.mjs new file mode 100644 index 0000000..d49d806 --- /dev/null +++ b/scripts/workflows/build-home-energy-calculators.mjs @@ -0,0 +1,67 @@ +export const meta = { + name: 'build-home-energy-calculators', + description: 'Build 5 new Home/Energy calculators using the shared UI kit (useCalculatorBase), per CALC-SPEC-HOME-ENERGY-DATE.md. Conflict-safe: agents create only their own files; registry merge is central.', + phases: [{ title: 'Build' }], +} + +const SPECS = [ + { name: 'Concrete Calculator', Name: 'ConcreteCalculator', slug: 'concrete-calculator', test: 'concrete', section: '1. Concrete Calculator' }, + { name: 'Gravel Calculator', Name: 'GravelCalculator', slug: 'gravel-calculator', test: 'gravel', section: '2. Gravel Calculator' }, + { name: 'Wallpaper Calculator', Name: 'WallpaperCalculator', slug: 'wallpaper-calculator', test: 'wallpaper', section: '3. Wallpaper Calculator' }, + { name: 'BTU Calculator', Name: 'BTUCalculator', slug: 'btu-calculator', test: 'btu', section: '4. BTU Calculator (room air-con / heating sizing)' }, + { name: 'EV Charging Cost Calculator', Name: 'EVChargingCostCalculator', slug: 'ev-charging-cost-calculator', test: 'ev-charging-cost', section: '5. EV Charging Cost Calculator' }, +] + +const BUILT_SCHEMA = { + type: 'object', + additionalProperties: false, + required: ['slug', 'filesCreated', 'logicFn', 'testResult', 'testCases'], + properties: { + slug: { type: 'string' }, + filesCreated: { type: 'array', items: { type: 'string' } }, + logicFn: { type: 'string' }, + testResult: { type: 'string', enum: ['passed', 'failed', 'not-run'] }, + testCases: { type: 'array', items: { type: 'string' } }, + notes: { type: 'string' }, + }, +} + +phase('Build') + +function buildPrompt(s) { + return `You are building ONE calculator for boring-math.com (Astro 5 + Preact + Tailwind, shared design-system UI kit). cwd is the repo root: C:/Users/skf_s/boring-maths. + +CALCULATOR: ${s.name} (component ${s.Name}, slug ${s.slug}) + +STEP 1 - READ FIRST (do not skip): +- CALC-SPEC-HOME-ENERGY-DATE.md -> your section "${s.section}" for the exact formula, inputs, affordances, invalid-state Alerts, and result hierarchy. ALSO read the "CANONICAL LAYOUT - USE THE SHARED UI KIT" section. +- src/components/calculators/TileCalculator/TileCalculator.tsx, types.ts, calculations.ts, index.ts (THE reference pattern - copy its structure: useCalculatorBase + kit components + types.ts + className). +- src/components/calculators/MulchCalculator/MulchCalculator.tsx (second reference, simpler). +- src/components/ui/index.ts (the exact kit component APIs: ThemeProvider, Card, CalculatorHeader, Label, Input, Select, ButtonGroup, Toggle, Grid, Divider, ResultCard, MetricCard, Alert). +- src/hooks/useCalculatorBase.ts (the hook signature + return shape). + +STEP 2 - CREATE EXACTLY THESE NEW FILES (UI-kit pattern, NOT the calc-card pattern). DO NOT modify any existing file. Above all DO NOT edit src/lib/calculators.ts. +1. src/components/calculators/${s.Name}/types.ts (export ${s.Name}Inputs, ${s.Name}Result interfaces, any selector enums, and getDefaultInputs(); include currency: Currency from '../../../lib/regions' if money is shown) +2. src/components/calculators/${s.Name}/calculations.ts (pure calculate${s.Name}(inputs): ${s.Name}Result; NaN-safe: const safe = (v) => Number.isFinite(v) ? Math.max(0, v) : 0; on EVERY numeric input; no JSX) +3. src/components/calculators/${s.Name}/${s.Name}.tsx (default export; const { inputs, result, updateInput } = useCalculatorBase({ name, slug: 'calc-${s.slug}-inputs', defaults: getDefaultInputs, compute: calculate${s.Name} }); build UI from the kit; className NOT class; live update, no submit button; ...; ONE primary ResultCard + secondary MetricCards in a Grid; invalid/degenerate inputs render an with the copy from the spec, never NaN) +4. src/components/calculators/${s.Name}/index.ts (export { default as ${s.Name} } from './${s.Name}'; export * from './calculations'; export * from './types';) +5. src/pages/calculators/${s.slug}.astro (CalculatorLayout + SEOHead + HeroSection + ContentSection + FAQSection; import { ${s.Name} } from '../../components/calculators/${s.Name}'; client:load; 6+ accurate FAQs; 600+ words across "How to use" / "How it is calculated" (formula in words) / "Understanding your results"; related 3-5 from the safe pool in the spec. Use a reference page like src/pages/calculators/tile-calculator.astro for shape.) +6. tests/calculations/${s.test}.test.ts (vitest; import calculate${s.Name} from '../../src/components/calculators/${s.Name}/calculations'; 4+ assertions with hand-computed expected values incl an edge case. This exact location is REQUIRED.) + +HARD RULES (non-negotiable): +- Use the SHARED UI KIT via useCalculatorBase, matching TileCalculator - NOT the calc-card pattern. +- NO emojis. NO em dashes in any frontend string (use commas/colons/hyphens). NO fake stats. +- Math MUST be correct. Pinned constants from the spec: BTU cooling=20/heating=25 BTU per sqft; EV cost is GBP (ratePence/100), the test MUST assert the GBP value not 100x pence. +- QUOTE explicit visible