@jjlmoya/utils-drones 1.31.0 → 1.33.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 (67) hide show
  1. package/package.json +1 -1
  2. package/src/category/i18n/de.ts +1 -1
  3. package/src/category/i18n/en.ts +2 -2
  4. package/src/category/i18n/es.ts +1 -1
  5. package/src/category/i18n/fr.ts +2 -2
  6. package/src/category/i18n/id.ts +1 -1
  7. package/src/category/i18n/it.ts +1 -1
  8. package/src/category/i18n/nl.ts +1 -1
  9. package/src/category/i18n/pl.ts +1 -1
  10. package/src/category/i18n/pt.ts +1 -1
  11. package/src/category/i18n/sv.ts +1 -1
  12. package/src/category/i18n/tr.ts +1 -1
  13. package/src/components/PreviewNavSidebar.astro +116 -116
  14. package/src/components/PreviewToolbar.astro +143 -143
  15. package/src/data.ts +1 -1
  16. package/src/env.d.ts +1 -1
  17. package/src/layouts/PreviewLayout.astro +118 -118
  18. package/src/pages/[locale].astro +251 -251
  19. package/src/pages/index.astro +4 -4
  20. package/src/tests/bibliography_wellformed_export.test.ts +46 -0
  21. package/src/tests/category_seo_quality.test.ts +76 -0
  22. package/src/tests/faq_count.test.ts +1 -1
  23. package/src/tests/mocks/astro_mock.js +1 -1
  24. package/src/tests/no_h1_in_components.test.ts +1 -1
  25. package/src/tests/seo_translation_completeness.test.ts +69 -0
  26. package/src/tests/seo_wellformed_export.test.ts +65 -0
  27. package/src/tests/tool_validation.test.ts +15 -15
  28. package/src/tool/antenna-length-calculator/bibliography.astro +6 -6
  29. package/src/tool/antenna-length-calculator/component.astro +329 -329
  30. package/src/tool/antenna-length-calculator/seo.astro +15 -15
  31. package/src/tool/drone-battery-c-rating-calculator/i18n/de.ts +192 -192
  32. package/src/tool/drone-battery-c-rating-calculator/i18n/en.ts +192 -192
  33. package/src/tool/drone-battery-c-rating-calculator/i18n/es.ts +192 -192
  34. package/src/tool/drone-battery-c-rating-calculator/i18n/id.ts +192 -192
  35. package/src/tool/drone-battery-c-rating-calculator/i18n/ja.ts +192 -192
  36. package/src/tool/drone-battery-c-rating-calculator/i18n/ko.ts +192 -192
  37. package/src/tool/drone-battery-c-rating-calculator/i18n/nl.ts +192 -192
  38. package/src/tool/drone-battery-c-rating-calculator/i18n/pl.ts +192 -192
  39. package/src/tool/drone-battery-c-rating-calculator/i18n/ru.ts +184 -184
  40. package/src/tool/drone-battery-c-rating-calculator/i18n/sv.ts +189 -189
  41. package/src/tool/drone-battery-c-rating-calculator/i18n/tr.ts +187 -187
  42. package/src/tool/drone-battery-c-rating-calculator/i18n/zh.ts +192 -192
  43. package/src/tool/drone-flight-time/component.astro +187 -187
  44. package/src/tool/drone-flight-time/seo.astro +15 -15
  45. package/src/tool/drone-motor-propeller-calculator/i18n/de.ts +1 -1
  46. package/src/tool/drone-motor-propeller-calculator/i18n/fr.ts +1 -1
  47. package/src/tool/drone-motor-propeller-calculator/i18n/id.ts +1 -1
  48. package/src/tool/drone-motor-propeller-calculator/i18n/it.ts +1 -1
  49. package/src/tool/drone-motor-propeller-calculator/i18n/ja.ts +1 -1
  50. package/src/tool/drone-motor-propeller-calculator/i18n/ko.ts +1 -1
  51. package/src/tool/drone-motor-propeller-calculator/i18n/nl.ts +1 -1
  52. package/src/tool/drone-motor-propeller-calculator/i18n/pl.ts +1 -1
  53. package/src/tool/drone-motor-propeller-calculator/i18n/pt.ts +1 -1
  54. package/src/tool/drone-motor-propeller-calculator/i18n/ru.ts +1 -1
  55. package/src/tool/drone-motor-propeller-calculator/i18n/sv.ts +1 -1
  56. package/src/tool/drone-motor-propeller-calculator/i18n/tr.ts +1 -1
  57. package/src/tool/drone-motor-propeller-calculator/i18n/zh.ts +2 -2
  58. package/src/tool/drone-motor-propeller-calculator/seo.astro +7 -4
  59. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/id.ts +1 -1
  60. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/it.ts +1 -1
  61. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/nl.ts +1 -1
  62. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/pt.ts +1 -1
  63. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/ru.ts +1 -1
  64. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/sv.ts +2 -2
  65. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/tr.ts +1 -1
  66. package/src/tool/gps-coordinates-converter/component.astro +275 -275
  67. package/src/tool/gps-coordinates-converter/seo.astro +15 -15
