Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions CALC-SPEC-HOME-ENERGY-DATE.md
Original file line number Diff line number Diff line change
@@ -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 `<ThemeProvider defaultColor="<color>"><Card variant="elevated">...`.
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.
67 changes: 67 additions & 0 deletions scripts/workflows/build-home-energy-calculators.mjs
Original file line number Diff line number Diff line change
@@ -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; <ThemeProvider defaultColor="..."><Card variant="elevated">...; ONE primary ResultCard + secondary MetricCards in a Grid; invalid/degenerate inputs render an <Alert variant="warning"> 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 <Label> strings for every control. Gravel density = a Select of named gravel types mapping to t/m3. Concrete: when shape==='column' the dimension labels must read 'Diameter' and 'Height'.
- Match existing style; do not invent new design tokens.

STEP 3 - VERIFY: run npx vitest run tests/calculations/${s.test}.test.ts --no-coverage. If it fails, fix calculations.ts (never the test) until it passes. Report pass/fail.

Return the structured result (filesCreated repo-relative, logicFn name, testResult, 4+ testCases as 'inputs -> expected', notes).`
}

const built = await parallel(SPECS.map((s) => () => agent(buildPrompt(s), { label: `build:${s.slug}`, phase: 'Build', schema: BUILT_SCHEMA })))
const ok = built.filter(Boolean)
log(`Built ${ok.length}/${SPECS.length} calculators`)
for (const b of ok) log(` ${b.slug}: ${b.testResult} (${(b.filesCreated || []).length} files)`)
return { built: ok, specs: SPECS }
Loading
Loading