@jjlmoya/utils-babies 1.2.0 → 1.4.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.
- package/package.json +2 -2
- package/src/tool/baby-feeding-calculator/bibliography.astro +8 -4
- package/src/tool/baby-feeding-calculator/component.astro +208 -177
- package/src/tool/baby-feeding-calculator/style.css +317 -229
- package/src/tool/baby-percentile-calculator/bibliography.astro +8 -4
- package/src/tool/baby-percentile-calculator/component.astro +98 -240
- package/src/tool/baby-percentile-calculator/i18n/en.ts +1 -0
- package/src/tool/baby-percentile-calculator/i18n/es.ts +1 -0
- package/src/tool/baby-percentile-calculator/i18n/fr.ts +1 -0
- package/src/tool/baby-percentile-calculator/index.ts +1 -0
- package/src/tool/baby-percentile-calculator/style.css +342 -268
- package/src/tool/baby-size-converter/bibliography.astro +8 -4
- package/src/tool/baby-size-converter/component.astro +221 -212
- package/src/tool/baby-size-converter/style.css +433 -263
- package/src/tool/fertile-days-estimator/bibliography.astro +8 -4
- package/src/tool/fertile-days-estimator/component.astro +202 -200
- package/src/tool/fertile-days-estimator/style.css +408 -270
- package/src/tool/pregnancy-calculator/bibliography.astro +8 -4
- package/src/tool/pregnancy-calculator/component.astro +50 -8
- package/src/tool/pregnancy-calculator/i18n/en.ts +8 -0
- package/src/tool/pregnancy-calculator/i18n/es.ts +8 -0
- package/src/tool/pregnancy-calculator/i18n/fr.ts +8 -0
- package/src/tool/pregnancy-calculator/index.ts +8 -0
- package/src/tool/pregnancy-calculator/style.css +351 -134
- package/src/tool/vaccination-calendar/bibliography.astro +8 -4
- package/src/tool/vaccination-calendar/component.astro +120 -124
- package/src/tool/vaccination-calendar/style.css +296 -209
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { Bibliography as BibliographyUI } from '@jjlmoya/utils-shared';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { vaccinationCalendar } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props { locale?: KnownLocale; }
|
|
7
|
+
const { locale = 'es' } = Astro.props;
|
|
8
|
+
const content = await vaccinationCalendar.i18n[locale]?.();
|
|
9
|
+
if (!content) return null;
|
|
6
10
|
---
|
|
7
|
-
<BibliographyUI links={
|
|
11
|
+
<BibliographyUI links={content.bibliography} title={content.bibliographyTitle ?? ''} />
|
|
@@ -4,100 +4,82 @@ import type { VaccinationCalendarUI } from './index';
|
|
|
4
4
|
interface Props { ui: VaccinationCalendarUI; }
|
|
5
5
|
const { ui } = Astro.props;
|
|
6
6
|
---
|
|
7
|
-
<div
|
|
8
|
-
<
|
|
9
|
-
<div class="
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
<input
|
|
13
|
-
class="
|
|
14
|
-
|
|
15
|
-
type="text"
|
|
16
|
-
|
|
17
|
-
maxlength="
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/>
|
|
21
|
-
<span class="vaccination-calendar-sep">/</span>
|
|
22
|
-
<input
|
|
23
|
-
class="vaccination-calendar-segment"
|
|
24
|
-
id="vc-mm"
|
|
25
|
-
type="text"
|
|
26
|
-
inputmode="numeric"
|
|
27
|
-
maxlength="2"
|
|
28
|
-
placeholder={ui.placeholderMM}
|
|
29
|
-
aria-label="Mes"
|
|
30
|
-
/>
|
|
31
|
-
<span class="vaccination-calendar-sep">/</span>
|
|
32
|
-
<input
|
|
33
|
-
class="vaccination-calendar-segment vaccination-calendar-segment-year"
|
|
34
|
-
id="vc-yyyy"
|
|
35
|
-
type="text"
|
|
36
|
-
inputmode="numeric"
|
|
37
|
-
maxlength="4"
|
|
38
|
-
placeholder={ui.placeholderAAAA}
|
|
39
|
-
aria-label="Año"
|
|
40
|
-
/>
|
|
7
|
+
<div class="vc-card" id="vc-root" data-ui={JSON.stringify(ui)}>
|
|
8
|
+
<header class="vc-header">
|
|
9
|
+
<div class="vc-inputs">
|
|
10
|
+
<div class="vc-field">
|
|
11
|
+
<label>{ui.labelBirthDate}</label>
|
|
12
|
+
<div class="vc-triple-input" id="vc-triple">
|
|
13
|
+
<input type="text" id="vc-dd" class="vc-segment" placeholder={ui.placeholderDD} inputmode="numeric" maxlength="2" aria-label="Día" />
|
|
14
|
+
<span class="vc-sep">/</span>
|
|
15
|
+
<input type="text" id="vc-mm" class="vc-segment" placeholder={ui.placeholderMM} inputmode="numeric" maxlength="2" aria-label="Mes" />
|
|
16
|
+
<span class="vc-sep">/</span>
|
|
17
|
+
<input type="text" id="vc-yyyy" class="vc-segment vc-segment-year" placeholder={ui.placeholderAAAA} inputmode="numeric" maxlength="4" aria-label="Año" />
|
|
18
|
+
</div>
|
|
19
|
+
<div class="vc-age-badge" id="vc-age-badge"></div>
|
|
41
20
|
</div>
|
|
42
|
-
<div class="vaccination-calendar-error" id="vc-error" role="alert" aria-live="polite"></div>
|
|
43
21
|
</div>
|
|
44
|
-
|
|
45
|
-
</div>
|
|
22
|
+
</header>
|
|
46
23
|
|
|
47
|
-
<div
|
|
48
|
-
|
|
24
|
+
<div id="vc-empty" class="vc-empty-state">
|
|
25
|
+
{ui.emptyMsg}
|
|
49
26
|
</div>
|
|
50
27
|
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
<div class="
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
<button class="vaccination-calendar-btn-primary vaccination-calendar-btn-share" id="vc-btn-share">{ui.labelShare}</button>
|
|
58
|
-
</div>
|
|
59
|
-
<div class="vaccination-calendar-footer">
|
|
60
|
-
<span>{ui.labelSource}</span>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
28
|
+
<section class="vc-main-context" id="vc-context">
|
|
29
|
+
<h3 class="vc-next-title">{ui.labelNextAppointment}</h3>
|
|
30
|
+
<div class="vc-next-date" id="vc-next-date"></div>
|
|
31
|
+
<div class="vc-vac-list" id="vc-next-vaccines"></div>
|
|
32
|
+
<button id="vc-btn-reminder" class="vc-btn-primary">{ui.btnAddReminder}</button>
|
|
33
|
+
</section>
|
|
63
34
|
|
|
64
|
-
<div class="
|
|
65
|
-
<
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
<
|
|
35
|
+
<div class="vc-sections" id="vc-sections">
|
|
36
|
+
<div class="vc-accordion-item" id="vc-accordion-past">
|
|
37
|
+
<button class="vc-accordion-trigger">
|
|
38
|
+
<span>{ui.labelPassed}</span>
|
|
39
|
+
<span class="vc-timeline-status vc-check">OK</span>
|
|
40
|
+
</button>
|
|
41
|
+
<div class="vc-accordion-content">
|
|
42
|
+
<div class="vc-timeline-compact" id="vc-timeline-past"></div>
|
|
69
43
|
</div>
|
|
70
|
-
</
|
|
71
|
-
<
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
<
|
|
44
|
+
</div>
|
|
45
|
+
<div class="vc-accordion-item" id="vc-accordion-future">
|
|
46
|
+
<button class="vc-accordion-trigger">
|
|
47
|
+
<span>{ui.labelFuture}</span>
|
|
48
|
+
<span class="vc-timeline-status vc-clock">...</span>
|
|
49
|
+
</button>
|
|
50
|
+
<div class="vc-accordion-content">
|
|
51
|
+
<div class="vc-timeline-compact" id="vc-timeline-future"></div>
|
|
75
52
|
</div>
|
|
76
|
-
</
|
|
53
|
+
</div>
|
|
77
54
|
</div>
|
|
55
|
+
|
|
56
|
+
<footer class="vc-footer">
|
|
57
|
+
{ui.labelSource} •
|
|
58
|
+
<a href="javascript:void(0)" id="vc-share-link" class="vc-share-link">{ui.labelShare}</a>
|
|
59
|
+
</footer>
|
|
78
60
|
</div>
|
|
79
61
|
|
|
80
62
|
<script>
|
|
81
63
|
import { buildDoseGroups, getAgeLabel, calculateAge, buildIcsContent } from './logic';
|
|
82
64
|
import type { DoseGroup } from './logic';
|
|
83
65
|
|
|
84
|
-
const root = document.getElementById('
|
|
66
|
+
const root = document.getElementById('vc-root') as HTMLElement;
|
|
85
67
|
const ui = JSON.parse(root.dataset.ui as string) as Record<string, string>;
|
|
86
68
|
|
|
87
|
-
const ddInput =
|
|
88
|
-
const mmInput =
|
|
89
|
-
const yyyyInput =
|
|
90
|
-
const
|
|
91
|
-
const ageBadge =
|
|
92
|
-
const emptyEl =
|
|
93
|
-
const contextEl =
|
|
94
|
-
const sectionsEl =
|
|
95
|
-
const nextDateEl =
|
|
96
|
-
const nextVaccinesEl =
|
|
97
|
-
const timelinePast =
|
|
98
|
-
const timelineFuture =
|
|
99
|
-
const btnReminder =
|
|
100
|
-
const
|
|
69
|
+
const ddInput = document.getElementById('vc-dd') as HTMLInputElement;
|
|
70
|
+
const mmInput = document.getElementById('vc-mm') as HTMLInputElement;
|
|
71
|
+
const yyyyInput = document.getElementById('vc-yyyy') as HTMLInputElement;
|
|
72
|
+
const tripleEl = document.getElementById('vc-triple') as HTMLElement;
|
|
73
|
+
const ageBadge = document.getElementById('vc-age-badge') as HTMLElement;
|
|
74
|
+
const emptyEl = document.getElementById('vc-empty') as HTMLElement;
|
|
75
|
+
const contextEl = document.getElementById('vc-context') as HTMLElement;
|
|
76
|
+
const sectionsEl = document.getElementById('vc-sections') as HTMLElement;
|
|
77
|
+
const nextDateEl = document.getElementById('vc-next-date') as HTMLElement;
|
|
78
|
+
const nextVaccinesEl = document.getElementById('vc-next-vaccines') as HTMLElement;
|
|
79
|
+
const timelinePast = document.getElementById('vc-timeline-past') as HTMLElement;
|
|
80
|
+
const timelineFuture = document.getElementById('vc-timeline-future') as HTMLElement;
|
|
81
|
+
const btnReminder = document.getElementById('vc-btn-reminder') as HTMLButtonElement;
|
|
82
|
+
const shareLink = document.getElementById('vc-share-link') as HTMLAnchorElement;
|
|
101
83
|
|
|
102
84
|
const doseGroups = buildDoseGroups();
|
|
103
85
|
let currentBirthDate: Date | null = null;
|
|
@@ -121,27 +103,27 @@ const { ui } = Astro.props;
|
|
|
121
103
|
return new Intl.DateTimeFormat('es-ES', { day: '2-digit', month: 'long', year: 'numeric' }).format(date);
|
|
122
104
|
}
|
|
123
105
|
|
|
124
|
-
function buildTimelineRow(group: DoseGroup,
|
|
125
|
-
const target = new Date(birthDate);
|
|
126
|
-
target.setMonth(target.getMonth() + group.months);
|
|
106
|
+
function buildTimelineRow(group: DoseGroup, _birthDate: Date, isPast: boolean): HTMLElement {
|
|
127
107
|
const row = document.createElement('div');
|
|
128
|
-
row.className = '
|
|
129
|
-
|
|
130
|
-
ageEl
|
|
108
|
+
row.className = 'vc-timeline-row';
|
|
109
|
+
|
|
110
|
+
const ageEl = document.createElement('span');
|
|
111
|
+
ageEl.className = 'vc-timeline-age';
|
|
131
112
|
ageEl.textContent = getAgeLabel(group.months);
|
|
113
|
+
|
|
132
114
|
const vacEl = document.createElement('div');
|
|
133
|
-
vacEl.className = '
|
|
115
|
+
vacEl.className = 'vc-timeline-vac';
|
|
134
116
|
group.vaccines.forEach((name) => {
|
|
135
117
|
const pill = document.createElement('span');
|
|
136
|
-
pill.className = '
|
|
118
|
+
pill.className = 'vc-vac-pill';
|
|
137
119
|
pill.textContent = name;
|
|
138
120
|
vacEl.appendChild(pill);
|
|
139
121
|
});
|
|
140
|
-
|
|
141
|
-
statusEl
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
122
|
+
|
|
123
|
+
const statusEl = document.createElement('span');
|
|
124
|
+
statusEl.className = isPast ? 'vc-timeline-status vc-check' : 'vc-timeline-status vc-clock';
|
|
125
|
+
statusEl.textContent = isPast ? (ui['labelStatusOk'] ?? 'OK') : (ui['labelStatusPending'] ?? 'PEND.');
|
|
126
|
+
|
|
145
127
|
row.appendChild(ageEl);
|
|
146
128
|
row.appendChild(vacEl);
|
|
147
129
|
row.appendChild(statusEl);
|
|
@@ -158,12 +140,9 @@ const { ui } = Astro.props;
|
|
|
158
140
|
target.setMonth(target.getMonth() + group.months);
|
|
159
141
|
target.setHours(0, 0, 0, 0);
|
|
160
142
|
const isPast = target <= today;
|
|
161
|
-
const row = buildTimelineRow(group, birthDate,
|
|
162
|
-
if (isPast)
|
|
163
|
-
|
|
164
|
-
} else {
|
|
165
|
-
timelineFuture.appendChild(row);
|
|
166
|
-
}
|
|
143
|
+
const row = buildTimelineRow(group, birthDate, isPast);
|
|
144
|
+
if (isPast) timelinePast.appendChild(row);
|
|
145
|
+
else timelineFuture.appendChild(row);
|
|
167
146
|
});
|
|
168
147
|
}
|
|
169
148
|
|
|
@@ -183,39 +162,40 @@ const { ui } = Astro.props;
|
|
|
183
162
|
const today = new Date();
|
|
184
163
|
today.setHours(0, 0, 0, 0);
|
|
185
164
|
const next = findNextGroup(birthDate);
|
|
165
|
+
nextVaccinesEl.innerHTML = '';
|
|
186
166
|
if (!next) {
|
|
187
167
|
nextDateEl.textContent = 'Calendario completo';
|
|
188
|
-
nextVaccinesEl.innerHTML = '';
|
|
189
168
|
return;
|
|
190
169
|
}
|
|
191
170
|
const target = new Date(birthDate);
|
|
192
171
|
target.setMonth(target.getMonth() + next.months);
|
|
193
172
|
target.setHours(0, 0, 0, 0);
|
|
194
173
|
const isToday = target.getTime() === today.getTime();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
li.className = 'vaccination-calendar-vac-item';
|
|
206
|
-
const nameEl = document.createElement('span');
|
|
207
|
-
nameEl.className = 'vaccination-calendar-vac-name';
|
|
208
|
-
nameEl.textContent = name;
|
|
209
|
-
li.appendChild(nameEl);
|
|
210
|
-
nextVaccinesEl.appendChild(li);
|
|
174
|
+
root.classList.toggle('vc-is-today', isToday);
|
|
175
|
+
nextDateEl.textContent = isToday
|
|
176
|
+
? (ui['btnToday'] ?? '¡Hoy!')
|
|
177
|
+
: `${getAgeLabel(next.months)} — ${formatDate(target)}`;
|
|
178
|
+
|
|
179
|
+
next.vaccines.forEach((name, idx) => {
|
|
180
|
+
const item = document.createElement('div');
|
|
181
|
+
item.className = 'vc-vac-item';
|
|
182
|
+
item.innerHTML = `<div class="vc-vac-icon">${idx + 1}</div><div class="vc-vac-info"><span class="vc-vac-name">${name}</span></div>`;
|
|
183
|
+
nextVaccinesEl.appendChild(item);
|
|
211
184
|
});
|
|
185
|
+
|
|
186
|
+
if (btnReminder) {
|
|
187
|
+
btnReminder.innerHTML = isToday
|
|
188
|
+
? `<span>${ui['btnToday'] ?? '¡Hoy!'}</span>`
|
|
189
|
+
: `<span>${ui['btnAddReminder'] ?? ''}</span>`;
|
|
190
|
+
}
|
|
212
191
|
}
|
|
213
192
|
|
|
214
193
|
function updateDisplay(birthDate: Date): void {
|
|
215
194
|
const today = new Date();
|
|
216
195
|
today.setHours(0, 0, 0, 0);
|
|
217
196
|
ageBadge.textContent = calculateAge(birthDate, today);
|
|
218
|
-
|
|
197
|
+
ageBadge.classList.add('vc-age-visible');
|
|
198
|
+
root.classList.add('vc-active');
|
|
219
199
|
emptyEl.style.display = 'none';
|
|
220
200
|
contextEl.style.display = '';
|
|
221
201
|
sectionsEl.style.display = '';
|
|
@@ -224,12 +204,13 @@ const { ui } = Astro.props;
|
|
|
224
204
|
}
|
|
225
205
|
|
|
226
206
|
function clearDisplay(): void {
|
|
227
|
-
root.classList.remove('
|
|
207
|
+
root.classList.remove('vc-active', 'vc-is-today');
|
|
228
208
|
ageBadge.textContent = '';
|
|
209
|
+
ageBadge.classList.remove('vc-age-visible');
|
|
229
210
|
emptyEl.style.display = '';
|
|
230
211
|
contextEl.style.display = 'none';
|
|
231
212
|
sectionsEl.style.display = 'none';
|
|
232
|
-
|
|
213
|
+
tripleEl.classList.remove('has-error');
|
|
233
214
|
}
|
|
234
215
|
|
|
235
216
|
function handleInput(): void {
|
|
@@ -237,7 +218,7 @@ const { ui } = Astro.props;
|
|
|
237
218
|
if (!date) {
|
|
238
219
|
currentBirthDate = null;
|
|
239
220
|
const hasValue = ddInput.value || mmInput.value || yyyyInput.value;
|
|
240
|
-
|
|
221
|
+
tripleEl.classList.toggle('has-error', !!hasValue);
|
|
241
222
|
clearDisplay();
|
|
242
223
|
return;
|
|
243
224
|
}
|
|
@@ -245,17 +226,18 @@ const { ui } = Astro.props;
|
|
|
245
226
|
today.setHours(0, 0, 0, 0);
|
|
246
227
|
if (date > today) {
|
|
247
228
|
currentBirthDate = null;
|
|
248
|
-
|
|
229
|
+
tripleEl.classList.add('has-error');
|
|
249
230
|
clearDisplay();
|
|
250
231
|
return;
|
|
251
232
|
}
|
|
252
|
-
|
|
233
|
+
tripleEl.classList.remove('has-error');
|
|
253
234
|
currentBirthDate = date;
|
|
254
235
|
updateDisplay(date);
|
|
255
236
|
}
|
|
256
237
|
|
|
257
238
|
function autoAdvance(input: HTMLInputElement, next: HTMLInputElement | null, maxLen: number): void {
|
|
258
239
|
input.addEventListener('input', () => {
|
|
240
|
+
input.value = input.value.replace(/\D/g, '');
|
|
259
241
|
if (input.value.length >= maxLen && next) next.focus();
|
|
260
242
|
handleInput();
|
|
261
243
|
});
|
|
@@ -263,9 +245,9 @@ const { ui } = Astro.props;
|
|
|
263
245
|
|
|
264
246
|
autoAdvance(ddInput, mmInput, 2);
|
|
265
247
|
autoAdvance(mmInput, yyyyInput, 2);
|
|
266
|
-
yyyyInput.addEventListener('input', handleInput);
|
|
248
|
+
yyyyInput.addEventListener('input', () => { yyyyInput.value = yyyyInput.value.replace(/\D/g, ''); handleInput(); });
|
|
267
249
|
|
|
268
|
-
btnReminder
|
|
250
|
+
btnReminder?.addEventListener('click', () => {
|
|
269
251
|
if (!currentBirthDate) return;
|
|
270
252
|
const ics = buildIcsContent(currentBirthDate, doseGroups);
|
|
271
253
|
const blob = new Blob([ics], { type: 'text/calendar' });
|
|
@@ -277,9 +259,23 @@ const { ui } = Astro.props;
|
|
|
277
259
|
URL.revokeObjectURL(url);
|
|
278
260
|
});
|
|
279
261
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
262
|
+
shareLink?.addEventListener('click', async () => {
|
|
263
|
+
try {
|
|
264
|
+
if (navigator.share) {
|
|
265
|
+
await navigator.share({ title: ui['labelNextAppointment'] ?? '', url: window.location.href });
|
|
266
|
+
} else {
|
|
267
|
+
await navigator.clipboard.writeText(window.location.href);
|
|
268
|
+
const orig = shareLink.textContent ?? '';
|
|
269
|
+
shareLink.textContent = '¡Enlace copiado!';
|
|
270
|
+
setTimeout(() => { shareLink.textContent = orig; }, 2000);
|
|
271
|
+
}
|
|
272
|
+
} catch {}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
root.querySelectorAll('.vc-accordion-trigger').forEach((trigger) => {
|
|
276
|
+
trigger.addEventListener('click', () => {
|
|
277
|
+
trigger.parentElement?.classList.toggle('vc-open');
|
|
278
|
+
});
|
|
283
279
|
});
|
|
284
280
|
|
|
285
281
|
clearDisplay();
|