Skip to content

Commit d0e5312

Browse files
kitfunsoclaude
andcommitted
Add 3 new high-traffic calculators
New calculators: - Pregnancy Due Date Calculator: LMP, conception, IVF, and ultrasound methods with milestone tracking and trimester progress - Sleep Calculator: Optimal bed/wake times based on 90-min sleep cycles, age-adjusted recommendations, and nap calculator - EV vs Gas Cost Calculator: Comprehensive cost comparison with fuel, maintenance, insurance, break-even analysis, and CO2 savings All calculators include: - SEO-optimized pages with FAQs and content sections - Share and print functionality - LocalStorage persistence - Mobile-responsive design - Multi-currency support (where applicable) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5ac532a commit d0e5312

13 files changed

Lines changed: 2651 additions & 0 deletions

File tree

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
/**
2+
* Due Date Calculator - Preact Component
3+
*
4+
* Calculate pregnancy due date using multiple methods.
5+
*/
6+
7+
import { useMemo } from 'preact/hooks';
8+
import { useLocalStorage } from '../../../hooks/useLocalStorage';
9+
import { calculateDueDate } from './calculations';
10+
import { getDefaultInputs, type DueDateInputs, type CalculationMethod } from './types';
11+
import {
12+
ThemeProvider,
13+
Card,
14+
CalculatorHeader,
15+
Label,
16+
Input,
17+
ButtonGroup,
18+
Grid,
19+
Divider,
20+
Alert,
21+
} from '../../ui';
22+
import ShareResults from '../../ui/ShareResults';
23+
import PrintResults from '../../ui/PrintResults';
24+
import { useCalculatorTracking } from '../../../hooks/useCalculatorTracking';
25+
26+
export default function DueDateCalculator() {
27+
// Track calculator usage for analytics
28+
useCalculatorTracking('Due Date Calculator');
29+
30+
const [inputs, setInputs] = useLocalStorage<DueDateInputs>(
31+
'calc-due-date-inputs',
32+
getDefaultInputs
33+
);
34+
35+
const result = useMemo(() => {
36+
return calculateDueDate(inputs);
37+
}, [inputs]);
38+
39+
const updateInput = <K extends keyof DueDateInputs>(field: K, value: DueDateInputs[K]) => {
40+
setInputs((prev) => ({ ...prev, [field]: value }));
41+
};
42+
43+
const methodOptions = [
44+
{ value: 'lmp' as const, label: 'Last Period' },
45+
{ value: 'conception' as const, label: 'Conception' },
46+
{ value: 'ivf' as const, label: 'IVF Transfer' },
47+
{ value: 'ultrasound' as const, label: 'Ultrasound' },
48+
];
49+
50+
const ivfDayOptions = [
51+
{ value: '3day' as const, label: '3-Day Embryo' },
52+
{ value: '5day' as const, label: '5-Day Embryo' },
53+
];
54+
55+
const getTrimesterColor = (trimester: 1 | 2 | 3) => {
56+
switch (trimester) {
57+
case 1:
58+
return 'bg-pink-950/50 border-pink-500/30 text-pink-400';
59+
case 2:
60+
return 'bg-coral-950/50 border-coral-500/30 text-coral-400';
61+
case 3:
62+
return 'bg-orange-950/50 border-orange-500/30 text-orange-400';
63+
}
64+
};
65+
66+
return (
67+
<ThemeProvider defaultColor="coral">
68+
<Card variant="elevated">
69+
<CalculatorHeader
70+
title="Pregnancy Due Date Calculator"
71+
subtitle="Estimate your baby's arrival date"
72+
/>
73+
74+
<div className="p-6 md:p-8">
75+
<div className="space-y-6 mb-8">
76+
{/* Calculation Method */}
77+
<div>
78+
<Label>Calculation Method</Label>
79+
<ButtonGroup
80+
options={methodOptions}
81+
value={inputs.method}
82+
onChange={(value) => updateInput('method', value as CalculationMethod)}
83+
columns={2}
84+
/>
85+
</div>
86+
87+
{/* LMP Method */}
88+
{inputs.method === 'lmp' && (
89+
<>
90+
<div>
91+
<Label htmlFor="lmpDate" required>
92+
First Day of Last Menstrual Period
93+
</Label>
94+
<Input
95+
id="lmpDate"
96+
type="date"
97+
value={inputs.lmpDate}
98+
onChange={(e) => updateInput('lmpDate', (e.target as HTMLInputElement).value)}
99+
/>
100+
</div>
101+
<div>
102+
<Label htmlFor="cycleLength">Average Cycle Length (days)</Label>
103+
<Input
104+
id="cycleLength"
105+
type="number"
106+
min={21}
107+
max={45}
108+
value={inputs.cycleLength}
109+
onChange={(e) =>
110+
updateInput('cycleLength', Number((e.target as HTMLInputElement).value))
111+
}
112+
/>
113+
<p className="text-xs text-[var(--color-muted)] mt-1">
114+
Default is 28 days. Adjust if your cycle is longer or shorter.
115+
</p>
116+
</div>
117+
</>
118+
)}
119+
120+
{/* Conception Date Method */}
121+
{inputs.method === 'conception' && (
122+
<div>
123+
<Label htmlFor="conceptionDate" required>
124+
Conception Date
125+
</Label>
126+
<Input
127+
id="conceptionDate"
128+
type="date"
129+
value={inputs.conceptionDate}
130+
onChange={(e) =>
131+
updateInput('conceptionDate', (e.target as HTMLInputElement).value)
132+
}
133+
/>
134+
<p className="text-xs text-[var(--color-muted)] mt-1">
135+
The date you believe conception occurred.
136+
</p>
137+
</div>
138+
)}
139+
140+
{/* IVF Method */}
141+
{inputs.method === 'ivf' && (
142+
<>
143+
<div>
144+
<Label htmlFor="ivfDate" required>
145+
IVF Transfer Date
146+
</Label>
147+
<Input
148+
id="ivfDate"
149+
type="date"
150+
value={inputs.ivfDate}
151+
onChange={(e) => updateInput('ivfDate', (e.target as HTMLInputElement).value)}
152+
/>
153+
</div>
154+
<div>
155+
<Label>Embryo Type</Label>
156+
<ButtonGroup
157+
options={ivfDayOptions}
158+
value={inputs.ivfDayType}
159+
onChange={(value) => updateInput('ivfDayType', value as '3day' | '5day')}
160+
columns={2}
161+
/>
162+
</div>
163+
</>
164+
)}
165+
166+
{/* Ultrasound Method */}
167+
{inputs.method === 'ultrasound' && (
168+
<>
169+
<div>
170+
<Label htmlFor="ultrasoundDate" required>
171+
Ultrasound Date
172+
</Label>
173+
<Input
174+
id="ultrasoundDate"
175+
type="date"
176+
value={inputs.ultrasoundDate}
177+
onChange={(e) =>
178+
updateInput('ultrasoundDate', (e.target as HTMLInputElement).value)
179+
}
180+
/>
181+
</div>
182+
<Grid responsive={{ sm: 1, md: 2 }} gap="md">
183+
<div>
184+
<Label htmlFor="ultrasoundWeeks" required>
185+
Gestational Age (Weeks)
186+
</Label>
187+
<Input
188+
id="ultrasoundWeeks"
189+
type="number"
190+
min={4}
191+
max={42}
192+
value={inputs.ultrasoundWeeks}
193+
onChange={(e) =>
194+
updateInput('ultrasoundWeeks', Number((e.target as HTMLInputElement).value))
195+
}
196+
/>
197+
</div>
198+
<div>
199+
<Label htmlFor="ultrasoundDays">Days</Label>
200+
<Input
201+
id="ultrasoundDays"
202+
type="number"
203+
min={0}
204+
max={6}
205+
value={inputs.ultrasoundDays}
206+
onChange={(e) =>
207+
updateInput('ultrasoundDays', Number((e.target as HTMLInputElement).value))
208+
}
209+
/>
210+
</div>
211+
</Grid>
212+
<p className="text-xs text-[var(--color-muted)]">
213+
Enter the gestational age shown on your ultrasound report.
214+
</p>
215+
</>
216+
)}
217+
</div>
218+
219+
<Divider />
220+
221+
{/* Results */}
222+
<div className="space-y-6">
223+
{/* Due Date Display */}
224+
<div className="rounded-2xl p-8 text-center border-2 bg-coral-950/50 border-coral-500/30">
225+
<p className="text-sm text-[var(--color-muted)] uppercase tracking-wide mb-2">
226+
Estimated Due Date
227+
</p>
228+
<p className="text-3xl md:text-4xl font-bold text-coral-400 mb-2">
229+
{result.dueDateFormatted}
230+
</p>
231+
<p className="text-lg text-[var(--color-cream)]">
232+
{result.daysUntilDue > 0
233+
? `${result.daysUntilDue} days to go`
234+
: result.daysUntilDue === 0
235+
? 'Due today!'
236+
: `${Math.abs(result.daysUntilDue)} days past due date`}
237+
</p>
238+
</div>
239+
240+
{/* Current Progress */}
241+
<div className={`rounded-xl p-6 border-2 ${getTrimesterColor(result.trimester)}`}>
242+
<div className="flex justify-between items-center mb-4">
243+
<div>
244+
<p className="text-2xl font-bold">
245+
{result.currentWeeks} weeks, {result.currentDays} days
246+
</p>
247+
<p className="text-sm opacity-75">{result.trimesterName}</p>
248+
</div>
249+
<div className="text-right">
250+
<p className="text-3xl font-bold">{result.percentComplete}%</p>
251+
<p className="text-sm opacity-75">Complete</p>
252+
</div>
253+
</div>
254+
255+
{/* Progress Bar */}
256+
<div className="w-full bg-[var(--color-night)] rounded-full h-4 overflow-hidden">
257+
<div
258+
className="h-full bg-gradient-to-r from-pink-500 via-coral-500 to-orange-500 rounded-full transition-all duration-500"
259+
style={{ width: `${result.percentComplete}%` }}
260+
/>
261+
</div>
262+
<div className="flex justify-between text-xs mt-2 opacity-75">
263+
<span>Week 0</span>
264+
<span>Week 13</span>
265+
<span>Week 27</span>
266+
<span>Week 40</span>
267+
</div>
268+
</div>
269+
270+
{/* Key Dates */}
271+
<Grid responsive={{ sm: 1, md: 2 }} gap="md">
272+
<div className="bg-[var(--color-night)] rounded-xl p-5 border border-white/10 text-center">
273+
<p className="text-sm text-[var(--color-muted)] uppercase tracking-wide mb-1">
274+
Estimated Conception
275+
</p>
276+
<p className="text-lg font-bold text-[var(--color-cream)]">
277+
{result.conceptionDateEstimate}
278+
</p>
279+
</div>
280+
<div className="bg-[var(--color-night)] rounded-xl p-5 border border-white/10 text-center">
281+
<p className="text-sm text-[var(--color-muted)] uppercase tracking-wide mb-1">
282+
Days Pregnant
283+
</p>
284+
<p className="text-lg font-bold text-[var(--color-cream)] tabular-nums">
285+
{result.daysPregnant} days
286+
</p>
287+
</div>
288+
</Grid>
289+
290+
{/* Milestones Timeline */}
291+
<div className="bg-[var(--color-night)] rounded-xl p-6 border border-white/10">
292+
<h3 className="text-sm font-semibold text-[var(--color-cream)] uppercase tracking-wider mb-4">
293+
Pregnancy Milestones
294+
</h3>
295+
<div className="space-y-3 max-h-80 overflow-y-auto">
296+
{result.milestones.map((milestone) => (
297+
<div
298+
key={milestone.week}
299+
className={`flex items-center gap-4 p-3 rounded-lg transition-opacity ${
300+
milestone.isPast ? 'opacity-50' : 'bg-white/5'
301+
}`}
302+
>
303+
<div
304+
className={`w-3 h-3 rounded-full flex-shrink-0 ${
305+
milestone.isPast ? 'bg-green-500' : 'bg-coral-500'
306+
}`}
307+
/>
308+
<div className="flex-1 min-w-0">
309+
<div className="flex justify-between items-center">
310+
<span className="font-medium text-[var(--color-cream)]">
311+
Week {milestone.week}: {milestone.name}
312+
</span>
313+
<span className="text-sm text-[var(--color-muted)] ml-2">
314+
{milestone.date}
315+
</span>
316+
</div>
317+
<p className="text-xs text-[var(--color-muted)]">{milestone.description}</p>
318+
</div>
319+
{milestone.isPast && (
320+
<span className="text-xs text-green-400 flex-shrink-0">Passed</span>
321+
)}
322+
</div>
323+
))}
324+
</div>
325+
</div>
326+
327+
<Alert variant="info" title="Note:">
328+
Due dates are estimates. Only about 5% of babies are born on their exact due date.
329+
Most babies are born within 2 weeks before or after. Always consult your healthcare
330+
provider for personalized guidance.
331+
</Alert>
332+
333+
{/* Share & Print Results */}
334+
<div className="flex justify-center gap-3 pt-4">
335+
<ShareResults
336+
result={`My due date: ${result.dueDateFormatted} - Currently ${result.currentWeeks} weeks, ${result.currentDays} days (${result.trimesterName})`}
337+
calculatorName="Pregnancy Due Date Calculator"
338+
/>
339+
<PrintResults
340+
title="Pregnancy Due Date Calculator Results"
341+
results={[
342+
{ label: 'Due Date', value: result.dueDateFormatted },
343+
{
344+
label: 'Current Progress',
345+
value: `${result.currentWeeks} weeks, ${result.currentDays} days`,
346+
},
347+
{ label: 'Trimester', value: result.trimesterName },
348+
{ label: 'Days Until Due', value: result.daysUntilDue.toString() },
349+
{ label: 'Percent Complete', value: `${result.percentComplete}%` },
350+
{ label: 'Estimated Conception', value: result.conceptionDateEstimate },
351+
]}
352+
/>
353+
</div>
354+
</div>
355+
</div>
356+
</Card>
357+
</ThemeProvider>
358+
);
359+
}

0 commit comments

Comments
 (0)