@@ -149,12 +149,12 @@ const seoSections: SEOSection[] = [
149
149
  },
150
150
  {
151
151
  type: 'paragraph',
152
- html: 'För drönare med TWR över 10 till 1 rekommenderas att aktivera Throttle PID Attenuation (TPA) i Betaflight för att eliminera vibrationer vid fullgas.',
152
+ html: 'För drönare med TWR över 10 till 1 rekommenderas att aktivera Throttle PID Attenuation (TPA) i Betaflight för att minska vibrationer vid fullgas. Använd förhållandet mellan dragkraft och vikt för att jämföra konfigurationer under samma förhållanden. Ta hänsyn till startvikt, spänning under belastning, verklig förbrukning och den reserv som behövs för din flygstil. Ett kontrollerat test visar hur modellen reagerar innan flygning i krävande miljöer.',
153
153
  },
154
154
  {
155
155
  type: 'tip',
156
156
  title: 'Tips för Betaflight TPA Inställning',
157
- html: 'Ställ in TPA brytpunkt på 1250 eller 1350 med en dämpning på 0.65 för att säkerställa perfekt stabilitet snabba raksträckor.',
157
+ html: 'Ställ in TPA-brytpunkten på 1250 eller 1350 med en dämpning på 0.65 och kontrollera resultatet med en kontrollerad provflygning.',
158
158
  },
159
159
  ];
160
160
 
@@ -149,7 +149,7 @@ const seoSections: SEOSection[] = [
149
149
  },
150
150
  {
151
151
  type: 'paragraph',
152
- html: 'TWR si 10 a 1 in üzerinde olan dronlar için tam gazdaki titreşimleri önlemek adına Betaflight Throttle PID Attenuation (TPA) özelliğinin etkinleştirilmesi önerilir.',
152
+ html: 'TWR si 10 a 1 in üzerinde olan dronlar için tam gazdaki titreşimleri önlemek adına Betaflight Throttle PID Attenuation (TPA) özelliğinin etkinleştirilmesi önerilir. İtki-ağırlık oranını yapılandırmaları karşılaştırmak için kullanın; kalkış ağırlığını, yük altındaki voltajı, gerçek tüketimi ve uçuş tarzınız için gereken payı da hesaba katın. Kontrollü bir test, zorlu koşullarda uçmadan önce modelin tepkisini doğrulamanıza yardımcı olur. Sonuçları farklı gaz seviyelerinde karşılaştırmak, kontrol ve uçuş süresini daha gerçekçi değerlendirmeyi sağlar.',
153
153
  },
