@jjlmoya/utils-travel 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 (58) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +185 -0
  3. package/src/category/i18n/es.ts +187 -0
  4. package/src/category/i18n/fr.ts +100 -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 +15 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +22 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +278 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +8 -0
  17. package/src/tests/locale_completeness.test.ts +21 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +8 -0
  20. package/src/tests/seo_length.test.ts +8 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/luggage-calculator/bibliography.astro +14 -0
  23. package/src/tool/luggage-calculator/component.astro +560 -0
  24. package/src/tool/luggage-calculator/i18n/en.ts +617 -0
  25. package/src/tool/luggage-calculator/i18n/es.ts +617 -0
  26. package/src/tool/luggage-calculator/i18n/fr.ts +549 -0
  27. package/src/tool/luggage-calculator/index.ts +53 -0
  28. package/src/tool/luggage-calculator/seo.astro +14 -0
  29. package/src/tool/mini-adventures/bibliography.astro +14 -0
  30. package/src/tool/mini-adventures/component.astro +665 -0
  31. package/src/tool/mini-adventures/i18n/en.ts +161 -0
  32. package/src/tool/mini-adventures/i18n/es.ts +286 -0
  33. package/src/tool/mini-adventures/i18n/fr.ts +144 -0
  34. package/src/tool/mini-adventures/index.ts +70 -0
  35. package/src/tool/mini-adventures/seo.astro +14 -0
  36. package/src/tool/optimal-routes/bibliography.astro +14 -0
  37. package/src/tool/optimal-routes/component.astro +439 -0
  38. package/src/tool/optimal-routes/i18n/en.ts +42 -0
  39. package/src/tool/optimal-routes/i18n/es.ts +52 -0
  40. package/src/tool/optimal-routes/index.ts +63 -0
  41. package/src/tool/optimal-routes/lib/RouteManager.ts +181 -0
  42. package/src/tool/optimal-routes/seo.astro +14 -0
  43. package/src/tool/suitcase-checklist/bibliography.astro +14 -0
  44. package/src/tool/suitcase-checklist/component.astro +710 -0
  45. package/src/tool/suitcase-checklist/i18n/en.ts +261 -0
  46. package/src/tool/suitcase-checklist/i18n/es.ts +261 -0
  47. package/src/tool/suitcase-checklist/i18n/fr.ts +259 -0
  48. package/src/tool/suitcase-checklist/index.ts +75 -0
  49. package/src/tool/suitcase-checklist/seo.astro +14 -0
  50. package/src/tool/tip-calculator/bibliography.astro +14 -0
  51. package/src/tool/tip-calculator/component.astro +683 -0
  52. package/src/tool/tip-calculator/i18n/en.ts +264 -0
  53. package/src/tool/tip-calculator/i18n/es.ts +264 -0
  54. package/src/tool/tip-calculator/i18n/fr.ts +264 -0
  55. package/src/tool/tip-calculator/index.ts +53 -0
  56. package/src/tool/tip-calculator/seo.astro +14 -0
  57. package/src/tools.ts +13 -0
  58. package/src/types.ts +73 -0
