@jjlmoya/utils-babies 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/package.json +69 -0
  2. package/src/category/i18n/en.ts +48 -0
  3. package/src/category/i18n/es.ts +48 -0
  4. package/src/category/i18n/fr.ts +48 -0
  5. package/src/category/index.ts +24 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +30 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +19 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/seo_length.test.ts +23 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/baby-feeding-calculator/bibliography.astro +7 -0
  23. package/src/tool/baby-feeding-calculator/component.astro +210 -0
  24. package/src/tool/baby-feeding-calculator/i18n/en.ts +162 -0
  25. package/src/tool/baby-feeding-calculator/i18n/es.ts +162 -0
  26. package/src/tool/baby-feeding-calculator/i18n/fr.ts +162 -0
  27. package/src/tool/baby-feeding-calculator/index.ts +47 -0
  28. package/src/tool/baby-feeding-calculator/logic.ts +85 -0
  29. package/src/tool/baby-feeding-calculator/seo.astro +58 -0
  30. package/src/tool/baby-feeding-calculator/style.css +329 -0
  31. package/src/tool/baby-percentile-calculator/bibliography.astro +7 -0
  32. package/src/tool/baby-percentile-calculator/component.astro +388 -0
  33. package/src/tool/baby-percentile-calculator/i18n/en.ts +244 -0
  34. package/src/tool/baby-percentile-calculator/i18n/es.ts +244 -0
  35. package/src/tool/baby-percentile-calculator/i18n/fr.ts +244 -0
  36. package/src/tool/baby-percentile-calculator/index.ts +54 -0
  37. package/src/tool/baby-percentile-calculator/lmsData.ts +80 -0
  38. package/src/tool/baby-percentile-calculator/logic.ts +85 -0
  39. package/src/tool/baby-percentile-calculator/seo.astro +36 -0
  40. package/src/tool/baby-percentile-calculator/style.css +393 -0
  41. package/src/tool/baby-size-converter/bibliography.astro +7 -0
  42. package/src/tool/baby-size-converter/component.astro +289 -0
  43. package/src/tool/baby-size-converter/data.json +11 -0
  44. package/src/tool/baby-size-converter/i18n/en.ts +203 -0
  45. package/src/tool/baby-size-converter/i18n/es.ts +203 -0
  46. package/src/tool/baby-size-converter/i18n/fr.ts +203 -0
  47. package/src/tool/baby-size-converter/index.ts +53 -0
  48. package/src/tool/baby-size-converter/logic.ts +44 -0
  49. package/src/tool/baby-size-converter/seo.astro +36 -0
  50. package/src/tool/baby-size-converter/style.css +394 -0
  51. package/src/tool/fertile-days-estimator/bibliography.astro +7 -0
  52. package/src/tool/fertile-days-estimator/component.astro +265 -0
  53. package/src/tool/fertile-days-estimator/i18n/en.ts +258 -0
  54. package/src/tool/fertile-days-estimator/i18n/es.ts +262 -0
  55. package/src/tool/fertile-days-estimator/i18n/fr.ts +258 -0
  56. package/src/tool/fertile-days-estimator/index.ts +47 -0
  57. package/src/tool/fertile-days-estimator/logic.ts +58 -0
  58. package/src/tool/fertile-days-estimator/seo.astro +36 -0
  59. package/src/tool/fertile-days-estimator/style.css +419 -0
  60. package/src/tool/pregnancy-calculator/bibliography.astro +7 -0
  61. package/src/tool/pregnancy-calculator/calculator.ts +41 -0
  62. package/src/tool/pregnancy-calculator/component.astro +432 -0
  63. package/src/tool/pregnancy-calculator/i18n/en.ts +315 -0
  64. package/src/tool/pregnancy-calculator/i18n/es.ts +319 -0
  65. package/src/tool/pregnancy-calculator/i18n/fr.ts +315 -0
  66. package/src/tool/pregnancy-calculator/index.ts +55 -0
  67. package/src/tool/pregnancy-calculator/milestones.ts +153 -0
  68. package/src/tool/pregnancy-calculator/seo.astro +36 -0
  69. package/src/tool/pregnancy-calculator/store.ts +60 -0
  70. package/src/tool/pregnancy-calculator/style.css +807 -0
  71. package/src/tool/vaccination-calendar/bibliography.astro +7 -0
  72. package/src/tool/vaccination-calendar/component.astro +286 -0
  73. package/src/tool/vaccination-calendar/i18n/en.ts +170 -0
  74. package/src/tool/vaccination-calendar/i18n/es.ts +174 -0
  75. package/src/tool/vaccination-calendar/i18n/fr.ts +170 -0
  76. package/src/tool/vaccination-calendar/index.ts +47 -0
  77. package/src/tool/vaccination-calendar/logic.ts +59 -0
  78. package/src/tool/vaccination-calendar/seo.astro +36 -0
  79. package/src/tool/vaccination-calendar/style.css +316 -0
  80. package/src/tool/vaccination-calendar/vaccinationData.ts +21 -0
  81. package/src/tools.ts +17 -0
  82. package/src/types.ts +72 -0
