@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,181 @@
1
+ export interface RoutePoint {
2
+ id: number;
3
+ lat: number;
4
+ lng: number;
5
+ marker: unknown;
6
+ name: string;
7
+ address?: string;
8
+ }
9
+
10
+ export class RouteManager extends EventTarget {
11
+ private map: unknown = null;
12
+ private points: RoutePoint[] = [];
13
+ private routeLine: unknown = null;
14
+ private L: unknown = null;
15
+
16
+ constructor(L: unknown) {
17
+ super();
18
+ this.L = L;
19
+ }
20
+
21
+ initMap(elementId: string) {
22
+ const el = document.getElementById(elementId);
23
+ if (!el || !this.L) return;
24
+
25
+ this.map = this.L.map(elementId).setView([40.416775, -3.70379], 6);
26
+
27
+ this.L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png", {
28
+ attribution: '© OpenStreetMap © CARTO',
29
+ subdomains: "abcd",
30
+ maxZoom: 20,
31
+ }).addTo(this.map);
32
+
33
+ this.map.on("click", (e: unknown) => {
34
+ this.addPoint(e.latlng.lat, e.latlng.lng);
35
+ });
36
+
37
+ if ("geolocation" in navigator) {
38
+ navigator.geolocation.getCurrentPosition((pos) => {
39
+ this.map?.setView([pos.coords.latitude, pos.coords.longitude], 13);
40
+ });
41
+ }
42
+ }
43
+
44
+ private getNumberedIcon(num: number) {
45
+ return this.L.divIcon({
46
+ className: "ma-number-icon-container",
47
+ html: `<div class="ma-number-icon">${num}</div>`,
48
+ iconSize: [30, 30],
49
+ iconAnchor: [15, 30]
50
+ });
51
+ }
52
+
53
+ async addPoint(lat: number, lng: number) {
54
+ if (!this.map) return;
55
+
56
+ const id = Date.now();
57
+ const index = this.points.length + 1;
58
+
59
+ const marker = this.L.marker([lat, lng], {
60
+ draggable: true,
61
+ icon: this.getNumberedIcon(index)
62
+ }).addTo(this.map);
63
+
64
+ const point: RoutePoint = { id, lat, lng, marker, name: `Punto ${index}`, address: "..." };
65
+ this.points.push(point);
66
+
67
+ this.notifyUpdate();
68
+
69
+ marker.on("dragend", () => {
70
+ const newPos = marker.getLatLng();
71
+ point.lat = newPos.lat;
72
+ point.lng = newPos.lng;
73
+ this.clearRoute();
74
+ this.updateAddress(point);
75
+ });
76
+
77
+ await this.updateAddress(point);
78
+ }
79
+
80
+ private async updateAddress(point: RoutePoint) {
81
+ try {
82
+ const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${point.lat}&lon=${point.lng}&zoom=18&addressdetails=1`, {
83
+ headers: { "User-Agent": "RutasApp/1.0" }
84
+ });
85
+ const data = await res.json();
86
+ point.address = data.display_name.split(",")[0] || "Punto";
87
+ point.name = point.address as string;
88
+ this.notifyUpdate();
89
+ } catch {
90
+ point.address = "Desconocido";
91
+ }
92
+ }
93
+
94
+ deletePoint(id: number) {
95
+ const idx = this.points.findIndex(p => p.id === id);
96
+ if (idx !== -1) {
97
+ this.map?.removeLayer(this.points[idx].marker);
98
+ this.points.splice(idx, 1);
99
+ this.updateAllMarkers();
100
+ this.clearRoute();
101
+ this.notifyUpdate();
102
+ }
103
+ }
104
+
105
+ private updateAllMarkers() {
106
+ this.points.forEach((p, i) => {
107
+ p.marker.setIcon(this.getNumberedIcon(i + 1));
108
+ });
109
+ }
110
+
111
+ private findLongestLegIndex(legs: unknown[]): number {
112
+ let maxIdx = -1;
113
+ let maxDist = -1;
114
+ (legs as Array<{ distance: number }>).forEach((l, i) => {
115
+ if (l.distance > maxDist) { maxDist = l.distance; maxIdx = i; }
116
+ });
117
+ return maxIdx;
118
+ }
119
+
120
+ clearAll() {
121
+ this.points.forEach(p => this.map?.removeLayer(p.marker));
122
+ this.points = [];
123
+ this.clearRoute();
124
+ this.notifyUpdate();
125
+ }
126
+
127
+ private clearRoute() {
128
+ if (this.routeLine && this.map) {
129
+ this.map.removeLayer(this.routeLine);
130
+ this.routeLine = null;
131
+ }
132
+ }
133
+
134
+ async optimizeRoute() {
135
+ if (this.points.length < 2) return;
136
+ this.dispatchEvent(new CustomEvent("loading", { detail: true }));
137
+
138
+ try {
139
+ const coords = this.points.map(p => `${p.lng},${p.lat}`).join(";");
140
+ const res = await fetch(`https://router.project-osrm.org/trip/v1/driving/${coords}?source=any&roundtrip=true&geometries=geojson&overview=full`);
141
+ const data = await res.json();
142
+
143
+ if (data.code !== "Ok") throw new Error();
144
+
145
+ const waypoints = data.waypoints;
146
+ const loopPoints = waypoints.map((wp: unknown) => this.points[wp.waypoint_index]);
147
+
148
+ const legs = data.trips[0].legs;
149
+ const maxIdx = this.findLongestLegIndex(legs);
150
+ const startIdx = (maxIdx + 1) % loopPoints.length;
151
+ this.points = [...loopPoints.slice(startIdx), ...loopPoints.slice(0, startIdx)];
152
+
153
+ this.updateAllMarkers();
154
+ this.notifyUpdate();
155
+
156
+ const routeRes = await fetch(`https://router.project-osrm.org/route/v1/driving/${this.points.map(p => `${p.lng},${p.lat}`).join(";")}?geometries=geojson&overview=full`);
157
+ const routeData = await routeRes.json();
158
+
159
+ this.clearRoute();
160
+ this.routeLine = this.L.geoJSON(routeData.routes[0].geometry, {
161
+ style: { color: "#0891b2", weight: 5, opacity: 0.8 }
162
+ }).addTo(this.map);
163
+
164
+ this.map?.fitBounds(this.routeLine.getBounds(), { padding: [50, 50] });
165
+ this.dispatchEvent(new CustomEvent("done", { detail: routeData.routes[0] }));
166
+ } catch {
167
+ this.dispatchEvent(new CustomEvent("error"));
168
+ } finally {
169
+ this.dispatchEvent(new CustomEvent("loading", { detail: false }));
170
+ }
171
+ }
172
+
173
+ panToPoint(id: number) {
174
+ const p = this.points.find(p => p.id === id);
175
+ if (p && this.map) this.map.flyTo([p.lat, p.lng], 16);
176
+ }
177
+
178
+ private notifyUpdate() {
179
+ this.dispatchEvent(new CustomEvent("update", { detail: this.points }));
180
+ }
181
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { SEORenderer } from "@jjlmoya/utils-shared";
3
+ import { optimalRoutes } from "./index";
4
+ import type { KnownLocale } from "../../types";
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = "es" } = Astro.props;
11
+ const content = await optimalRoutes.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from "@jjlmoya/utils-shared";
3
+ import { suitcaseChecklist } from "./index";
4
+ import type { KnownLocale } from "../../types";
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = "es" } = Astro.props;
11
+ const content = await suitcaseChecklist.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}