@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,499 @@
1
+ ---
2
+ import type { QRGeneratorUI } from './ui';
3
+
4
+ interface Props {
5
+ ui?: Record<string, unknown>;
6
+ }
7
+
8
+ const { ui = {} } = Astro.props;
9
+ const qrUI = ui as QRGeneratorUI;
10
+ ---
11
+
12
+ <script define:vars={{ ui }} is:inline>
13
+ window.__qrUI = ui;
14
+ </script>
15
+
16
+ <div class="qr-wrapper" id="qr-gen">
17
+ <div class="qr-card">
18
+ <div class="qr-left">
19
+ <div class="tab-bar" id="qr-tabs">
20
+ <button class="tab-btn active" data-tab="wifi">{qrUI.tabWifi}</button>
21
+ <button class="tab-btn" data-tab="url">{qrUI.tabUrl}</button>
22
+ <button class="tab-btn" data-tab="vcard">{qrUI.tabContact}</button>
23
+ </div>
24
+
25
+ <div id="form-wifi" class="qr-form">
26
+ <div class="field-group">
27
+ <label class="field-label">{qrUI.labelSsid}</label>
28
+ <input type="text" name="ssid" class="qr-input field-input" placeholder={qrUI.placeholderSsid} />
29
+ </div>
30
+ <div class="field-group">
31
+ <label class="field-label">{qrUI.labelPassword}</label>
32
+ <div class="input-row">
33
+ <input type="password" name="password" id="wifi-password" class="qr-input field-input" placeholder={qrUI.placeholderPassword} />
34
+ <button type="button" id="toggle-password" class="eye-btn" aria-label="toggle password">
35
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>
36
+ </button>
37
+ </div>
38
+ </div>
39
+ <div class="two-col">
40
+ <div class="field-group">
41
+ <label class="field-label">{qrUI.labelEncryption}</label>
42
+ <select name="encryption" class="qr-input field-input">
43
+ <option value="WPA">{qrUI.encWpa}</option>
44
+ <option value="WEP">{qrUI.encWep}</option>
45
+ <option value="nopass">{qrUI.encNone}</option>
46
+ </select>
47
+ </div>
48
+ <div class="field-group check-group">
49
+ <label class="check-label">
50
+ <input type="checkbox" name="hidden" class="qr-input check-input" />
51
+ <span>{qrUI.labelHidden}</span>
52
+ </label>
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <div id="form-url" class="qr-form hidden">
58
+ <div class="field-group">
59
+ <label class="field-label">{qrUI.labelUrl}</label>
60
+ <input type="url" name="url" class="qr-input field-input" placeholder={qrUI.placeholderUrl} />
61
+ </div>
62
+ </div>
63
+
64
+ <div id="form-vcard" class="qr-form hidden">
65
+ <div class="two-col">
66
+ <div class="field-group">
67
+ <label class="field-label">{qrUI.labelName}</label>
68
+ <input type="text" name="name" class="qr-input field-input" />
69
+ </div>
70
+ <div class="field-group">
71
+ <label class="field-label">{qrUI.labelSurname}</label>
72
+ <input type="text" name="surname" class="qr-input field-input" />
73
+ </div>
74
+ </div>
75
+ <div class="field-group">
76
+ <label class="field-label">{qrUI.labelPhone}</label>
77
+ <input type="tel" name="phone" class="qr-input field-input" />
78
+ </div>
79
+ <div class="field-group">
80
+ <label class="field-label">{qrUI.labelEmail}</label>
81
+ <input type="email" name="email" class="qr-input field-input" />
82
+ </div>
83
+ <div class="field-group">
84
+ <label class="field-label">{qrUI.labelOrg}</label>
85
+ <input type="text" name="org" class="qr-input field-input" />
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <div class="qr-right">
91
+ <div class="qr-preview-area">
92
+ <div class="canvas-glow">
93
+ <div class="canvas-box">
94
+ <canvas id="qr-canvas"></canvas>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ <button id="download-btn" class="download-btn">
99
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
100
+ <span>{qrUI.downloadBtn}</span>
101
+ </button>
102
+ <p class="privacy-msg">{qrUI.privacyMsg}</p>
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ <script>
108
+ import QRCode from 'qrcode';
109
+ import { wifi, url, vcard } from './logic';
110
+ const state = { type: 'wifi' };
111
+
112
+ interface FormsMap {
113
+ wifi: HTMLElement | null;
114
+ url: HTMLElement | null;
115
+ vcard: HTMLElement | null;
116
+ }
117
+
118
+ function strVal(form: HTMLElement, sel: string): string {
119
+ const el = form.querySelector(sel) as HTMLInputElement | null;
120
+ return el ? el.value : '';
121
+ }
122
+
123
+ function boolVal(form: HTMLElement, sel: string): boolean {
124
+ const el = form.querySelector(sel) as HTMLInputElement | null;
125
+ return el ? el.checked : false;
126
+ }
127
+
128
+ function getWifiPayload(form: HTMLElement | null): string {
129
+ if (!form) return '';
130
+ return wifi(
131
+ strVal(form, '[name="ssid"]'),
132
+ strVal(form, '[name="password"]'),
133
+ strVal(form, '[name="encryption"]') || 'WPA',
134
+ boolVal(form, '[name="hidden"]')
135
+ );
136
+ }
137
+
138
+ function getUrlPayload(form: HTMLElement | null): string {
139
+ if (!form) return '';
140
+ return url(strVal(form, '[name="url"]'));
141
+ }
142
+
143
+ function getVcardPayload(form: HTMLElement | null): string {
144
+ if (!form) return '';
145
+ return vcard({
146
+ name: strVal(form, '[name="name"]'),
147
+ surname: strVal(form, '[name="surname"]'),
148
+ phone: strVal(form, '[name="phone"]'),
149
+ email: strVal(form, '[name="email"]'),
150
+ org: strVal(form, '[name="org"]'),
151
+ });
152
+ }
153
+
154
+ function getPayload(type: string, forms: FormsMap): string {
155
+ if (type === 'wifi') return getWifiPayload(forms.wifi);
156
+ if (type === 'url') return getUrlPayload(forms.url);
157
+ if (type === 'vcard') return getVcardPayload(forms.vcard);
158
+ return 'https://jjlmoya.es';
159
+ }
160
+
161
+ async function generate(type: string, forms: FormsMap, canvas: HTMLCanvasElement) {
162
+ const payload = getPayload(type, forms) || 'https://jjlmoya.es';
163
+ const isDark = document.documentElement.classList.contains('dark')
164
+ || document.documentElement.classList.contains('theme-dark');
165
+ try {
166
+ await QRCode.toCanvas(canvas, payload, {
167
+ width: 1024,
168
+ margin: 2,
169
+ color: { dark: isDark ? '#fff' : '#000', light: isDark ? '#020617' : '#fff' },
170
+ });
171
+ canvas.style.width = '';
172
+ canvas.style.height = '';
173
+ } catch {}
174
+ }
175
+
176
+ function switchTab(type: string, tabs: NodeListOf<Element>, forms: FormsMap) {
177
+ tabs.forEach(t => t.classList.toggle('active', (t as HTMLElement).dataset.tab === type));
178
+ Object.entries(forms).forEach(([k, f]) => f?.classList.toggle('hidden', k !== type));
179
+ }
180
+
181
+ function attachTabs(tabs: NodeListOf<Element>, forms: FormsMap, canvas: HTMLCanvasElement) {
182
+ tabs.forEach(tab => {
183
+ tab.addEventListener('click', () => {
184
+ state.type = (tab as HTMLElement).dataset.tab ?? 'wifi';
185
+ switchTab(state.type, tabs, forms);
186
+ void generate(state.type, forms, canvas);
187
+ });
188
+ });
189
+ }
190
+
191
+ function attachInputs(forms: FormsMap, canvas: HTMLCanvasElement) {
192
+ document.querySelectorAll('.qr-input').forEach(el => {
193
+ el.addEventListener('input', () => { void generate(state.type, forms, canvas); });
194
+ });
195
+ }
196
+
197
+ function attachDownload(btn: HTMLElement | null, canvas: HTMLCanvasElement) {
198
+ btn?.addEventListener('click', () => {
199
+ const a = document.createElement('a');
200
+ a.download = `qr-${state.type}.webp`;
201
+ a.href = canvas.toDataURL('image/webp');
202
+ a.click();
203
+ });
204
+ }
205
+
206
+ function attachPasswordToggle() {
207
+ const btn = document.getElementById('toggle-password');
208
+ const input = document.getElementById('wifi-password') as HTMLInputElement | null;
209
+ btn?.addEventListener('click', () => {
210
+ if (input) input.type = input.type === 'password' ? 'text' : 'password';
211
+ });
212
+ }
213
+
214
+ function init() {
215
+ const canvas = document.getElementById('qr-canvas') as HTMLCanvasElement | null;
216
+ if (!canvas) return;
217
+ const tabs = document.querySelectorAll('.tab-btn');
218
+ const forms: FormsMap = {
219
+ wifi: document.getElementById('form-wifi'),
220
+ url: document.getElementById('form-url'),
221
+ vcard: document.getElementById('form-vcard'),
222
+ };
223
+ attachTabs(tabs, forms, canvas);
224
+ attachInputs(forms, canvas);
225
+ attachDownload(document.getElementById('download-btn'), canvas);
226
+ attachPasswordToggle();
227
+ void generate(state.type, forms, canvas);
228
+ }
229
+
230
+ document.addEventListener('astro:page-load', init);
231
+ init();
232
+ </script>
233
+
234
+ <style>
235
+ .qr-wrapper {
236
+ --qr-p: #3b82f6;
237
+
238
+ width: 100%;
239
+ padding: 1rem 0;
240
+ }
241
+
242
+ .qr-card {
243
+ background: var(--bg-surface);
244
+ width: calc(100% - 24px);
245
+ max-width: 900px;
246
+ margin: 0 auto;
247
+ border-radius: 24px;
248
+ overflow: hidden;
249
+ display: flex;
250
+ flex-direction: column;
251
+ border: 1px solid var(--border-color);
252
+ color: var(--text-main);
253
+ }
254
+
255
+ @media (min-width: 768px) {
256
+ .qr-card {
257
+ flex-direction: row;
258
+ min-height: 520px;
259
+ }
260
+ }
261
+
262
+ .qr-left {
263
+ flex: 1;
264
+ padding: 32px;
265
+ border-bottom: 1px solid var(--border-color);
266
+ display: flex;
267
+ flex-direction: column;
268
+ gap: 24px;
269
+ }
270
+
271
+ @media (min-width: 768px) {
272
+ .qr-left {
273
+ border-bottom: none;
274
+ border-right: 1px solid var(--border-color);
275
+ }
276
+ }
277
+
278
+ .qr-right {
279
+ flex: 0 0 auto;
280
+ width: 100%;
281
+ background: var(--bg-muted);
282
+ padding: 40px 32px;
283
+ display: flex;
284
+ flex-direction: column;
285
+ align-items: center;
286
+ justify-content: center;
287
+ gap: 24px;
288
+ position: relative;
289
+ }
290
+
291
+ @media (min-width: 768px) {
292
+ .qr-right {
293
+ width: 340px;
294
+ }
295
+ }
296
+
297
+ .tab-bar {
298
+ display: flex;
299
+ background: var(--bg-muted);
300
+ padding: 4px;
301
+ border-radius: 14px;
302
+ gap: 2px;
303
+ }
304
+
305
+ .tab-btn {
306
+ flex: 1;
307
+ padding: 10px;
308
+ border: none;
309
+ background: transparent;
310
+ border-radius: 10px;
311
+ font-size: 11px;
312
+ font-weight: 800;
313
+ text-transform: uppercase;
314
+ letter-spacing: 0.12em;
315
+ color: var(--text-muted);
316
+ cursor: pointer;
317
+ transition: all 0.2s;
318
+ }
319
+
320
+ .tab-btn.active {
321
+ background: var(--bg-surface);
322
+ color: var(--text-main);
323
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
324
+ }
325
+
326
+ .qr-form {
327
+ display: flex;
328
+ flex-direction: column;
329
+ gap: 16px;
330
+ }
331
+
332
+ .qr-form.hidden {
333
+ display: none;
334
+ }
335
+
336
+ .field-group {
337
+ display: flex;
338
+ flex-direction: column;
339
+ gap: 6px;
340
+ }
341
+
342
+ .field-label {
343
+ font-size: 12px;
344
+ font-weight: 700;
345
+ color: var(--text-muted);
346
+ }
347
+
348
+ .field-input {
349
+ width: 100%;
350
+ padding: 10px 14px;
351
+ background: var(--bg-muted);
352
+ border: 1px solid var(--border-color);
353
+ border-radius: 10px;
354
+ font-size: 14px;
355
+ font-weight: 500;
356
+ color: var(--text-main);
357
+ transition: border-color 0.2s, box-shadow 0.2s;
358
+ box-sizing: border-box;
359
+ }
360
+
361
+ .field-input:focus {
362
+ outline: none;
363
+ border-color: var(--qr-p);
364
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
365
+ }
366
+
367
+ .input-row {
368
+ position: relative;
369
+ }
370
+
371
+ .input-row .field-input {
372
+ padding-right: 44px;
373
+ }
374
+
375
+ .eye-btn {
376
+ position: absolute;
377
+ right: 10px;
378
+ top: 50%;
379
+ transform: translateY(-50%);
380
+ background: none;
381
+ border: none;
382
+ cursor: pointer;
383
+ color: var(--text-muted);
384
+ padding: 4px;
385
+ display: flex;
386
+ }
387
+
388
+ .eye-btn svg {
389
+ width: 18px;
390
+ height: 18px;
391
+ }
392
+
393
+ .two-col {
394
+ display: grid;
395
+ grid-template-columns: 1fr 1fr;
396
+ gap: 12px;
397
+ }
398
+
399
+ .check-group {
400
+ justify-content: flex-end;
401
+ padding-bottom: 2px;
402
+ }
403
+
404
+ .check-label {
405
+ display: flex;
406
+ align-items: center;
407
+ gap: 8px;
408
+ cursor: pointer;
409
+ font-size: 13px;
410
+ color: var(--text-muted);
411
+ padding-top: 22px;
412
+ }
413
+
414
+ .check-input {
415
+ width: 16px;
416
+ height: 16px;
417
+ accent-color: var(--qr-p);
418
+ }
419
+
420
+ .qr-preview-area {
421
+ position: relative;
422
+ width: 100%;
423
+ max-width: 260px;
424
+ }
425
+
426
+ .canvas-glow {
427
+ position: relative;
428
+ }
429
+
430
+ .canvas-glow::before {
431
+ content: "";
432
+ position: absolute;
433
+ inset: -4px;
434
+ background: linear-gradient(135deg, #3b82f6, #06b6d4);
435
+ border-radius: 20px;
436
+ filter: blur(12px);
437
+ opacity: 0.3;
438
+ transition: opacity 0.3s;
439
+ }
440
+
441
+ .canvas-glow:hover::before {
442
+ opacity: 0.55;
443
+ }
444
+
445
+ .canvas-box {
446
+ position: relative;
447
+ background: var(--bg-surface);
448
+ border: 1px solid var(--border-color);
449
+ border-radius: 16px;
450
+ padding: 16px;
451
+ display: flex;
452
+ align-items: center;
453
+ justify-content: center;
454
+ }
455
+
456
+ .canvas-box canvas {
457
+ width: 100%;
458
+ height: auto;
459
+ }
460
+
461
+ .download-btn {
462
+ display: flex;
463
+ align-items: center;
464
+ gap: 8px;
465
+ padding: 12px 24px;
466
+ background: var(--qr-p);
467
+ color: #fff;
468
+ border: none;
469
+ border-radius: 12px;
470
+ font-size: 13px;
471
+ font-weight: 700;
472
+ cursor: pointer;
473
+ transition: all 0.2s;
474
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
475
+ }
476
+
477
+ .download-btn:hover {
478
+ filter: brightness(1.1);
479
+ box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4);
480
+ transform: translateY(-1px);
481
+ }
482
+
483
+ .download-btn svg {
484
+ width: 18px;
485
+ height: 18px;
486
+ }
487
+
488
+ .privacy-msg {
489
+ font-size: 12px;
490
+ color: var(--text-muted);
491
+ text-align: center;
492
+ max-width: 220px;
493
+ line-height: 1.5;
494
+ }
495
+
496
+ .theme-dark .canvas-box {
497
+ background: #020617;
498
+ }
499
+ </style>