@@ -0,0 +1,560 @@
1
+ ---
2
+ import { Icon } from "astro-icon/components";
3
+
4
+ interface Airline {
5
+ name: string;
6
+ icon: string;
7
+ personal: string;
8
+ personalWeight: string;
9
+ cabin: string;
10
+ cabinWeight: string;
11
+ }
12
+
13
+ interface UIConfig {
14
+ title: string;
15
+ searchPlaceholder: string;
16
+ personalItemTitle: string;
17
+ cabinBagTitle: string;
18
+ noResults: string;
19
+ personalItemDetail: string;
20
+ cabinBagDetail: string;
21
+ weightLabel: string;
22
+ modalNoteTitle: string;
23
+ modalNoteText: string;
24
+ airlines?: Airline[];
25
+ }
26
+
27
+ interface Props {
28
+ ui: UIConfig;
29
+ }
30
+
31
+ const { ui } = Astro.props;
32
+ const airlinesToMap = ui?.airlines ?? [];
33
+ ---
34
+
35
+ <div class="luggage-calculator">
36
+ <div class="lc-card">
37
+ <div class="lc-header">
38
+ <h2 class="lc-title">{ui.title}</h2>
39
+ <div class="lc-badge">Ver. 2026</div>
40
+ </div>
41
+
42
+ <div class="search-section">
43
+ <div class="search-container">
44
+ <Icon name="mdi:magnify" class="search-icon" />
45
+ <input
46
+ type="text"
47
+ id="airline-search"
48
+ placeholder={ui.searchPlaceholder}
49
+ class="search-input"
50
+ />
51
+ </div>
52
+ </div>
53
+
54
+ <div id="results-container" class="results-grid">
55
+ {
56
+ airlinesToMap.map((airline: Airline) => (
57
+ <div
58
+ data-airline={airline.name}
59
+ data-airline-json={JSON.stringify(airline)}
60
+ class="airline-card"
61
+ onclick={`window.openDetail('${airline.name.replace(/'/g, "\\'")}')`}
62
+ >
63
+ <div class="airline-header">
64
+ <Icon name={airline.icon} class="airline-icon" />
65
+ <span class="airline-name">{airline.name}</span>
66
+ </div>
67
+ <div class="luggage-types">
68
+ <div class="luggage-item">
69
+ <Icon name="mdi:bag-personal" class="luggage-icon" />
70
+ <div class="luggage-info">
71
+ <span class="luggage-label">{ui.personalItemTitle}</span>
72
+ <span class="luggage-measure">{airline.personal}</span>
73
+ <span class="luggage-weight">{airline.personalWeight}</span>
74
+ </div>
75
+ </div>
76
+ <div class="luggage-item">
77
+ <Icon name="mdi:bag-suitcase" class="luggage-icon" />
78
+ <div class="luggage-info">
79
+ <span class="luggage-label">{ui.cabinBagTitle}</span>
80
+ <span class="luggage-measure">{airline.cabin}</span>
81
+ <span class="luggage-weight">{airline.cabinWeight}</span>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ ))
87
+ }
88
+ </div>
89
+
90
+ <div id="no-results" class="no-results" style="display: none;">
91
+ {ui.noResults}
92
+ </div>
93
+
94
+ <div id="detail-overlay" class="detail-overlay">
95
+ <div class="detail-modal">
96
+ <button id="close-detail" class="close-btn">
97
+ <Icon name="mdi:close" />
98
+ </button>
99
+ <div id="modal-content"></div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <script define:vars={{ ui }}>
106
+ const searchInput = document.getElementById("airline-search");
107
+ const resultsContainer = document.getElementById("results-container");
108
+ const noResults = document.getElementById("no-results");
109
+ const detailOverlay = document.getElementById("detail-overlay");
110
+ const closeDetail = document.getElementById("close-detail");
111
+ const modalContent = document.getElementById("modal-content");
112
+
113
+ if (searchInput) {
114
+ searchInput.addEventListener("input", (e) => {
115
+ const value = e.target.value.toLowerCase();
116
+ let found = 0;
117
+ const cards = document.querySelectorAll(".airline-card");
118
+ cards.forEach((card) => {
119
+ const name = (card.getAttribute("data-airline") ?? "").toLowerCase();
120
+ if (name.includes(value)) {
121
+ card.style.display = "block";
122
+ found++;
123
+ } else {
124
+ card.style.display = "none";
125
+ }
126
+ });
127
+
128
+ if (found === 0) {
129
+ if (noResults) noResults.style.display = "block";
130
+ if (resultsContainer) resultsContainer.style.display = "none";
131
+ } else {
132
+ if (noResults) noResults.style.display = "none";
133
+ if (resultsContainer) resultsContainer.style.display = "grid";
134
+ }
135
+ });
136
+ }
137
+
138
+ window.openDetail = (name) => {
139
+ const el = document.querySelector(`[data-airline="${name}"]`);
140
+ if (!el || !modalContent || !detailOverlay) return;
141
+ const airline = JSON.parse(el.getAttribute("data-airline-json") || "{}");
142
+ if (!airline.name) return;
143
+
144
+ modalContent.innerHTML = `
145
+ <h2 class="modal-title">${airline.name}</h2>
146
+ <div class="detail-props">
147
+ <div class="prop-card">
148
+ <span class="prop-label">${ui.personalItemDetail}</span>
149
+ <span class="prop-value">${airline.personal}</span>
150
+ <p class="luggage-weight-detail">${ui.weightLabel}: ${airline.personalWeight}</p>
151
+ </div>
152
+ <div class="prop-card">
153
+ <span class="prop-label">${ui.cabinBagDetail}</span>
154
+ <span class="prop-value">${airline.cabin}</span>
155
+ <p class="luggage-weight-detail">${ui.weightLabel}: ${airline.cabinWeight}</p>
156
+ </div>
157
+ </div>
158
+ <div class="modal-note">
159
+ <p class="modal-note-text">
160
+ <strong class="modal-note-strong">${ui.modalNoteTitle}</strong> ${ui.modalNoteText}
161
+ </p>
162
+ </div>
163
+ `;
164
+ detailOverlay.classList.add("active");
165
+ };
166
+
167
+ if (closeDetail && detailOverlay) {
168
+ closeDetail.addEventListener("click", () => {
169
+ detailOverlay.classList.remove("active");
170
+ });
171
+
172
+ detailOverlay.addEventListener("click", (e) => {
173
+ if (e.target === detailOverlay) {
174
+ detailOverlay.classList.remove("active");
175
+ }
176
+ });
177
+ }
178
+ </script>
179
+
180
+ <style>
181
+ .luggage-calculator {
182
+ --lc-primary: #3b82f6;
183
+ --lc-border: #e2e8f0;
184
+ --lc-text: #1e293b;
185
+ --lc-text-muted: #64748b;
186
+ --lc-card-bg: #fff;
187
+ --lc-input-bg: #f8fafc;
188
+ --lc-item-bg: #f1f5f9;
189
+ --lc-modal-bg: #fff;
190
+ --lc-note-bg: #eff6ff;
191
+ --lc-note-border: #dbeafe;
192
+ --lc-note-text: #1d4ed8;
193
+ --lc-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 10px 10px -5px rgba(0, 0, 0, 0.02);
194
+ }
195
+
196
+ :global(.theme-dark) .luggage-calculator {
197
+ --lc-border: #334155;
198
+ --lc-text: #f1f5f9;
199
+ --lc-text-muted: #94a3b8;
200
+ --lc-card-bg: #1e293b;
201
+ --lc-input-bg: #0f172a;
202
+ --lc-item-bg: #0f172a;
203
+ --lc-modal-bg: #1e293b;
204
+ --lc-note-bg: rgba(30, 58, 138, 0.3);
205
+ --lc-note-border: rgba(30, 58, 138, 0.6);
206
+ --lc-note-text: #bfdbfe;
207
+ --lc-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
208
+ --lc-primary: #60a5fa;
209
+ }
210
+
211
+ .lc-card {
212
+ background: var(--lc-card-bg);
213
+ border: 1px solid var(--lc-border);
214
+ border-radius: 2.5rem;
215
+ padding: 3rem;
216
+ box-shadow: var(--lc-shadow);
217
+ max-width: 900px;
218
+ margin: 2rem auto;
219
+ position: relative;
220
+ overflow: hidden;
221
+ }
222
+
223
+ .lc-header {
224
+ display: flex;
225
+ justify-content: space-between;
226
+ align-items: center;
227
+ margin-bottom: 2.5rem;
228
+ position: relative;
229
+ z-index: 2;
230
+ }
231
+
232
+ .lc-title {
233
+ font-size: 2rem;
234
+ font-weight: 900;
235
+ letter-spacing: -0.02em;
236
+ color: var(--lc-text);
237
+ margin: 0;
238
+ }
239
+
240
+ .lc-badge {
241
+ padding: 0.6rem 1.25rem;
242
+ background: var(--lc-primary);
243
+ color: white;
244
+ border-radius: 1rem;
245
+ font-size: 0.7rem;
246
+ font-weight: 800;
247
+ text-transform: uppercase;
248
+ letter-spacing: 0.1em;
249
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
250
+ }
251
+
252
+ .search-section {
253
+ margin-bottom: 2.5rem;
254
+ position: relative;
255
+ z-index: 2;
256
+ }
257
+
258
+ .search-container {
259
+ position: relative;
260
+ width: 100%;
261
+ }
262
+
263
+ .search-input {
264
+ width: 100%;
265
+ padding: 1.25rem 1.5rem 1.25rem 3.5rem;
266
+ background: var(--lc-input-bg);
267
+ border: 2px solid var(--lc-border);
268
+ border-radius: 1.5rem;
269
+ font-size: 1.05rem;
270
+ color: var(--lc-text);
271
+ transition: all 0.3s ease;
272
+ outline: none;
273
+ }
274
+
275
+ .search-input:focus {
276
+ border-color: var(--lc-primary);
277
+ background: var(--lc-card-bg);
278
+ box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
279
+ }
280
+
281
+ .search-icon {
282
+ position: absolute;
283
+ left: 1.5rem;
284
+ top: 50%;
285
+ transform: translateY(-50%);
286
+ color: var(--lc-text-muted);
287
+ width: 20px;
288
+ height: 20px;
289
+ }
290
+
291
+ .results-grid {
292
+ display: grid;
293
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
294
+ gap: 1.5rem;
295
+ position: relative;
296
+ z-index: 2;
297
+ }
298
+
299
+ .airline-card {
300
+ background: var(--lc-card-bg);
301
+ border: 1px solid var(--lc-border);
302
+ border-radius: 1.5rem;
303
+ padding: 1.5rem;
304
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
305
+ cursor: pointer;
306
+ display: flex;
307
+ flex-direction: column;
308
+ }
309
+
310
+ .airline-card:hover {
311
+ transform: scale(1.02);
312
+ border-color: var(--lc-primary);
313
+ box-shadow: 0 15px 30px -10px rgba(0, 0, 0, 0.1);
314
+ }
315
+
316
+ .airline-header {
317
+ display: flex;
318
+ align-items: center;
319
+ gap: 1rem;
320
+ margin-bottom: 1.25rem;
321
+ }
322
+
323
+ .airline-icon {
324
+ width: 28px;
325
+ height: 28px;
326
+ color: var(--lc-primary);
327
+ background: var(--lc-note-bg);
328
+ padding: 0.4rem;
329
+ border-radius: 0.75rem;
330
+ }
331
+
332
+ .airline-name {
333
+ font-size: 1.25rem;
334
+ font-weight: 800;
335
+ color: var(--lc-text);
336
+ }
337
+
338
+ .luggage-types {
339
+ display: flex;
340
+ flex-direction: column;
341
+ gap: 1rem;
342
+ }
343
+
344
+ .luggage-item {
345
+ padding: 1rem;
346
+ background: var(--lc-input-bg);
347
+ border-radius: 1.25rem;
348
+ display: flex;
349
+ align-items: center;
350
+ gap: 0.75rem;
351
+ border: 1px solid transparent;
352
+ transition: border-color 0.2s;
353
+ }
354
+
355
+ .airline-card:hover .luggage-item {
356
+ border-color: var(--lc-border);
357
+ }
358
+
359
+ .luggage-icon {
360
+ width: 28px;
361
+ height: 28px;
362
+ color: var(--lc-primary);
363
+ flex-shrink: 0;
364
+ }
365
+
366
+ .luggage-info {
367
+ display: flex;
368
+ flex-direction: column;
369
+ }
370
+
371
+ .luggage-label {
372
+ font-size: 0.7rem;
373
+ font-weight: 700;
374
+ color: var(--lc-text-muted);
375
+ text-transform: uppercase;
376
+ letter-spacing: 0.05em;
377
+ margin-bottom: 0.1rem;
378
+ }
379
+
380
+ .luggage-measure {
381
+ font-size: 1rem;
382
+ font-weight: 800;
383
+ color: var(--lc-text);
384
+ }
385
+
386
+ .luggage-weight {
387
+ font-size: 0.85rem;
388
+ color: var(--lc-primary);
389
+ font-weight: 700;
390
+ }
391
+
392
+ .no-results {
393
+ grid-column: 1 / -1;
394
+ text-align: center;
395
+ padding: 4rem;
396
+ color: var(--lc-text-muted);
397
+ font-size: 1.1rem;
398
+ font-weight: 600;
399
+ }
400
+
401
+ .detail-overlay {
402
+ position: fixed;
403
+ top: 0;
404
+ left: 0;
405
+ width: 100%;
406
+ height: 100%;
407
+ background: rgba(15, 23, 42, 0.8);
408
+ backdrop-filter: blur(12px);
409
+ display: flex;
410
+ align-items: center;
411
+ justify-content: center;
412
+ z-index: 1000;
413
+ opacity: 0;
414
+ pointer-events: none;
415
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
416
+ }
417
+
418
+ .detail-overlay.active {
419
+ opacity: 1;
420
+ pointer-events: auto;
421
+ }
422
+
423
+ .detail-modal {
424
+ background: var(--lc-modal-bg);
425
+ width: 90%;
426
+ max-width: 550px;
427
+ border-radius: 3rem;
428
+ padding: 3rem;
429
+ position: relative;
430
+ transform: translateY(30px) scale(0.95);
431
+ transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
432
+ box-shadow: 0 30px 60px -12px rgba(0, 0, 0, 0.5);
433
+ border: 1px solid var(--lc-border);
434
+ }
435
+
436
+ .detail-overlay.active .detail-modal {
437
+ transform: translateY(0) scale(1);
438
+ }
439
+
440
+ .close-btn {
441
+ position: absolute;
442
+ top: 2rem;
443
+ right: 2rem;
444
+ background: var(--lc-input-bg);
445
+ border: none;
446
+ width: 44px;
447
+ height: 44px;
448
+ border-radius: 50%;
449
+ cursor: pointer;
450
+ display: flex;
451
+ align-items: center;
452
+ justify-content: center;
453
+ color: var(--lc-text);
454
+ transition: all 0.3s;
455
+ }
456
+
457
+ .close-btn:hover {
458
+ background: var(--lc-primary);
459
+ color: white;
460
+ transform: rotate(90deg);
461
+ }
462
+
463
+ .modal-title {
464
+ font-size: 2.25rem;
465
+ font-weight: 900;
466
+ margin-bottom: 2rem;
467
+ text-align: center;
468
+ letter-spacing: -0.03em;
469
+ }
470
+
471
+ .detail-props {
472
+ display: grid;
473
+ grid-template-columns: 1fr 1fr;
474
+ gap: 1.5rem;
475
+ }
476
+
477
+ .prop-card {
478
+ background: var(--lc-input-bg);
479
+ padding: 1.5rem;
480
+ border-radius: 2rem;
481
+ text-align: center;
482
+ border: 1px solid var(--lc-border);
483
+ }
484
+
485
+ .prop-label {
486
+ font-size: 0.85rem;
487
+ color: var(--lc-text-muted);
488
+ font-weight: 700;
489
+ text-transform: uppercase;
490
+ margin-bottom: 0.75rem;
491
+ display: block;
492
+ }
493
+
494
+ .prop-value {
495
+ font-size: 1.5rem;
496
+ font-weight: 900;
497
+ color: var(--lc-text);
498
+ }
499
+
500
+ .luggage-weight-detail {
501
+ font-size: 0.95rem;
502
+ margin-top: 0.75rem;
503
+ color: var(--lc-primary);
504
+ font-weight: 800;
505
+ }
506
+
507
+ .modal-note {
508
+ margin-top: 2rem;
509
+ padding: 1.5rem;
510
+ background: var(--lc-note-bg);
511
+ border-radius: 1.5rem;
512
+ border: 1px solid var(--lc-note-border);
513
+ }
514
+
515
+ .modal-note-text {
516
+ font-size: 0.95rem;
517
+ color: var(--lc-note-text);
518
+ line-height: 1.6;
519
+ margin: 0;
520
+ }
521
+
522
+ .modal-note-strong {
523
+ font-weight: 900;
524
+ text-transform: uppercase;
525
+ font-size: 0.8rem;
526
+ display: block;
527
+ margin-bottom: 0.4rem;
528
+ }
529
+
530
+ @media (max-width: 640px) {
531
+ .lc-card {
532
+ padding: 2rem 1.5rem;
533
+ border-radius: 2rem;
534
+ margin: 1rem;
535
+ }
536
+
537
+ .lc-header {
538
+ flex-direction: column;
539
+ align-items: flex-start;
540
+ gap: 1rem;
541
+ }
542
+
543
+ .lc-title {
544
+ font-size: 1.5rem;
545
+ }
546
+
547
+ .results-grid {
548
+ grid-template-columns: 1fr;
549
+ }
550
+
551
+ .detail-props {
552
+ grid-template-columns: 1fr;
553
+ }
554
+
555
+ .detail-modal {
556
+ padding: 2rem;
557
+ border-radius: 2rem;
558
+ }
559
+ }
560
+ </style>