@@ -0,0 +1,432 @@
1
+ ---
2
+ import './style.css';
3
+ import type { PregnancyCalculatorUI } from './index';
4
+ interface Props { ui: PregnancyCalculatorUI; }
5
+ const { ui } = Astro.props;
6
+ ---
7
+ <div id="pregnancy-calculator-root" class="pregnancy-calculator" data-ui={JSON.stringify(ui)}>
8
+
9
+ <div class="pregnancy-calculator-header">
10
+ <div class="pregnancy-calculator-method-group">
11
+ <button class="pregnancy-calculator-method-btn" data-method="fur">{ui.btnFUR}</button>
12
+ <button class="pregnancy-calculator-method-btn" data-method="conception">{ui.btnConception}</button>
13
+ </div>
14
+ <label class="pregnancy-calculator-partner-wrap">
15
+ <span class="pregnancy-calculator-partner-label">{ui.labelPartnerMode}</span>
16
+ <div class="pregnancy-calculator-toggle-track">
17
+ <div class="pregnancy-calculator-toggle-thumb"></div>
18
+ </div>
19
+ </label>
20
+ </div>
21
+
22
+ <div class="pregnancy-calculator-main">
23
+
24
+ <div class="pregnancy-calculator-left">
25
+ <div class="pregnancy-calculator-dp-wrap">
26
+ <span class="pregnancy-calculator-dp-label" id="pc-dp-label">{ui.labelFUR}</span>
27
+ <div class="pregnancy-calculator-dp-selects">
28
+ <select id="pc-dp-day" class="pregnancy-calculator-dp-select" aria-label="Día">
29
+ <option value="">Día</option>
30
+ </select>
31
+ <span class="pregnancy-calculator-dp-sep">/</span>
32
+ <select id="pc-dp-month" class="pregnancy-calculator-dp-select" aria-label="Mes">
33
+ <option value="">Mes</option>
34
+ </select>
35
+ <span class="pregnancy-calculator-dp-sep">/</span>
36
+ <select id="pc-dp-year" class="pregnancy-calculator-dp-select" aria-label="Año">
37
+ <option value="">Año</option>
38
+ </select>
39
+ </div>
40
+ </div>
41
+
42
+ <div class="pregnancy-calculator-cs-wrap" id="pc-cs-wrap">
43
+ <div class="pregnancy-calculator-cs-header">
44
+ <span class="pregnancy-calculator-cs-label">{ui.labelCycleLength}</span>
45
+ <span class="pregnancy-calculator-cs-value-wrap">
46
+ <span id="pc-cs-value">28</span>
47
+ <span class="pregnancy-calculator-cs-unit">{ui.unitDays}</span>
48
+ </span>
49
+ </div>
50
+ <input
51
+ type="range"
52
+ id="pc-cs-slider"
53
+ min="21"
54
+ max="45"
55
+ value="28"
56
+ class="pregnancy-calculator-cs-slider"
57
+ aria-label="Duración del ciclo menstrual en días"
58
+ />
59
+ <div class="pregnancy-calculator-cs-extremes">
60
+ <span>21 {ui.unitDays}</span>
61
+ <span>45 {ui.unitDays}</span>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="pregnancy-calculator-sd-wrap">
66
+ <div class="pregnancy-calculator-stats-row">
67
+ <div class="pregnancy-calculator-stat-chip">
68
+ <span class="pregnancy-calculator-stat-key">{ui.labelWeeks}</span>
69
+ <span class="pregnancy-calculator-stat-value pregnancy-calculator-accent" id="pc-sd-weeks">—</span>
70
+ </div>
71
+ <div class="pregnancy-calculator-stat-chip">
72
+ <span class="pregnancy-calculator-stat-key">{ui.labelTrimester}</span>
73
+ <span class="pregnancy-calculator-stat-value" id="pc-sd-tri">—</span>
74
+ </div>
75
+ </div>
76
+ <div class="pregnancy-calculator-edd-box">
77
+ <div class="pregnancy-calculator-edd-label">{ui.labelEDD}</div>
78
+ <div class="pregnancy-calculator-edd-date" id="pc-sd-edd">{ui.eddPlaceholder}</div>
79
+ <div class="pregnancy-calculator-edd-note">{ui.eddNote}</div>
80
+ </div>
81
+ <button class="pregnancy-calculator-btn-cal" id="pc-sd-btn-cal" disabled>{ui.btnCalendar}</button>
82
+ </div>
83
+ </div>
84
+
85
+ <div class="pregnancy-calculator-center">
86
+ <div class="pregnancy-calculator-mp-empty" id="pc-mp-empty">
87
+ <div class="pregnancy-calculator-mp-empty-dot"></div>
88
+ <div class="pregnancy-calculator-mp-empty-title">{ui.labelStartHere}</div>
89
+ <p class="pregnancy-calculator-mp-empty-body">{ui.labelStartBody}</p>
90
+ </div>
91
+
92
+ <div class="pregnancy-calculator-mp-results" id="pc-mp-results" style="display:none">
93
+ <div class="pregnancy-calculator-mp-top-row">
94
+ <span class="pregnancy-calculator-mp-badge" id="pc-mp-badge">Semana —</span>
95
+ <div class="pregnancy-calculator-analogy-tabs">
96
+ <button class="pregnancy-calculator-at-btn" data-cat="fruits">{ui.labelFruits}</button>
97
+ <button class="pregnancy-calculator-at-btn" data-cat="geek">{ui.labelGeek}</button>
98
+ <button class="pregnancy-calculator-at-btn" data-cat="sweets">{ui.labelSweets}</button>
99
+ </div>
100
+ </div>
101
+ <div class="pregnancy-calculator-size-card">
102
+ <span class="pregnancy-calculator-size-name" id="pc-mp-analogy">—</span>
103
+ <span class="pregnancy-calculator-size-measure" id="pc-mp-size">—</span>
104
+ </div>
105
+ <div class="pregnancy-calculator-info-stack">
106
+ <div class="pregnancy-calculator-info-block">
107
+ <div class="pregnancy-calculator-info-key">{ui.labelBioLabel}</div>
108
+ <p class="pregnancy-calculator-info-text" id="pc-mp-bio"></p>
109
+ </div>
110
+ <div class="pregnancy-calculator-info-block">
111
+ <div class="pregnancy-calculator-info-key" id="pc-mp-mom-key">{ui.labelMomKey}</div>
112
+ <p class="pregnancy-calculator-info-text" id="pc-mp-mom"></p>
113
+ </div>
114
+ </div>
115
+ <div class="pregnancy-calculator-wonder-line" id="pc-mp-wonder"></div>
116
+ <div class="pregnancy-calculator-semaphore">
117
+ <div class="pregnancy-calculator-sema pregnancy-calculator-sema-safe">
118
+ <div class="pregnancy-calculator-sema-title">{ui.labelNormalMolestias}</div>
119
+ <ul class="pregnancy-calculator-sema-list" id="pc-mp-safe"></ul>
120
+ </div>
121
+ <div class="pregnancy-calculator-sema pregnancy-calculator-sema-alert">
122
+ <div class="pregnancy-calculator-sema-title">{ui.labelAlert}</div>
123
+ <ul class="pregnancy-calculator-sema-list" id="pc-mp-alert"></ul>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <div class="pregnancy-calculator-right">
130
+ <div class="pregnancy-calculator-tl-header">{ui.labelTimeline}</div>
131
+ <div class="pregnancy-calculator-tl-scroll" id="pc-tl-scroll"></div>
132
+ </div>
133
+
134
+ </div>
135
+ </div>
136
+
137
+ <script>
138
+ import { getState, setState, subscribe } from './store';
139
+ import { getMilestone, timelineLabels } from './milestones';
140
+ import type { PCState } from './store';
141
+ import type { CalcResult } from './calculator';
142
+
143
+ const root = document.getElementById('pregnancy-calculator-root') as HTMLElement;
144
+ const ui = JSON.parse(root.dataset.ui as string) as Record<string, string>;
145
+
146
+ const dpLabel = root.querySelector('#pc-dp-label') as HTMLElement;
147
+ const dpDay = root.querySelector('#pc-dp-day') as HTMLSelectElement;
148
+ const dpMonth = root.querySelector('#pc-dp-month') as HTMLSelectElement;
149
+ const dpYear = root.querySelector('#pc-dp-year') as HTMLSelectElement;
150
+ const csWrap = root.querySelector('#pc-cs-wrap') as HTMLElement;
151
+ const csSlider = root.querySelector('#pc-cs-slider') as HTMLInputElement;
152
+ const csValue = root.querySelector('#pc-cs-value') as HTMLElement;
153
+ const sdWeeks = root.querySelector('#pc-sd-weeks') as HTMLElement;
154
+ const sdTri = root.querySelector('#pc-sd-tri') as HTMLElement;
155
+ const sdEdd = root.querySelector('#pc-sd-edd') as HTMLElement;
156
+ const sdBtnCal = root.querySelector('#pc-sd-btn-cal') as HTMLButtonElement;
157
+ const mpEmpty = root.querySelector('#pc-mp-empty') as HTMLElement;
158
+ const mpResults = root.querySelector('#pc-mp-results') as HTMLElement;
159
+ const mpBadge = root.querySelector('#pc-mp-badge') as HTMLElement;
160
+ const mpAnalogy = root.querySelector('#pc-mp-analogy') as HTMLElement;
161
+ const mpSize = root.querySelector('#pc-mp-size') as HTMLElement;
162
+ const mpBio = root.querySelector('#pc-mp-bio') as HTMLElement;
163
+ const mpMomKey = root.querySelector('#pc-mp-mom-key') as HTMLElement;
164
+ const mpMom = root.querySelector('#pc-mp-mom') as HTMLElement;
165
+ const mpWonder = root.querySelector('#pc-mp-wonder') as HTMLElement;
166
+ const mpSafe = root.querySelector('#pc-mp-safe') as HTMLElement;
167
+ const mpAlert = root.querySelector('#pc-mp-alert') as HTMLElement;
168
+ const tlScroll = root.querySelector('#pc-tl-scroll') as HTMLElement;
169
+
170
+ const MONTHS_ES = ['Ene','Feb','Mar','Abr','May','Jun','Jul','Ago','Sep','Oct','Nov','Dic'];
171
+ const fmtEdd = new Intl.DateTimeFormat('es-ES', { day: 'numeric', month: 'long', year: 'numeric' });
172
+
173
+ function buildDateDropdowns(): void {
174
+ for (let d = 1; d <= 31; d++) {
175
+ const o = document.createElement('option');
176
+ o.value = String(d);
177
+ o.textContent = String(d).padStart(2, '0');
178
+ dpDay.appendChild(o);
179
+ }
180
+ MONTHS_ES.forEach((m, i) => {
181
+ const o = document.createElement('option');
182
+ o.value = String(i + 1);
183
+ o.textContent = m;
184
+ dpMonth.appendChild(o);
185
+ });
186
+ const now = new Date();
187
+ for (let y = now.getFullYear(); y >= now.getFullYear() - 1; y--) {
188
+ const o = document.createElement('option');
189
+ o.value = String(y);
190
+ o.textContent = String(y);
191
+ dpYear.appendChild(o);
192
+ }
193
+ }
194
+
195
+ function buildTimeline(): void {
196
+ const frag = document.createDocumentFragment();
197
+ for (let w = 4; w <= 40; w++) {
198
+ const row = document.createElement('div');
199
+ row.className = 'pregnancy-calculator-tl-row pregnancy-calculator-tl-row-future';
200
+ row.dataset.week = String(w);
201
+ const lineCol = document.createElement('div');
202
+ lineCol.className = 'pregnancy-calculator-tl-line-col';
203
+ const dot = document.createElement('div');
204
+ dot.className = 'pregnancy-calculator-tl-dot';
205
+ const line = document.createElement('div');
206
+ line.className = 'pregnancy-calculator-tl-line';
207
+ lineCol.appendChild(dot);
208
+ lineCol.appendChild(line);
209
+ const info = document.createElement('div');
210
+ info.className = 'pregnancy-calculator-tl-info';
211
+ const num = document.createElement('span');
212
+ num.className = 'pregnancy-calculator-tl-num';
213
+ num.textContent = String(w);
214
+ const label = document.createElement('span');
215
+ label.className = 'pregnancy-calculator-tl-label';
216
+ label.textContent = timelineLabels[w] ?? '';
217
+ info.appendChild(num);
218
+ info.appendChild(label);
219
+ row.appendChild(lineCol);
220
+ row.appendChild(info);
221
+ frag.appendChild(row);
222
+ }
223
+ tlScroll.appendChild(frag);
224
+ }
225
+
226
+ function getDateString(d: string, m: string, y: string): string {
227
+ if (!d || !m || !y) return '';
228
+ const mm = m.padStart(2, '0');
229
+ const dd = d.padStart(2, '0');
230
+ return `${y}-${mm}-${dd}`;
231
+ }
232
+
233
+ function initFromState(): void {
234
+ const s = getState();
235
+ csSlider.value = String(s.cycle);
236
+ csValue.textContent = String(s.cycle);
237
+ updateMethodUI(s.method);
238
+ updatePartnerUI(s.partner);
239
+ updateAnalogyTabs(s.analogyCat);
240
+ csWrap.style.display = s.method === 'fur' ? '' : 'none';
241
+ if (s.date) {
242
+ const parts = s.date.split('-');
243
+ if (parts.length === 3) {
244
+ dpYear.value = parts[0] ?? '';
245
+ dpMonth.value = String(parseInt(parts[1] ?? '0', 10));
246
+ dpDay.value = String(parseInt(parts[2] ?? '0', 10));
247
+ }
248
+ }
249
+ }
250
+
251
+ function updateMethodUI(method: string): void {
252
+ root.querySelectorAll('.pregnancy-calculator-method-btn').forEach((btn) => {
253
+ const el = btn as HTMLElement;
254
+ el.classList.toggle('pregnancy-calculator-method-btn-active', el.dataset.method === method);
255
+ });
256
+ dpLabel.textContent = (method === 'fur' ? ui['labelFUR'] : ui['labelConception']) ?? null;
257
+ csWrap.style.display = method === 'fur' ? '' : 'none';
258
+ }
259
+
260
+ function updatePartnerUI(partner: boolean): void {
261
+ const track = root.querySelector('.pregnancy-calculator-toggle-track') as HTMLElement;
262
+ track.classList.toggle('pregnancy-calculator-toggle-track-on', partner);
263
+ mpMomKey.textContent = (partner ? ui['labelPartnerKey'] : ui['labelMomKey']) ?? null;
264
+ }
265
+
266
+ function updateAnalogyTabs(cat: string): void {
267
+ root.querySelectorAll('.pregnancy-calculator-at-btn').forEach((btn) => {
268
+ const el = btn as HTMLElement;
269
+ el.classList.toggle('pregnancy-calculator-at-btn-active', el.dataset.cat === cat);
270
+ });
271
+ }
272
+
273
+ function applyTrimester(tri: 1 | 2 | 3): void {
274
+ root.classList.remove(
275
+ 'pregnancy-calculator-t1',
276
+ 'pregnancy-calculator-t2',
277
+ 'pregnancy-calculator-t3'
278
+ );
279
+ root.classList.add(`pregnancy-calculator-t${tri}`);
280
+ }
281
+
282
+ function renderStats(result: CalcResult): void {
283
+ if (!result.valid) {
284
+ sdWeeks.textContent = '—';
285
+ sdTri.textContent = '—';
286
+ sdEdd.textContent = ui['eddPlaceholder'] ?? null;
287
+ sdBtnCal.disabled = true;
288
+ return;
289
+ }
290
+ sdWeeks.textContent = `${result.weeks}+${result.days}`;
291
+ sdTri.textContent = String(result.trimester);
292
+ sdEdd.textContent = fmtEdd.format(result.edd);
293
+ sdBtnCal.disabled = false;
294
+ applyTrimester(result.trimester);
295
+ }
296
+
297
+ function buildList(ul: HTMLElement, items: string[]): void {
298
+ ul.innerHTML = '';
299
+ items.forEach((item) => {
300
+ const li = document.createElement('li');
301
+ li.textContent = item;
302
+ ul.appendChild(li);
303
+ });
304
+ }
305
+
306
+ function renderMilestone(weeks: number, cat: string, partner: boolean): void {
307
+ const ms = getMilestone(weeks);
308
+ mpBadge.textContent = `Semana ${weeks}`;
309
+ mpAnalogy.textContent = ms.analogies[cat as 'fruits' | 'geek' | 'sweets'] ?? ms.analogies.fruits;
310
+ mpSize.textContent = ms.size;
311
+ mpBio.textContent = ms.biolook;
312
+ mpMom.textContent = partner ? ms.partner : ms.mom;
313
+ mpWonder.textContent = ms.wonder;
314
+ buildList(mpSafe, ms.symptoms);
315
+ buildList(mpAlert, ms.alerts);
316
+ }
317
+
318
+ function renderTimeline(weeks: number): void {
319
+ tlScroll.querySelectorAll('.pregnancy-calculator-tl-row').forEach((row) => {
320
+ const el = row as HTMLElement;
321
+ const w = parseInt(el.dataset.week ?? '0', 10);
322
+ el.classList.remove(
323
+ 'pregnancy-calculator-tl-row-past',
324
+ 'pregnancy-calculator-tl-row-current',
325
+ 'pregnancy-calculator-tl-row-future'
326
+ );
327
+ if (w < weeks) {
328
+ el.classList.add('pregnancy-calculator-tl-row-past');
329
+ } else if (w === weeks) {
330
+ el.classList.add('pregnancy-calculator-tl-row-current');
331
+ } else {
332
+ el.classList.add('pregnancy-calculator-tl-row-future');
333
+ }
334
+ });
335
+ const currentRow = tlScroll.querySelector('.pregnancy-calculator-tl-row-current') as HTMLElement | null;
336
+ if (currentRow) {
337
+ currentRow.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
338
+ }
339
+ }
340
+
341
+ function onStateChange(s: Readonly<PCState>): void {
342
+ updateMethodUI(s.method);
343
+ updatePartnerUI(s.partner);
344
+ updateAnalogyTabs(s.analogyCat);
345
+ if (!s.result || !s.result.valid) {
346
+ mpEmpty.style.display = '';
347
+ mpResults.style.display = 'none';
348
+ if (s.result) renderStats(s.result);
349
+ return;
350
+ }
351
+ renderStats(s.result);
352
+ renderMilestone(s.result.weeks, s.analogyCat, s.partner);
353
+ renderTimeline(s.result.weeks);
354
+ mpEmpty.style.display = 'none';
355
+ mpResults.style.display = '';
356
+ mpResults.classList.add('pregnancy-calculator-mp-enter');
357
+ }
358
+
359
+ function onDateChange(): void {
360
+ const dateStr = getDateString(dpDay.value, dpMonth.value, dpYear.value);
361
+ if (dateStr) setState({ date: dateStr });
362
+ }
363
+
364
+ function downloadICS(edd: Date): void {
365
+ const pad = (n: number) => String(n).padStart(2, '0');
366
+ const y = edd.getFullYear();
367
+ const m = pad(edd.getMonth() + 1);
368
+ const d = pad(edd.getDate());
369
+ const stamp = `${y}${m}${d}T000000Z`;
370
+ const lines = [
371
+ 'BEGIN:VCALENDAR',
372
+ 'VERSION:2.0',
373
+ 'BEGIN:VEVENT',
374
+ `DTSTART;VALUE=DATE:${y}${m}${d}`,
375
+ `DTEND;VALUE=DATE:${y}${m}${d}`,
376
+ `DTSTAMP:${stamp}`,
377
+ 'SUMMARY:Fecha Probable de Parto',
378
+ 'DESCRIPTION:Fecha estimada de parto calculada por jjlmoya',
379
+ 'END:VEVENT',
380
+ 'END:VCALENDAR',
381
+ ];
382
+ const blob = new Blob([lines.join('\r\n')], { type: 'text/calendar' });
383
+ const url = URL.createObjectURL(blob);
384
+ const a = document.createElement('a');
385
+ a.href = url;
386
+ a.download = 'fecha-parto.ics';
387
+ a.click();
388
+ URL.revokeObjectURL(url);
389
+ }
390
+
391
+ buildDateDropdowns();
392
+ buildTimeline();
393
+ initFromState();
394
+
395
+ const initialState = getState();
396
+ if (initialState.result) onStateChange(initialState);
397
+
398
+ dpDay.addEventListener('change', onDateChange);
399
+ dpMonth.addEventListener('change', onDateChange);
400
+ dpYear.addEventListener('change', onDateChange);
401
+
402
+ csSlider.addEventListener('input', () => {
403
+ const v = Number(csSlider.value);
404
+ csValue.textContent = String(v);
405
+ setState({ cycle: v });
406
+ });
407
+
408
+ root.querySelectorAll('.pregnancy-calculator-method-btn').forEach((btn) => {
409
+ btn.addEventListener('click', () => {
410
+ const method = (btn as HTMLElement).dataset.method as 'fur' | 'conception';
411
+ setState({ method });
412
+ });
413
+ });
414
+
415
+ root.querySelector('.pregnancy-calculator-partner-wrap')?.addEventListener('click', () => {
416
+ setState({ partner: !getState().partner });
417
+ });
418
+
419
+ root.querySelectorAll('.pregnancy-calculator-at-btn').forEach((btn) => {
420
+ btn.addEventListener('click', () => {
421
+ const cat = (btn as HTMLElement).dataset.cat as 'fruits' | 'geek' | 'sweets';
422
+ setState({ analogyCat: cat });
423
+ });
424
+ });
425
+
426
+ sdBtnCal.addEventListener('click', () => {
427
+ const s = getState();
428
+ if (s.result?.valid) downloadICS(s.result.edd);
429
+ });
430
+
431
+ subscribe(onStateChange);
432
+ </script>