154
154
  {
155
155
  type: 'tip',
@@ -1,275 +1,275 @@
1
- ---
2
- import GpsInputs from "./components/GpsInputs.astro";
3
- import GpsResults from "./components/GpsResults.astro";
4
- import GpsMap from "./components/GpsMap.astro";
5
- import GpsHistory from "./components/GpsHistory.astro";
6
-
7
- const { ui } = Astro.props;
8
- ---
9
-
10
- <div class="gps-converter-ui"
11
- data-ui-copied={ui.copied}
12
- data-ui-no-history={ui.noHistory}
13
- data-ui-load={ui.load}
14
- data-ui-delete={ui.delete}
15
- >
16
- <div class="tech-mega-card animate-in fade-in zoom-in duration-700">
17
- <div class="card-grid">
18
- <aside class="config-sidebar">
19
- <GpsInputs ui={ui} />
20
- <div class="divider"></div>
21
- <GpsHistory ui={ui} />
22
- </aside>
23
-
24
- <main class="main-display">
25
- <GpsResults ui={ui} />
26
- <div class="divider"></div>
27
- <GpsMap ui={ui} />
28
- </main>
29
- </div>
30
- </div>
31
- </div>
32
-
33
- <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
34
- <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin="" is:inline></script>
35
-
36
- <script is:inline>
37
- function getHemisphere(val, isLat) {
38
- if (val >= 0) return isLat ? 'N' : 'E';
39
- return isLat ? 'S' : 'W';
40
- }
41
- function decimalToGMS(d, isLat) {
42
- const h = getHemisphere(d, isLat);
43
- const absD = Math.abs(d);
44
- const g = Math.floor(absD);
45
- const m = Math.floor((absD - g) * 60);
46
- const s = ((absD - g) * 60 - m) * 60;
47
- return {g, m, s, h, mDec: (absD - g) * 60};
48
- }
49
- function formatGMS(obj) {
50
- return `${obj.g}° ${obj.m}' ${obj.s.toFixed(2)}" ${obj.h}`;
51
- }
52
- function formatDM(obj) {
53
- return `${obj.g}° ${obj.mDec.toFixed(3)}' ${obj.h}`;
54
- }
55
- function id(name) {
56
- return document.getElementById(name);
57
- }
58
- const state={el:{}, ui:{}, mode:'DD', map:null, L:window.L};
59
- function initElements() {
60
- state.el = {
61
- latDD:id('latDD'),lngDD:id('lngDD'),latG:id('latG'),latM:id('latM'),
62
- latS:id('latS'),latH:id('latH'),lngG:id('lngG'),lngM:id('lngM'),
63
- lngS:id('lngS'),lngH:id('lngH'),valDD:id('valDD'),valGMS:id('valGMS'),
64
- valDM:id('valDM'),valMaps:id('valMaps'),historyList:id('historyList'),
65
- modeDD:id('modeDD'),modeGMS:id('modeGMS'),ddInputs:id('dd-inputs'),
66
- gmsInputs:id('gms-inputs'),btnMyLocation:id('btnMyLocation')
67
- };
68
- }
69
- function getUIValue(attr, def) {
70
- const ct = document.querySelector('.gps-converter-ui');
71
- return ct?.dataset[attr] || def;
72
- }
73
- function initUI() {
74
- state.ui = {
75
- copied: getUIValue('uiCopied', 'Copied'),
76
- noHistory: getUIValue('uiNoHistory', 'No history'),
77
- load: getUIValue('uiLoad', 'Load'),
78
- delete: getUIValue('uiDelete', 'Delete')
79
- };
80
- }
81
- function initState() {
82
- initElements();
83
- initUI();
84
- }
85
- function setLatGMS(gms) {
86
- if (state.el.latG) state.el.latG.value = gms.g.toString();
87
- if (state.el.latM) state.el.latM.value = gms.m.toString();
88
- if (state.el.latS) state.el.latS.value = gms.s.toFixed(2);
89
- if (state.el.latH) state.el.latH.value = gms.h;
90
- }
91
- function setLngGMS(gms) {
92
- if (state.el.lngG) state.el.lngG.value = gms.g.toString();
93
- if (state.el.lngM) state.el.lngM.value = gms.m.toString();
94
- if (state.el.lngS) state.el.lngS.value = gms.s.toFixed(2);
95
- if (state.el.lngH) state.el.lngH.value = gms.h;
96
- }
97
- function updateGMSDisplay(lat, lng) {
98
- const lGMS = decimalToGMS(lat, true);
99
- const nGMS = decimalToGMS(lng, false);
100
- setLatGMS(lGMS);
101
- setLngGMS(nGMS);
102
- }
103
- function updateResultDisplay(lat, lng) {
104
- const lGMS = decimalToGMS(lat, true);
105
- const nGMS = decimalToGMS(lng, false);
106
- if (state.el.valDD) {
107
- state.el.valDD.textContent = `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
108
- }
109
- if (state.el.valGMS) {
110
- state.el.valGMS.textContent = `${formatGMS(lGMS)}, ${formatGMS(nGMS)}`;
111
- }
112
- if (state.el.valDM) {
113
- state.el.valDM.textContent = `${formatDM(lGMS)}, ${formatDM(nGMS)}`;
114
- }
115
- if (state.el.valMaps) {
116
- state.el.valMaps.textContent = `${lat.toFixed(6)},${lng.toFixed(6)}`;
117
- }
118
- }
119
- function setDDInputs(lat, lng) {
120
- if (state.el.latDD) state.el.latDD.value = lat.toFixed(6);
121
- if (state.el.lngDD) state.el.lngDD.value = lng.toFixed(6);
122
- }
123
- function updateFromDD(lat, lng, upd=true) {
124
- if (isNaN(lat) || isNaN(lng)) return;
125
- if (upd) setDDInputs(lat, lng);
126
- if (!upd && state.mode !== 'DD') return;
127
- updateGMSDisplay(lat, lng);
128
- updateResultDisplay(lat, lng);
129
- saveHistory(lat, lng);
130
- }
131
- function createHistoryItem(item) {
132
- return `<div class="history-item" data-id="${item.id}"><div class="hist-info" data-lat="${item.lat}" data-lng="${item.lng}">
133
- <span class="hist-coords">${item.lat}, ${item.lng}</span><span class="hist-time">${item.time}</span></div><div class="hist-actions">
134
- <button class="action-btn load-btn" data-lat="${item.lat}" data-lng="${item.lng}"><svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
135
- <button class="action-btn delete-btn" data-id="${item.id}"><svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button></div></div>`;
136
- }
137
- function setupHistoryListeners() {
138
- document.querySelectorAll('.load-btn').forEach(btn => {
139
- btn.addEventListener('click', (evt) => {
140
- evt.stopPropagation();
141
- const lat = btn.getAttribute('data-lat');
142
- const lng = btn.getAttribute('data-lng');
143
- if (lat && lng) updateFromDD(parseFloat(lat), parseFloat(lng));
144
- });
145
- });
146
- document.querySelectorAll('.delete-btn').forEach(btn => {
147
- btn.addEventListener('click', (evt) => {
148
- evt.stopPropagation();
149
- const itemId = btn.getAttribute('data-id');
150
- if (itemId) {
151
- let history = JSON.parse(localStorage.getItem('gps_history') || '[]');
152
- history = history.filter(it => it.id !== parseInt(itemId));
153
- localStorage.setItem('gps_history', JSON.stringify(history));
154
- renderHistory(history);
155
- }
156
- });
157
- });
158
- document.querySelectorAll('.hist-info').forEach(info => {
159
- info.addEventListener('click', () => {
160
- const lat = info.getAttribute('data-lat');
161
- const lng = info.getAttribute('data-lng');
162
- if (lat && lng) updateFromDD(parseFloat(lat), parseFloat(lng));
163
- });
164
- });
165
- }
166
- function renderHistory(history) {
167
- if (!state.el.historyList) return;
168
- if (history.length === 0) {
169
- state.el.historyList.innerHTML = `<div class="no-history">${state.ui.noHistory}</div>`;
170
- } else {
171
- state.el.historyList.innerHTML = history.map(createHistoryItem).join('');
172
- }
173
- setupHistoryListeners();
174
- }
175
- function saveHistory(lat, lng) {
176
- let history = JSON.parse(localStorage.getItem('gps_history') || '[]');
177
- const entry = {
178
- id: Date.now(),
179
- lat: lat.toFixed(5),
180
- lng: lng.toFixed(5),
181
- time: new Date().toLocaleTimeString()
182
- };
183
- if (history.length > 0 && history[0].lat === entry.lat && history[0].lng === entry.lng) return;
184
- history.unshift(entry);
185
- history = history.slice(0, 5);
186
- localStorage.setItem('gps_history', JSON.stringify(history));
187
- renderHistory(history);
188
- }
189
- function loadHistory() {
190
- const history = JSON.parse(localStorage.getItem('gps_history') || '[]');
191
- renderHistory(history);
192
- }
193
- function initMap() {
194
- const mapEl = id('gpsMap');
195
- if (!mapEl || !state.L) return;
196
- state.map = state.L.map('gpsMap').setView([40.4168, -3.7038], 13);
197
- state.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
198
- attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
199
- maxZoom: 19
200
- }).addTo(state.map);
201
- state.map.on('click', (evt) => updateFromDD(evt.latlng.lat, evt.latlng.lng));
202
- }
203
- function setupModeListeners() {
204
- state.el.modeDD?.addEventListener('click', () => {
205
- state.mode = 'DD';
206
- state.el.modeDD?.classList.add('active');
207
- state.el.modeGMS?.classList.remove('active');
208
- if (state.el.ddInputs) state.el.ddInputs.style.display = 'grid';
209
- if (state.el.gmsInputs) state.el.gmsInputs.style.display = 'none';
210
- });
211
- state.el.modeGMS?.addEventListener('click', () => {
212
- state.mode = 'GMS';
213
- state.el.modeGMS?.classList.add('active');
214
- state.el.modeDD?.classList.remove('active');
215
- if (state.el.ddInputs) state.el.ddInputs.style.display = 'none';
216
- if (state.el.gmsInputs) state.el.gmsInputs.style.display = 'flex';
217
- });
218
- }
219
- function setupInputListeners() {
220
- state.el.btnMyLocation?.addEventListener('click', () => {
221
- if ('geolocation' in navigator) {
222
- navigator.geolocation.getCurrentPosition(pos => {
223
- updateFromDD(pos.coords.latitude, pos.coords.longitude);
224
- });
225
- }
226
- });
227
- [state.el.latDD, state.el.lngDD].forEach(el => {
228
- el?.addEventListener('input', () => {
229
- if (state.mode === 'DD' && state.el.latDD && state.el.lngDD) {
230
- updateFromDD(parseFloat(state.el.latDD.value), parseFloat(state.el.lngDD.value), false);
231
- }
232
- });
233
- });
234
- }
235
- function setupCopyListeners() {
236
- document.querySelectorAll('.copy-btn').forEach(btn => {
237
- btn.addEventListener('click', () => {
238
- const targetId = btn.getAttribute('data-copy');
239
- const text = targetId ? id(targetId)?.textContent : null;
240
- if (text) {
241
- navigator.clipboard.writeText(text);
242
- const original = btn.innerHTML;
243
- btn.innerHTML = state.ui.copied;
244
- setTimeout(() => { btn.innerHTML = original; }, 2000);
245
- }
246
- });
247
- });
248
- id('clearHistory')?.addEventListener('click', () => {
249
- localStorage.removeItem('gps_history');
250
- loadHistory();
251
- });
252
- }
253
- function setupAll() {
254
- setupModeListeners();
255
- setupInputListeners();
256
- setupCopyListeners();
257
- setupHistoryListeners();
258
- }
259
- function checkLeaflet() {
260
- if (window.L) {
261
- state.L = window.L;
262
- initMap();
263
- setupAll();
264
- loadHistory();
265
- updateFromDD(40.4168, -3.7038);
266
- } else {
267
- setTimeout(checkLeaflet, 100);
268
- }
269
- }
270
- initState();
271
- checkLeaflet();
272
- </script>
273
-
274
-
275
-
1
+ ---
2
+ import GpsInputs from "./components/GpsInputs.astro";
3
+ import GpsResults from "./components/GpsResults.astro";
4
+ import GpsMap from "./components/GpsMap.astro";
5
+ import GpsHistory from "./components/GpsHistory.astro";
6
+
7
+ const { ui } = Astro.props;
8
+ ---
9
+
10
+ <div class="gps-converter-ui"
11
+ data-ui-copied={ui.copied}
12
+ data-ui-no-history={ui.noHistory}
13
+ data-ui-load={ui.load}
14
+ data-ui-delete={ui.delete}
15
+ >
16
+ <div class="tech-mega-card animate-in fade-in zoom-in duration-700">
17
+ <div class="card-grid">
18
+ <aside class="config-sidebar">
19
+ <GpsInputs ui={ui} />
20
+ <div class="divider"></div>
21
+ <GpsHistory ui={ui} />
22
+ </aside>
23
+
24
+ <main class="main-display">
25
+ <GpsResults ui={ui} />
26
+ <div class="divider"></div>
27
+ <GpsMap ui={ui} />
28
+ </main>
29
+ </div>
30
+ </div>
31
+ </div>
32
+
33
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
34
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin="" is:inline></script>
35
+
36
+ <script is:inline>
37
+ function getHemisphere(val, isLat) {
38
+ if (val >= 0) return isLat ? 'N' : 'E';
39
+ return isLat ? 'S' : 'W';
40
+ }
41
+ function decimalToGMS(d, isLat) {
42
+ const h = getHemisphere(d, isLat);
43
+ const absD = Math.abs(d);
44
+ const g = Math.floor(absD);
45
+ const m = Math.floor((absD - g) * 60);
46
+ const s = ((absD - g) * 60 - m) * 60;
47
+ return {g, m, s, h, mDec: (absD - g) * 60};
48
+ }
49
+ function formatGMS(obj) {
50
+ return `${obj.g}° ${obj.m}' ${obj.s.toFixed(2)}" ${obj.h}`;
51
+ }
52
+ function formatDM(obj) {
53
+ return `${obj.g}° ${obj.mDec.toFixed(3)}' ${obj.h}`;
54
+ }
55
+ function id(name) {
56
+ return document.getElementById(name);
57
+ }
58
+ const state={el:{}, ui:{}, mode:'DD', map:null, L:window.L};
59
+ function initElements() {
60
+ state.el = {
61
+ latDD:id('latDD'),lngDD:id('lngDD'),latG:id('latG'),latM:id('latM'),
62
+ latS:id('latS'),latH:id('latH'),lngG:id('lngG'),lngM:id('lngM'),
63
+ lngS:id('lngS'),lngH:id('lngH'),valDD:id('valDD'),valGMS:id('valGMS'),
64
+ valDM:id('valDM'),valMaps:id('valMaps'),historyList:id('historyList'),
65
+ modeDD:id('modeDD'),modeGMS:id('modeGMS'),ddInputs:id('dd-inputs'),
66
+ gmsInputs:id('gms-inputs'),btnMyLocation:id('btnMyLocation')
67
+ };
68
+ }
69
+ function getUIValue(attr, def) {
70
+ const ct = document.querySelector('.gps-converter-ui');
71
+ return ct?.dataset[attr] || def;
72
+ }
73
+ function initUI() {
74
+ state.ui = {
75
+ copied: getUIValue('uiCopied', 'Copied'),
76
+ noHistory: getUIValue('uiNoHistory', 'No history'),
77
+ load: getUIValue('uiLoad', 'Load'),
78
+ delete: getUIValue('uiDelete', 'Delete')
79
+ };
80
+ }
81
+ function initState() {
82
+ initElements();
83
+ initUI();
84
+ }
85
+ function setLatGMS(gms) {
86
+ if (state.el.latG) state.el.latG.value = gms.g.toString();
87
+ if (state.el.latM) state.el.latM.value = gms.m.toString();
88
+ if (state.el.latS) state.el.latS.value = gms.s.toFixed(2);
89
+ if (state.el.latH) state.el.latH.value = gms.h;
90
+ }
91
+ function setLngGMS(gms) {
92
+ if (state.el.lngG) state.el.lngG.value = gms.g.toString();
93
+ if (state.el.lngM) state.el.lngM.value = gms.m.toString();
94
+ if (state.el.lngS) state.el.lngS.value = gms.s.toFixed(2);
95
+ if (state.el.lngH) state.el.lngH.value = gms.h;
96
+ }
97
+ function updateGMSDisplay(lat, lng) {
98
+ const lGMS = decimalToGMS(lat, true);
99
+ const nGMS = decimalToGMS(lng, false);
100
+ setLatGMS(lGMS);
101
+ setLngGMS(nGMS);
102
+ }
103
+ function updateResultDisplay(lat, lng) {
104
+ const lGMS = decimalToGMS(lat, true);
105
+ const nGMS = decimalToGMS(lng, false);
106
+ if (state.el.valDD) {
107
+ state.el.valDD.textContent = `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
108
+ }
109
+ if (state.el.valGMS) {
110
+ state.el.valGMS.textContent = `${formatGMS(lGMS)}, ${formatGMS(nGMS)}`;
111
+ }
112
+ if (state.el.valDM) {
113
+ state.el.valDM.textContent = `${formatDM(lGMS)}, ${formatDM(nGMS)}`;
114
+ }
115
+ if (state.el.valMaps) {
116
+ state.el.valMaps.textContent = `${lat.toFixed(6)},${lng.toFixed(6)}`;
117
+ }
118
+ }
119
+ function setDDInputs(lat, lng) {
120
+ if (state.el.latDD) state.el.latDD.value = lat.toFixed(6);
121
+ if (state.el.lngDD) state.el.lngDD.value = lng.toFixed(6);
122
+ }
123
+ function updateFromDD(lat, lng, upd=true) {
124
+ if (isNaN(lat) || isNaN(lng)) return;
125
+ if (upd) setDDInputs(lat, lng);
126
+ if (!upd && state.mode !== 'DD') return;
127
+ updateGMSDisplay(lat, lng);
128
+ updateResultDisplay(lat, lng);
129
+ saveHistory(lat, lng);
130
+ }
131
+ function createHistoryItem(item) {
132
+ return `<div class="history-item" data-id="${item.id}"><div class="hist-info" data-lat="${item.lat}" data-lng="${item.lng}">
133
+ <span class="hist-coords">${item.lat}, ${item.lng}</span><span class="hist-time">${item.time}</span></div><div class="hist-actions">
134
+ <button class="action-btn load-btn" data-lat="${item.lat}" data-lng="${item.lng}"><svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
135
+ <button class="action-btn delete-btn" data-id="${item.id}"><svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button></div></div>`;
136
+ }
137
+ function setupHistoryListeners() {
138
+ document.querySelectorAll('.load-btn').forEach(btn => {
139
+ btn.addEventListener('click', (evt) => {
140
+ evt.stopPropagation();
141
+ const lat = btn.getAttribute('data-lat');
142
+ const lng = btn.getAttribute('data-lng');
143
+ if (lat && lng) updateFromDD(parseFloat(lat), parseFloat(lng));
144
+ });
145
+ });
146
+ document.querySelectorAll('.delete-btn').forEach(btn => {
147
+ btn.addEventListener('click', (evt) => {
148
+ evt.stopPropagation();
149
+ const itemId = btn.getAttribute('data-id');
150
+ if (itemId) {
151
+ let history = JSON.parse(localStorage.getItem('gps_history') || '[]');
152
+ history = history.filter(it => it.id !== parseInt(itemId));
153
+ localStorage.setItem('gps_history', JSON.stringify(history));
154
+ renderHistory(history);
155
+ }
156
+ });
157
+ });
158
+ document.querySelectorAll('.hist-info').forEach(info => {
159
+ info.addEventListener('click', () => {
160
+ const lat = info.getAttribute('data-lat');
161
+ const lng = info.getAttribute('data-lng');
162
+ if (lat && lng) updateFromDD(parseFloat(lat), parseFloat(lng));
163
+ });
164
+ });
165
+ }
166
+ function renderHistory(history) {
167
+ if (!state.el.historyList) return;
168
+ if (history.length === 0) {
169
+ state.el.historyList.innerHTML = `<div class="no-history">${state.ui.noHistory}</div>`;
170
+ } else {
171
+ state.el.historyList.innerHTML = history.map(createHistoryItem).join('');
172
+ }
173
+ setupHistoryListeners();
174
+ }
175
+ function saveHistory(lat, lng) {
176
+ let history = JSON.parse(localStorage.getItem('gps_history') || '[]');
177
+ const entry = {
178
+ id: Date.now(),
179
+ lat: lat.toFixed(5),
180
+ lng: lng.toFixed(5),
181
+ time: new Date().toLocaleTimeString()
182
+ };
183
+ if (history.length > 0 && history[0].lat === entry.lat && history[0].lng === entry.lng) return;
184
+ history.unshift(entry);
185
+ history = history.slice(0, 5);
186
+ localStorage.setItem('gps_history', JSON.stringify(history));
187
+ renderHistory(history);
188
+ }
189
+ function loadHistory() {
190
+ const history = JSON.parse(localStorage.getItem('gps_history') || '[]');
191
+ renderHistory(history);
192
+ }
193
+ function initMap() {
194
+ const mapEl = id('gpsMap');
195
+ if (!mapEl || !state.L) return;
196
+ state.map = state.L.map('gpsMap').setView([40.4168, -3.7038], 13);
197
+ state.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
198
+ attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
199
+ maxZoom: 19
200
+ }).addTo(state.map);
201
+ state.map.on('click', (evt) => updateFromDD(evt.latlng.lat, evt.latlng.lng));
202
+ }
203
+ function setupModeListeners() {
204
+ state.el.modeDD?.addEventListener('click', () => {
205
+ state.mode = 'DD';
206
+ state.el.modeDD?.classList.add('active');
207
+ state.el.modeGMS?.classList.remove('active');
208
+ if (state.el.ddInputs) state.el.ddInputs.style.display = 'grid';
209
+ if (state.el.gmsInputs) state.el.gmsInputs.style.display = 'none';
210
+ });
211
+ state.el.modeGMS?.addEventListener('click', () => {
212
+ state.mode = 'GMS';
213
+ state.el.modeGMS?.classList.add('active');
214
+ state.el.modeDD?.classList.remove('active');
215
+ if (state.el.ddInputs) state.el.ddInputs.style.display = 'none';
216
+ if (state.el.gmsInputs) state.el.gmsInputs.style.display = 'flex';
217
+ });
218
+ }
219
+ function setupInputListeners() {
220
+ state.el.btnMyLocation?.addEventListener('click', () => {
221
+ if ('geolocation' in navigator) {
222
+ navigator.geolocation.getCurrentPosition(pos => {
223
+ updateFromDD(pos.coords.latitude, pos.coords.longitude);
224
+ });
225
+ }
226
+ });
227
+ [state.el.latDD, state.el.lngDD].forEach(el => {
228
+ el?.addEventListener('input', () => {
229
+ if (state.mode === 'DD' && state.el.latDD && state.el.lngDD) {
230
+ updateFromDD(parseFloat(state.el.latDD.value), parseFloat(state.el.lngDD.value), false);
231
+ }
232
+ });
233
+ });
234
+ }
235
+ function setupCopyListeners() {
236
+ document.querySelectorAll('.copy-btn').forEach(btn => {
237
+ btn.addEventListener('click', () => {
238
+ const targetId = btn.getAttribute('data-copy');
239
+ const text = targetId ? id(targetId)?.textContent : null;
240
+ if (text) {
241
+ navigator.clipboard.writeText(text);
242
+ const original = btn.innerHTML;
243
+ btn.innerHTML = state.ui.copied;
244
+ setTimeout(() => { btn.innerHTML = original; }, 2000);
245
+ }
246
+ });
247
+ });
248
+ id('clearHistory')?.addEventListener('click', () => {
249
+ localStorage.removeItem('gps_history');
250
+ loadHistory();
251
+ });
252
+ }
253
+ function setupAll() {
254
+ setupModeListeners();
255
+ setupInputListeners();
256
+ setupCopyListeners();
257
+ setupHistoryListeners();
258
+ }
259
+ function checkLeaflet() {
260
+ if (window.L) {
261
+ state.L = window.L;
262
+ initMap();
263
+ setupAll();
264
+ loadHistory();
265
+ updateFromDD(40.4168, -3.7038);
266
+ } else {
267
+ setTimeout(checkLeaflet, 100);
268
+ }
269
+ }
270
+ initState();
271
+ checkLeaflet();
272
+ </script>
273
+
274
+
275
+
@@ -1,15 +1,15 @@
1
- ---
2
- import { SEORenderer } from '@jjlmoya/utils-shared';
3
- import { gpsCoordinatesConverter } 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 gpsCoordinatesConverter.i18n[locale]?.();
12
- if (!content) return null;
13
- ---
14
-
15
- {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { gpsCoordinatesConverter } 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 gpsCoordinatesConverter.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}