@jjlmoya/utils-home 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 (77) hide show
  1. package/package.json +62 -0
  2. package/src/category/i18n/en.ts +24 -0
  3. package/src/category/i18n/es.ts +24 -0
  4. package/src/category/i18n/fr.ts +24 -0
  5. package/src/category/index.ts +12 -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 +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +26 -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 +22 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/dewPointCalculator/bibliography.astro +14 -0
  23. package/src/tool/dewPointCalculator/component.astro +443 -0
  24. package/src/tool/dewPointCalculator/i18n/en.ts +183 -0
  25. package/src/tool/dewPointCalculator/i18n/es.ts +183 -0
  26. package/src/tool/dewPointCalculator/i18n/fr.ts +183 -0
  27. package/src/tool/dewPointCalculator/index.ts +34 -0
  28. package/src/tool/dewPointCalculator/logic.ts +16 -0
  29. package/src/tool/dewPointCalculator/seo.astro +14 -0
  30. package/src/tool/dewPointCalculator/ui.ts +13 -0
  31. package/src/tool/ledSavingCalculator/bibliography.astro +14 -0
  32. package/src/tool/ledSavingCalculator/component.astro +520 -0
  33. package/src/tool/ledSavingCalculator/i18n/en.ts +217 -0
  34. package/src/tool/ledSavingCalculator/i18n/es.ts +217 -0
  35. package/src/tool/ledSavingCalculator/i18n/fr.ts +217 -0
  36. package/src/tool/ledSavingCalculator/index.ts +34 -0
  37. package/src/tool/ledSavingCalculator/logic.ts +31 -0
  38. package/src/tool/ledSavingCalculator/seo.astro +14 -0
  39. package/src/tool/ledSavingCalculator/ui.ts +32 -0
  40. package/src/tool/projectorCalculator/bibliography.astro +14 -0
  41. package/src/tool/projectorCalculator/component.astro +569 -0
  42. package/src/tool/projectorCalculator/i18n/en.ts +181 -0
  43. package/src/tool/projectorCalculator/i18n/es.ts +181 -0
  44. package/src/tool/projectorCalculator/i18n/fr.ts +181 -0
  45. package/src/tool/projectorCalculator/index.ts +34 -0
  46. package/src/tool/projectorCalculator/logic.ts +21 -0
  47. package/src/tool/projectorCalculator/seo.astro +14 -0
  48. package/src/tool/projectorCalculator/ui.ts +16 -0
  49. package/src/tool/qrGenerator/bibliography.astro +14 -0
  50. package/src/tool/qrGenerator/component.astro +499 -0
  51. package/src/tool/qrGenerator/i18n/en.ts +233 -0
  52. package/src/tool/qrGenerator/i18n/es.ts +233 -0
  53. package/src/tool/qrGenerator/i18n/fr.ts +233 -0
  54. package/src/tool/qrGenerator/index.ts +34 -0
  55. package/src/tool/qrGenerator/logic.ts +27 -0
  56. package/src/tool/qrGenerator/seo.astro +14 -0
  57. package/src/tool/qrGenerator/ui.ts +23 -0
  58. package/src/tool/solarCalculator/bibliography.astro +14 -0
  59. package/src/tool/solarCalculator/component.astro +532 -0
  60. package/src/tool/solarCalculator/i18n/en.ts +176 -0
  61. package/src/tool/solarCalculator/i18n/es.ts +176 -0
  62. package/src/tool/solarCalculator/i18n/fr.ts +176 -0
  63. package/src/tool/solarCalculator/index.ts +34 -0
  64. package/src/tool/solarCalculator/logic.ts +31 -0
  65. package/src/tool/solarCalculator/seo.astro +14 -0
  66. package/src/tool/solarCalculator/ui.ts +11 -0
  67. package/src/tool/tariffComparator/bibliography.astro +14 -0
  68. package/src/tool/tariffComparator/component.astro +595 -0
  69. package/src/tool/tariffComparator/i18n/en.ts +192 -0
  70. package/src/tool/tariffComparator/i18n/es.ts +192 -0
  71. package/src/tool/tariffComparator/i18n/fr.ts +192 -0
  72. package/src/tool/tariffComparator/index.ts +34 -0
  73. package/src/tool/tariffComparator/logic.ts +47 -0
  74. package/src/tool/tariffComparator/seo.astro +14 -0
  75. package/src/tool/tariffComparator/ui.ts +25 -0
  76. package/src/tools.ts +9 -0
  77. package/src/types.ts +72 -0
@@ -0,0 +1,520 @@
1
+ ---
2
+ import type { LedSavingCalculatorUI } from './ui';
3
+ import { calculateLedSaving, fmt2 } from './logic';
4
+
5
+ interface Props {
6
+ ui?: Record<string, unknown>;
7
+ }
8
+
9
+ const { ui = {} } = Astro.props;
10
+ const lUI = ui as LedSavingCalculatorUI;
11
+
12
+ const INITIAL_BULBS = 10;
13
+ const INITIAL_OLD_W = 60;
14
+ const INITIAL_LED_W = 9;
15
+ const INITIAL_HOURS = 5;
16
+ const INITIAL_PRICE = 0.18;
17
+
18
+ const initial = calculateLedSaving({
19
+ numBulbs: INITIAL_BULBS,
20
+ oldWatts: INITIAL_OLD_W,
21
+ ledWatts: INITIAL_LED_W,
22
+ hoursPerDay: INITIAL_HOURS,
23
+ pricePerKwh: INITIAL_PRICE,
24
+ });
25
+ ---
26
+
27
+ <div class="led-wrapper">
28
+ <div
29
+ class="led-card"
30
+ data-usage-never={lUI.usageNever}
31
+ data-usage-low={lUI.usageLow}
32
+ data-usage-normal={lUI.usageNormal}
33
+ data-usage-moderate={lUI.usageModerate}
34
+ data-usage-heavy={lUI.usageHeavy}
35
+ data-usage-pro={lUI.usagePro}
36
+ data-usage-very-heavy={lUI.usageVeryHeavy}
37
+ data-usage-always={lUI.usageAlways}
38
+ data-unit-less={lUI.unitLess}
39
+ data-currency-sign={lUI.currencySign}
40
+ >
41
+ <div class="led-left">
42
+ <p class="led-section-title">{lUI.sectionTitle}</p>
43
+
44
+ <div class="led-field">
45
+ <label class="led-label" for="led-num-bulbs">{lUI.labelBulbs}</label>
46
+ <div class="led-number-row">
47
+ <input type="number" id="led-num-bulbs" value={INITIAL_BULBS} min="1" max="200" class="led-number-input" />
48
+ <span class="led-number-unit">{lUI.unitBulbs}</span>
49
+ </div>
50
+ </div>
51
+
52
+ <div class="led-field">
53
+ <label class="led-label">{lUI.labelType}</label>
54
+ <div class="led-type-grid">
55
+ <button class="led-type-btn led-type-active" data-old="60" data-led="9">
56
+ <span class="led-type-title">{lUI.btnInc60Title}</span>
57
+ <span class="led-type-sub">{lUI.btnInc60Sub}</span>
58
+ </button>
59
+ <button class="led-type-btn" data-old="40" data-led="5">
60
+ <span class="led-type-title">{lUI.btnInc40Title}</span>
61
+ <span class="led-type-sub">{lUI.btnInc40Sub}</span>
62
+ </button>
63
+ <button class="led-type-btn" data-old="100" data-led="14">
64
+ <span class="led-type-title">{lUI.btnInc100Title}</span>
65
+ <span class="led-type-sub">{lUI.btnInc100Sub}</span>
66
+ </button>
67
+ <button class="led-type-btn" data-old="50" data-led="6">
68
+ <span class="led-type-title">{lUI.btnHalo50Title}</span>
69
+ <span class="led-type-sub">{lUI.btnHalo50Sub}</span>
70
+ </button>
71
+ </div>
72
+ <input type="hidden" id="led-old-watts" value={INITIAL_OLD_W} />
73
+ <input type="hidden" id="led-led-watts" value={INITIAL_LED_W} />
74
+ </div>
75
+
76
+ <div class="led-field">
77
+ <label class="led-label" for="led-hours-slider">{lUI.labelHours}</label>
78
+ <input type="range" id="led-hours-slider" min="1" max="24" value={INITIAL_HOURS} step="1" class="led-slider" />
79
+ <p id="led-usage-desc" class="led-usage-desc">{lUI.usageNormal} ({INITIAL_HOURS}h)</p>
80
+ <input type="hidden" id="led-hours" value={INITIAL_HOURS} />
81
+ </div>
82
+
83
+ <div class="led-field">
84
+ <label class="led-label" for="led-price">{lUI.labelPrice}</label>
85
+ <div class="led-price-row">
86
+ <input type="number" id="led-price" value={INITIAL_PRICE} min="0.01" step="0.01" class="led-price-input" />
87
+ <span class="led-price-unit">{lUI.unitPrice}</span>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ <div class="led-right">
93
+ <div class="led-result-badge">{lUI.resultBadge}</div>
94
+
95
+ <div class="led-annual-section">
96
+ <p class="led-annual-label">{lUI.labelAnnual}</p>
97
+ <p class="led-annual-value">
98
+ <span id="led-annual-num">{fmt2(initial.annualEuro)}</span><span id="led-currency-sign" class="led-euro-sign">{lUI.currencySign}</span>
99
+ </p>
100
+ </div>
101
+
102
+ <div class="led-stats">
103
+ <div class="led-stat">
104
+ <p class="led-stat-label">{lUI.labelMonthly}</p>
105
+ <p id="led-monthly" class="led-stat-value">{fmt2(initial.monthlyEuro)}{lUI.currencySign}</p>
106
+ </div>
107
+ <div class="led-stat-divider"></div>
108
+ <div class="led-stat">
109
+ <p class="led-stat-label">{lUI.labelEfficiency}</p>
110
+ <p id="led-efficiency" class="led-stat-value">{initial.efficiency}{lUI.unitLess}</p>
111
+ </div>
112
+ </div>
113
+
114
+ <div class="led-eco-box">
115
+ <div class="led-eco-dot"></div>
116
+ <div class="led-eco-text">
117
+ <span id="led-co2" class="led-eco-val">{Math.round(initial.co2Kg)} kg</span>
118
+ <span class="led-eco-desc">{lUI.labelCo2}</span>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+
125
+ <script>
126
+ import { calculateLedSaving, fmt2 } from './logic';
127
+
128
+ function setTxt(id: string, val: string) {
129
+ const el = document.getElementById(id);
130
+ if (el) el.textContent = val;
131
+ }
132
+
133
+ function getUsageKey(h: number): string {
134
+ if (h === 1) return 'never';
135
+ if (h <= 3) return 'low';
136
+ if (h <= 5) return 'normal';
137
+ if (h <= 7) return 'moderate';
138
+ if (h <= 11) return 'heavy';
139
+ if (h <= 15) return 'pro';
140
+ if (h < 24) return 'veryHeavy';
141
+ return 'always';
142
+ }
143
+
144
+ function getDataset(card: HTMLElement, key: string): string {
145
+ return card.dataset[key] ?? '';
146
+ }
147
+
148
+ function getVal(id: string): number {
149
+ return Number((document.getElementById(id) as HTMLInputElement | null)?.value) || 0;
150
+ }
151
+
152
+ function calculate(card: HTMLElement) {
153
+ const r = calculateLedSaving({
154
+ numBulbs: getVal('led-num-bulbs'),
155
+ oldWatts: getVal('led-old-watts'),
156
+ ledWatts: getVal('led-led-watts'),
157
+ hoursPerDay: getVal('led-hours'),
158
+ pricePerKwh: getVal('led-price'),
159
+ });
160
+ const unitLess = getDataset(card, 'unitLess');
161
+ const currencySign = getDataset(card, 'currencySign');
162
+ setTxt('led-annual-num', fmt2(r.annualEuro));
163
+ setTxt('led-currency-sign', currencySign);
164
+ setTxt('led-monthly', `${fmt2(r.monthlyEuro)}${currencySign}`);
165
+ setTxt('led-efficiency', `${r.efficiency}${unitLess}`);
166
+ setTxt('led-co2', `${Math.round(r.co2Kg)} kg`);
167
+ }
168
+
169
+ function updateUsageDesc(h: number, card: HTMLElement) {
170
+ const key = getUsageKey(h);
171
+ const camel = key.charAt(0).toUpperCase() + key.slice(1);
172
+ const label = getDataset(card, `usage${camel}`);
173
+ setTxt('led-usage-desc', `${label} (${h}h)`);
174
+ }
175
+
176
+ function attachTypeButtons(card: HTMLElement) {
177
+ document.querySelectorAll<HTMLElement>('.led-type-btn').forEach(btn => {
178
+ btn.addEventListener('click', () => {
179
+ document.querySelectorAll('.led-type-btn').forEach(b => b.classList.remove('led-type-active'));
180
+ btn.classList.add('led-type-active');
181
+ const oldEl = document.getElementById('led-old-watts') as HTMLInputElement | null;
182
+ const ledEl = document.getElementById('led-led-watts') as HTMLInputElement | null;
183
+ if (oldEl && ledEl && btn.dataset.old && btn.dataset.led) {
184
+ oldEl.value = btn.dataset.old;
185
+ ledEl.value = btn.dataset.led;
186
+ calculate(card);
187
+ }
188
+ });
189
+ });
190
+ }
191
+
192
+ function init() {
193
+ const card = document.querySelector('.led-card') as HTMLElement | null;
194
+ if (!card) return;
195
+ const hoursSlider = document.getElementById('led-hours-slider') as HTMLInputElement | null;
196
+ const hoursHidden = document.getElementById('led-hours') as HTMLInputElement | null;
197
+ if (hoursSlider && hoursHidden) {
198
+ hoursSlider.addEventListener('input', () => {
199
+ hoursHidden.value = hoursSlider.value;
200
+ updateUsageDesc(Number(hoursSlider.value), card);
201
+ calculate(card);
202
+ });
203
+ }
204
+ attachTypeButtons(card);
205
+ const numBulbsEl = document.getElementById('led-num-bulbs');
206
+ const priceEl = document.getElementById('led-price');
207
+ if (numBulbsEl) numBulbsEl.addEventListener('input', () => calculate(card));
208
+ if (priceEl) priceEl.addEventListener('input', () => calculate(card));
209
+ calculate(card);
210
+ }
211
+
212
+ document.addEventListener('astro:page-load', init);
213
+ init();
214
+ </script>
215
+
216
+ <style>
217
+ .led-wrapper {
218
+ --led-p: #f59e0b;
219
+
220
+ width: 100%;
221
+ padding: 1rem 0;
222
+ }
223
+
224
+ .led-card {
225
+ background: var(--bg-surface);
226
+ width: calc(100% - 24px);
227
+ max-width: 960px;
228
+ margin: 0 auto;
229
+ border-radius: 24px;
230
+ overflow: hidden;
231
+ display: flex;
232
+ flex-direction: column;
233
+ border: 1px solid var(--border-color);
234
+ color: var(--text-main);
235
+ }
236
+
237
+ @media (min-width: 768px) {
238
+ .led-card {
239
+ flex-direction: row;
240
+ min-height: 560px;
241
+ }
242
+ }
243
+
244
+ .led-left {
245
+ flex: 0 0 auto;
246
+ width: 100%;
247
+ padding: 32px;
248
+ border-bottom: 1px solid var(--border-color);
249
+ display: flex;
250
+ flex-direction: column;
251
+ gap: 24px;
252
+ }
253
+
254
+ @media (min-width: 768px) {
255
+ .led-left {
256
+ width: 380px;
257
+ border-bottom: none;
258
+ border-right: 1px solid var(--border-color);
259
+ }
260
+ }
261
+
262
+ .led-right {
263
+ flex: 1;
264
+ background: var(--bg-page);
265
+ display: flex;
266
+ flex-direction: column;
267
+ align-items: center;
268
+ justify-content: center;
269
+ gap: 28px;
270
+ padding: 40px 32px;
271
+ min-height: 360px;
272
+ }
273
+
274
+ .led-section-title {
275
+ font-size: 0.75rem;
276
+ font-weight: 900;
277
+ text-transform: uppercase;
278
+ letter-spacing: 0.14em;
279
+ color: var(--led-p);
280
+ margin: 0;
281
+ }
282
+
283
+ .led-field {
284
+ display: flex;
285
+ flex-direction: column;
286
+ gap: 10px;
287
+ }
288
+
289
+ .led-label {
290
+ font-size: 0.6875rem;
291
+ font-weight: 700;
292
+ text-transform: uppercase;
293
+ letter-spacing: 0.1em;
294
+ color: var(--text-muted);
295
+ }
296
+
297
+ .led-number-row {
298
+ display: flex;
299
+ align-items: baseline;
300
+ gap: 8px;
301
+ }
302
+
303
+ .led-number-input {
304
+ width: 100px;
305
+ font-size: 2.5rem;
306
+ font-weight: 900;
307
+ color: var(--text-main);
308
+ background: transparent;
309
+ border: none;
310
+ border-bottom: 2px solid var(--border-color);
311
+ padding: 4px 0;
312
+ outline: none;
313
+ transition: border-color 0.2s;
314
+ }
315
+
316
+ .led-number-input:focus {
317
+ border-color: var(--led-p);
318
+ }
319
+
320
+ .led-number-unit {
321
+ font-size: 0.875rem;
322
+ color: var(--text-muted);
323
+ }
324
+
325
+ .led-type-grid {
326
+ display: grid;
327
+ grid-template-columns: 1fr 1fr;
328
+ gap: 8px;
329
+ }
330
+
331
+ .led-type-btn {
332
+ display: flex;
333
+ flex-direction: column;
334
+ align-items: flex-start;
335
+ gap: 2px;
336
+ padding: 10px 12px;
337
+ border-radius: 12px;
338
+ border: 1px solid var(--border-color);
339
+ background: var(--bg-surface);
340
+ color: var(--text-muted);
341
+ cursor: pointer;
342
+ transition: all 0.2s;
343
+ text-align: left;
344
+ }
345
+
346
+ .led-type-btn:hover {
347
+ border-color: var(--led-p);
348
+ color: var(--text-main);
349
+ }
350
+
351
+ .led-type-active {
352
+ background: var(--led-p);
353
+ border-color: var(--led-p);
354
+ color: #fff;
355
+ box-shadow: 0 4px 14px rgba(245, 158, 11, 0.35);
356
+ }
357
+
358
+ .led-type-title {
359
+ font-size: 0.75rem;
360
+ font-weight: 700;
361
+ }
362
+
363
+ .led-type-sub {
364
+ font-size: 0.625rem;
365
+ opacity: 0.75;
366
+ }
367
+
368
+ .led-slider {
369
+ width: 100%;
370
+ height: 6px;
371
+ accent-color: var(--led-p);
372
+ cursor: pointer;
373
+ border-radius: 9999px;
374
+ }
375
+
376
+ .led-usage-desc {
377
+ font-size: 0.8125rem;
378
+ color: var(--text-muted);
379
+ margin: 0;
380
+ }
381
+
382
+ .led-price-row {
383
+ display: flex;
384
+ align-items: baseline;
385
+ gap: 8px;
386
+ }
387
+
388
+ .led-price-input {
389
+ width: 90px;
390
+ font-size: 1.5rem;
391
+ font-weight: 900;
392
+ color: var(--text-main);
393
+ background: transparent;
394
+ border: none;
395
+ border-bottom: 2px solid var(--border-color);
396
+ padding: 4px 0;
397
+ outline: none;
398
+ transition: border-color 0.2s;
399
+ }
400
+
401
+ .led-price-input:focus {
402
+ border-color: var(--led-p);
403
+ }
404
+
405
+ .led-price-unit {
406
+ font-size: 0.875rem;
407
+ color: var(--text-muted);
408
+ }
409
+
410
+ .led-result-badge {
411
+ font-size: 0.625rem;
412
+ font-weight: 900;
413
+ text-transform: uppercase;
414
+ letter-spacing: 0.18em;
415
+ color: var(--led-p);
416
+ padding: 6px 14px;
417
+ border: 1px solid rgba(245, 158, 11, 0.3);
418
+ border-radius: 9999px;
419
+ }
420
+
421
+ .led-annual-section {
422
+ text-align: center;
423
+ }
424
+
425
+ .led-annual-label {
426
+ font-size: 0.6875rem;
427
+ font-weight: 700;
428
+ text-transform: uppercase;
429
+ letter-spacing: 0.15em;
430
+ color: var(--text-muted);
431
+ margin: 0 0 8px;
432
+ }
433
+
434
+ .led-annual-value {
435
+ font-size: 5rem;
436
+ font-weight: 900;
437
+ color: var(--led-p);
438
+ line-height: 1;
439
+ margin: 0;
440
+ }
441
+
442
+ .led-euro-sign {
443
+ font-size: 2.5rem;
444
+ font-weight: 300;
445
+ color: var(--text-muted);
446
+ }
447
+
448
+ .led-stats {
449
+ display: flex;
450
+ align-items: center;
451
+ gap: 24px;
452
+ width: 100%;
453
+ max-width: 320px;
454
+ justify-content: center;
455
+ }
456
+
457
+ .led-stat {
458
+ text-align: center;
459
+ }
460
+
461
+ .led-stat-label {
462
+ font-size: 0.625rem;
463
+ font-weight: 900;
464
+ text-transform: uppercase;
465
+ letter-spacing: 0.12em;
466
+ color: var(--text-muted);
467
+ margin: 0 0 4px;
468
+ }
469
+
470
+ .led-stat-value {
471
+ font-size: 1.25rem;
472
+ font-weight: 700;
473
+ color: var(--text-main);
474
+ margin: 0;
475
+ }
476
+
477
+ .led-stat-divider {
478
+ width: 1px;
479
+ height: 36px;
480
+ background: var(--border-color);
481
+ flex-shrink: 0;
482
+ }
483
+
484
+ .led-eco-box {
485
+ display: flex;
486
+ align-items: center;
487
+ gap: 14px;
488
+ padding: 16px 20px;
489
+ border-radius: 16px;
490
+ background: rgba(34, 197, 94, 0.08);
491
+ border: 1px solid rgba(34, 197, 94, 0.2);
492
+ width: 100%;
493
+ max-width: 320px;
494
+ }
495
+
496
+ .led-eco-dot {
497
+ width: 14px;
498
+ height: 14px;
499
+ border-radius: 50%;
500
+ background: #22c55e;
501
+ flex-shrink: 0;
502
+ }
503
+
504
+ .led-eco-text {
505
+ display: flex;
506
+ flex-direction: column;
507
+ gap: 2px;
508
+ }
509
+
510
+ .led-eco-val {
511
+ font-size: 1.25rem;
512
+ font-weight: 900;
513
+ color: #4ade80;
514
+ }
515
+
516
+ .led-eco-desc {
517
+ font-size: 0.6875rem;
518
+ color: var(--text-muted);
519
+ }
520
+ </style>