@jjlmoya/utils-tools 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 (134) hide show
  1. package/package.json +63 -0
  2. package/src/category/i18n/en.ts +172 -0
  3. package/src/category/i18n/es.ts +172 -0
  4. package/src/category/i18n/fr.ts +172 -0
  5. package/src/category/index.ts +23 -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 +90 -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/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +23 -0
  22. package/src/tests/title_quality.test.ts +56 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/date-diff-calculator/bibliography.astro +14 -0
  25. package/src/tool/date-diff-calculator/component.astro +370 -0
  26. package/src/tool/date-diff-calculator/i18n/en.ts +132 -0
  27. package/src/tool/date-diff-calculator/i18n/es.ts +132 -0
  28. package/src/tool/date-diff-calculator/i18n/fr.ts +132 -0
  29. package/src/tool/date-diff-calculator/index.ts +22 -0
  30. package/src/tool/date-diff-calculator/seo.astro +14 -0
  31. package/src/tool/date-diff-calculator/ui.ts +17 -0
  32. package/src/tool/drive-direct-link/bibliography.astro +14 -0
  33. package/src/tool/drive-direct-link/component.astro +280 -0
  34. package/src/tool/drive-direct-link/i18n/en.ts +118 -0
  35. package/src/tool/drive-direct-link/i18n/es.ts +118 -0
  36. package/src/tool/drive-direct-link/i18n/fr.ts +118 -0
  37. package/src/tool/drive-direct-link/index.ts +22 -0
  38. package/src/tool/drive-direct-link/seo.astro +14 -0
  39. package/src/tool/drive-direct-link/ui.ts +10 -0
  40. package/src/tool/email-list-cleaner/bibliography.astro +14 -0
  41. package/src/tool/email-list-cleaner/component.astro +375 -0
  42. package/src/tool/email-list-cleaner/i18n/en.ts +140 -0
  43. package/src/tool/email-list-cleaner/i18n/es.ts +140 -0
  44. package/src/tool/email-list-cleaner/i18n/fr.ts +140 -0
  45. package/src/tool/email-list-cleaner/index.ts +22 -0
  46. package/src/tool/email-list-cleaner/seo.astro +14 -0
  47. package/src/tool/email-list-cleaner/ui.ts +15 -0
  48. package/src/tool/env-badge-spain/bibliography.astro +14 -0
  49. package/src/tool/env-badge-spain/component.astro +303 -0
  50. package/src/tool/env-badge-spain/components/BadgeForm.astro +243 -0
  51. package/src/tool/env-badge-spain/components/BadgeResult.astro +151 -0
  52. package/src/tool/env-badge-spain/i18n/en.ts +153 -0
  53. package/src/tool/env-badge-spain/i18n/es.ts +153 -0
  54. package/src/tool/env-badge-spain/i18n/fr.ts +153 -0
  55. package/src/tool/env-badge-spain/index.ts +22 -0
  56. package/src/tool/env-badge-spain/seo.astro +14 -0
  57. package/src/tool/env-badge-spain/ui.ts +53 -0
  58. package/src/tool/morse-beacon/bibliography.astro +14 -0
  59. package/src/tool/morse-beacon/component.astro +534 -0
  60. package/src/tool/morse-beacon/i18n/en.ts +157 -0
  61. package/src/tool/morse-beacon/i18n/es.ts +157 -0
  62. package/src/tool/morse-beacon/i18n/fr.ts +157 -0
  63. package/src/tool/morse-beacon/index.ts +22 -0
  64. package/src/tool/morse-beacon/logic/MorseEngine.ts +124 -0
  65. package/src/tool/morse-beacon/seo.astro +14 -0
  66. package/src/tool/morse-beacon/ui.ts +18 -0
  67. package/src/tool/password-generator/bibliography.astro +14 -0
  68. package/src/tool/password-generator/component.astro +259 -0
  69. package/src/tool/password-generator/components/Config.astro +227 -0
  70. package/src/tool/password-generator/components/Display.astro +147 -0
  71. package/src/tool/password-generator/components/Strength.astro +70 -0
  72. package/src/tool/password-generator/i18n/en.ts +166 -0
  73. package/src/tool/password-generator/i18n/es.ts +166 -0
  74. package/src/tool/password-generator/i18n/fr.ts +166 -0
  75. package/src/tool/password-generator/index.ts +22 -0
  76. package/src/tool/password-generator/seo.astro +14 -0
  77. package/src/tool/password-generator/ui.ts +16 -0
  78. package/src/tool/routes/bibliography.astro +14 -0
  79. package/src/tool/routes/component.astro +543 -0
  80. package/src/tool/routes/i18n/en.ts +157 -0
  81. package/src/tool/routes/i18n/es.ts +157 -0
  82. package/src/tool/routes/i18n/fr.ts +157 -0
  83. package/src/tool/routes/index.ts +22 -0
  84. package/src/tool/routes/logic/GeocodingService.ts +60 -0
  85. package/src/tool/routes/logic/RouteManager.ts +192 -0
  86. package/src/tool/routes/logic/RouteService.ts +66 -0
  87. package/src/tool/routes/seo.astro +14 -0
  88. package/src/tool/routes/ui.ts +16 -0
  89. package/src/tool/rule-of-three/bibliography.astro +14 -0
  90. package/src/tool/rule-of-three/component.astro +369 -0
  91. package/src/tool/rule-of-three/i18n/en.ts +171 -0
  92. package/src/tool/rule-of-three/i18n/es.ts +171 -0
  93. package/src/tool/rule-of-three/i18n/fr.ts +171 -0
  94. package/src/tool/rule-of-three/index.ts +22 -0
  95. package/src/tool/rule-of-three/seo.astro +14 -0
  96. package/src/tool/rule-of-three/ui.ts +13 -0
  97. package/src/tool/seo-content-optimizer/bibliography.astro +14 -0
  98. package/src/tool/seo-content-optimizer/component.astro +552 -0
  99. package/src/tool/seo-content-optimizer/i18n/en.ts +136 -0
  100. package/src/tool/seo-content-optimizer/i18n/es.ts +136 -0
  101. package/src/tool/seo-content-optimizer/i18n/fr.ts +136 -0
  102. package/src/tool/seo-content-optimizer/index.ts +22 -0
  103. package/src/tool/seo-content-optimizer/seo.astro +14 -0
  104. package/src/tool/seo-content-optimizer/ui.ts +29 -0
  105. package/src/tool/speed-reader/bibliography.astro +14 -0
  106. package/src/tool/speed-reader/component.astro +586 -0
  107. package/src/tool/speed-reader/i18n/en.ts +152 -0
  108. package/src/tool/speed-reader/i18n/es.ts +152 -0
  109. package/src/tool/speed-reader/i18n/fr.ts +152 -0
  110. package/src/tool/speed-reader/index.ts +22 -0
  111. package/src/tool/speed-reader/logic/RSVPEngine.ts +106 -0
  112. package/src/tool/speed-reader/seo.astro +14 -0
  113. package/src/tool/speed-reader/ui.ts +14 -0
  114. package/src/tool/text-pixel-calculator/bibliography.astro +14 -0
  115. package/src/tool/text-pixel-calculator/component.astro +315 -0
  116. package/src/tool/text-pixel-calculator/components/Editor.astro +240 -0
  117. package/src/tool/text-pixel-calculator/components/Preview.astro +155 -0
  118. package/src/tool/text-pixel-calculator/components/Stats.astro +87 -0
  119. package/src/tool/text-pixel-calculator/i18n/en.ts +133 -0
  120. package/src/tool/text-pixel-calculator/i18n/es.ts +133 -0
  121. package/src/tool/text-pixel-calculator/i18n/fr.ts +133 -0
  122. package/src/tool/text-pixel-calculator/index.ts +22 -0
  123. package/src/tool/text-pixel-calculator/seo.astro +14 -0
  124. package/src/tool/text-pixel-calculator/ui.ts +15 -0
  125. package/src/tool/whatsapp-link/bibliography.astro +14 -0
  126. package/src/tool/whatsapp-link/component.astro +455 -0
  127. package/src/tool/whatsapp-link/i18n/en.ts +128 -0
  128. package/src/tool/whatsapp-link/i18n/es.ts +128 -0
  129. package/src/tool/whatsapp-link/i18n/fr.ts +128 -0
  130. package/src/tool/whatsapp-link/index.ts +22 -0
  131. package/src/tool/whatsapp-link/seo.astro +14 -0
  132. package/src/tool/whatsapp-link/ui.ts +15 -0
  133. package/src/tools.ts +15 -0
  134. package/src/types.ts +72 -0
@@ -0,0 +1,543 @@
1
+ ---
2
+ import type { KnownLocale } from '../../types';
3
+ import type { RoutesUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: KnownLocale;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ const t = (ui ?? {}) as RoutesUI;
12
+ ---
13
+
14
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
15
+ <script is:inline src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
16
+
17
+ <div class="rut-root" data-ui={JSON.stringify(t)}>
18
+ <div class="rut-layout">
19
+ <aside class="rut-sidebar">
20
+ <div class="rut-sidebar-header">
21
+ <h2 class="rut-sidebar-title">{t.titleSidebar}</h2>
22
+ <p class="rut-sidebar-desc">{t.descriptionSidebar}</p>
23
+ </div>
24
+
25
+ <div class="rut-list-wrapper">
26
+ <ul id="rut-points-list" class="rut-points-list">
27
+ <li id="rut-empty-state" class="rut-empty-state">
28
+ {t.emptyState}
29
+ </li>
30
+ </ul>
31
+ </div>
32
+
33
+ <div class="rut-actions">
34
+ <button id="rut-btn-optimize" class="rut-btn-primary" disabled>
35
+ <svg class="rut-btn-icon rut-icon-chart" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
36
+ <path d="M3 3v18h18"></path>
37
+ <path d="M18.7 8l-5.1 5.2-2.8-2.7L7 14.3"></path>
38
+ </svg>
39
+ <svg class="rut-btn-icon rut-icon-spin" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
40
+ <circle cx="12" cy="12" r="10" stroke-opacity="0.25"></circle>
41
+ <path d="M12 2a10 10 0 0 1 10 10" stroke-opacity="0.75"></path>
42
+ </svg>
43
+ <span class="rut-btn-text">{t.btnOptimize}</span>
44
+ </button>
45
+
46
+ <button id="rut-btn-clear" class="rut-btn-secondary">
47
+ {t.btnClear}
48
+ </button>
49
+ </div>
50
+
51
+ <div id="rut-stats-panel" class="rut-stats-panel rut-hidden">
52
+ <div class="rut-stats-label">{t.labelDistance}</div>
53
+ <div id="rut-stats-value" class="rut-stats-value">0 km</div>
54
+ </div>
55
+ </aside>
56
+
57
+ <div class="rut-map-area">
58
+ <div id="rut-map" class="rut-map"></div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <style>
64
+ .rut-root {
65
+ --rut-accent: #06b6d4;
66
+ --rut-accent-hover: #0891b2;
67
+ --rut-sidebar-bg: #f8fafc;
68
+ --rut-sidebar-border: #e2e8f0;
69
+ --rut-map-bg: #f1f5f9;
70
+ --rut-text-main: #1e293b;
71
+ --rut-text-muted: #64748b;
72
+ --rut-item-bg: #fff;
73
+ --rut-item-border: #f1f5f9;
74
+ --rut-item-hover-border: rgba(6, 182, 212, 0.5);
75
+ --rut-badge-bg: rgba(207, 250, 254, 1);
76
+ --rut-badge-border: #a5f3fc;
77
+ --rut-badge-text: #0e7490;
78
+ --rut-stats-bg: rgba(236, 254, 255, 1);
79
+ --rut-stats-border: #a5f3fc;
80
+ --rut-stats-label: #164e63;
81
+ --rut-stats-value: #0891b2;
82
+ --rut-btn-clear-bg: #fff;
83
+ --rut-btn-clear-text: #334155;
84
+ --rut-btn-clear-border: #e2e8f0;
85
+ --rut-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
86
+ }
87
+
88
+ :global(.theme-dark) .rut-root {
89
+ --rut-sidebar-bg: rgba(15, 23, 42, 0.5);
90
+ --rut-sidebar-border: #1e293b;
91
+ --rut-map-bg: #1e293b;
92
+ --rut-text-main: #f1f5f9;
93
+ --rut-text-muted: #94a3b8;
94
+ --rut-item-bg: #1e293b;
95
+ --rut-item-border: #334155;
96
+ --rut-badge-bg: rgba(8, 145, 178, 0.3);
97
+ --rut-badge-border: #164e63;
98
+ --rut-badge-text: #67e8f9;
99
+ --rut-stats-bg: rgba(8, 145, 178, 0.2);
100
+ --rut-stats-border: rgba(8, 145, 178, 0.3);
101
+ --rut-stats-label: #a5f3fc;
102
+ --rut-stats-value: #22d3ee;
103
+ --rut-btn-clear-bg: #1e293b;
104
+ --rut-btn-clear-text: #cbd5e1;
105
+ --rut-btn-clear-border: #334155;
106
+ --rut-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
107
+ }
108
+
109
+ .rut-layout {
110
+ display: flex;
111
+ flex-direction: column;
112
+ height: calc(100vh - 100px);
113
+ min-height: 500px;
114
+ max-height: 800px;
115
+ }
116
+
117
+ @media (min-width: 1024px) {
118
+ .rut-layout {
119
+ display: grid;
120
+ grid-template-columns: 320px 1fr;
121
+ flex-direction: unset;
122
+ }
123
+ }
124
+
125
+ .rut-sidebar {
126
+ display: flex;
127
+ flex-direction: column;
128
+ padding: 1.5rem;
129
+ background: var(--rut-sidebar-bg);
130
+ border-bottom: 1px solid var(--rut-sidebar-border);
131
+ overflow: hidden;
132
+ height: 50%;
133
+ }
134
+
135
+ @media (min-width: 1024px) {
136
+ .rut-sidebar {
137
+ height: 100%;
138
+ border-bottom: none;
139
+ border-right: 1px solid var(--rut-sidebar-border);
140
+ order: 1;
141
+ }
142
+ }
143
+
144
+ .rut-sidebar-header {
145
+ margin-bottom: 1.25rem;
146
+ }
147
+
148
+ .rut-sidebar-title {
149
+ font-size: 1.2rem;
150
+ font-weight: 700;
151
+ color: var(--rut-text-main);
152
+ margin: 0 0 0.375rem;
153
+ }
154
+
155
+ .rut-sidebar-desc {
156
+ font-size: 0.875rem;
157
+ color: var(--rut-text-muted);
158
+ margin: 0;
159
+ }
160
+
161
+ .rut-list-wrapper {
162
+ flex: 1;
163
+ overflow-y: auto;
164
+ min-height: 0;
165
+ margin-bottom: 1rem;
166
+ padding-right: 0.25rem;
167
+ }
168
+
169
+ .rut-list-wrapper::-webkit-scrollbar {
170
+ width: 6px;
171
+ }
172
+
173
+ .rut-list-wrapper::-webkit-scrollbar-track {
174
+ background: transparent;
175
+ }
176
+
177
+ .rut-list-wrapper::-webkit-scrollbar-thumb {
178
+ background-color: rgba(156, 163, 175, 0.5);
179
+ border-radius: 20px;
180
+ }
181
+
182
+ .rut-points-list {
183
+ list-style: none;
184
+ margin: 0;
185
+ padding: 0;
186
+ display: flex;
187
+ flex-direction: column;
188
+ gap: 0.5rem;
189
+ }
190
+
191
+ .rut-empty-state {
192
+ text-align: center;
193
+ padding: 2rem 1rem;
194
+ color: var(--rut-text-muted);
195
+ border: 2px dashed var(--rut-sidebar-border);
196
+ border-radius: 0.5rem;
197
+ font-size: 0.875rem;
198
+ }
199
+
200
+ .rut-actions {
201
+ margin-top: auto;
202
+ padding-top: 1rem;
203
+ border-top: 1px solid var(--rut-sidebar-border);
204
+ display: flex;
205
+ flex-direction: column;
206
+ gap: 0.75rem;
207
+ }
208
+
209
+ .rut-btn-primary {
210
+ width: 100%;
211
+ padding: 0.75rem 1rem;
212
+ background: var(--rut-accent-hover);
213
+ color: #fff;
214
+ border: none;
215
+ border-radius: 0.5rem;
216
+ font-size: 0.9rem;
217
+ font-weight: 600;
218
+ cursor: pointer;
219
+ display: flex;
220
+ align-items: center;
221
+ justify-content: center;
222
+ gap: 0.5rem;
223
+ transition: background 0.15s;
224
+ box-shadow: 0 4px 6px -1px rgba(8, 145, 178, 0.2);
225
+ }
226
+
227
+ .rut-btn-primary:hover:not(:disabled) {
228
+ background: var(--rut-accent);
229
+ }
230
+
231
+ .rut-btn-primary:disabled {
232
+ opacity: 0.5;
233
+ cursor: not-allowed;
234
+ }
235
+
236
+ .rut-btn-secondary {
237
+ width: 100%;
238
+ padding: 0.5rem 1rem;
239
+ background: var(--rut-btn-clear-bg);
240
+ color: var(--rut-btn-clear-text);
241
+ border: 1px solid var(--rut-btn-clear-border);
242
+ border-radius: 0.5rem;
243
+ font-size: 0.875rem;
244
+ font-weight: 500;
245
+ cursor: pointer;
246
+ transition: opacity 0.15s;
247
+ }
248
+
249
+ .rut-btn-secondary:hover {
250
+ opacity: 0.8;
251
+ }
252
+
253
+ .rut-btn-icon {
254
+ flex-shrink: 0;
255
+ }
256
+
257
+ .rut-icon-spin {
258
+ display: none;
259
+ animation: rut-spin 1s linear infinite;
260
+ }
261
+
262
+ @keyframes rut-spin {
263
+ to { transform: rotate(360deg); }
264
+ }
265
+
266
+ .rut-btn-primary[data-loading] .rut-icon-chart {
267
+ display: none;
268
+ }
269
+
270
+ .rut-btn-primary[data-loading] .rut-icon-spin {
271
+ display: block;
272
+ }
273
+
274
+ .rut-stats-panel {
275
+ margin-top: 1rem;
276
+ padding: 1rem;
277
+ background: var(--rut-stats-bg);
278
+ border: 1px solid var(--rut-stats-border);
279
+ border-radius: 0.5rem;
280
+ }
281
+
282
+ .rut-stats-label {
283
+ font-size: 0.875rem;
284
+ font-weight: 500;
285
+ color: var(--rut-stats-label);
286
+ }
287
+
288
+ .rut-stats-value {
289
+ font-size: 1.5rem;
290
+ font-weight: 700;
291
+ color: var(--rut-stats-value);
292
+ }
293
+
294
+ .rut-map-area {
295
+ position: relative;
296
+ height: 50%;
297
+ order: 0;
298
+ }
299
+
300
+ @media (min-width: 1024px) {
301
+ .rut-map-area {
302
+ height: 100%;
303
+ order: 2;
304
+ }
305
+ }
306
+
307
+ .rut-map {
308
+ width: 100%;
309
+ height: 100%;
310
+ background: var(--rut-map-bg);
311
+ }
312
+
313
+ :global(.rut-hidden) {
314
+ display: none;
315
+ }
316
+ </style>
317
+
318
+ <style is:global>
319
+ .rut-icon-container {
320
+ background: transparent;
321
+ border: none;
322
+ }
323
+
324
+ .rut-icon {
325
+ width: 30px;
326
+ height: 30px;
327
+ background-color: #0891b2;
328
+ color: #fff;
329
+ border: 2px solid #fff;
330
+ border-radius: 50%;
331
+ text-align: center;
332
+ line-height: 26px;
333
+ font-weight: 700;
334
+ font-size: 14px;
335
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
336
+ position: relative;
337
+ }
338
+
339
+ .rut-icon::after {
340
+ content: '';
341
+ position: absolute;
342
+ bottom: -6px;
343
+ left: 50%;
344
+ transform: translateX(-50%);
345
+ border-width: 6px 6px 0;
346
+ border-style: solid;
347
+ border-color: #0891b2 transparent transparent;
348
+ }
349
+
350
+ .rut-point-item {
351
+ display: flex;
352
+ align-items: center;
353
+ justify-content: space-between;
354
+ padding: 0.75rem;
355
+ background: var(--rut-item-bg);
356
+ border: 1px solid var(--rut-item-border);
357
+ border-radius: 0.5rem;
358
+ box-shadow: var(--rut-shadow);
359
+ cursor: pointer;
360
+ transition: border-color 0.15s;
361
+ }
362
+
363
+ .rut-point-item:hover {
364
+ border-color: var(--rut-item-hover-border);
365
+ }
366
+
367
+ .rut-point-badge {
368
+ flex-shrink: 0;
369
+ width: 1.5rem;
370
+ height: 1.5rem;
371
+ border-radius: 50%;
372
+ background: var(--rut-badge-bg);
373
+ color: var(--rut-badge-text);
374
+ border: 1px solid var(--rut-badge-border);
375
+ display: flex;
376
+ align-items: center;
377
+ justify-content: center;
378
+ font-size: 0.75rem;
379
+ font-weight: 700;
380
+ margin-right: 0.75rem;
381
+ }
382
+
383
+ .rut-point-address {
384
+ flex: 1;
385
+ font-size: 0.875rem;
386
+ font-weight: 500;
387
+ color: var(--rut-text-main);
388
+ white-space: nowrap;
389
+ overflow: hidden;
390
+ text-overflow: ellipsis;
391
+ }
392
+
393
+ .rut-point-delete {
394
+ flex-shrink: 0;
395
+ background: none;
396
+ border: none;
397
+ cursor: pointer;
398
+ padding: 0.25rem;
399
+ color: var(--rut-text-muted);
400
+ opacity: 0;
401
+ transition: color 0.15s, opacity 0.15s;
402
+ display: flex;
403
+ align-items: center;
404
+ }
405
+
406
+ .rut-point-item:hover .rut-point-delete {
407
+ opacity: 1;
408
+ }
409
+
410
+ .rut-point-delete:hover {
411
+ color: #ef4444;
412
+ }
413
+
414
+ .rut-popup {
415
+ text-align: center;
416
+ min-width: 140px;
417
+ }
418
+
419
+ .rut-popup-name {
420
+ display: block;
421
+ font-size: 0.875rem;
422
+ margin-bottom: 0.25rem;
423
+ }
424
+
425
+ .rut-popup-coords {
426
+ font-size: 0.75rem;
427
+ color: #64748b;
428
+ margin: 0 0 0.5rem;
429
+ }
430
+
431
+ .rut-popup-btn {
432
+ font-size: 0.75rem;
433
+ background: #ef4444;
434
+ color: #fff;
435
+ border: none;
436
+ padding: 0.25rem 0.5rem;
437
+ border-radius: 0.25rem;
438
+ cursor: pointer;
439
+ transition: background 0.15s;
440
+ }
441
+
442
+ .rut-popup-btn:hover {
443
+ background: #dc2626;
444
+ }
445
+ </style>
446
+
447
+ <script>
448
+ import { GeocodingService } from './logic/GeocodingService';
449
+ import { RouteService } from './logic/RouteService';
450
+ import { RouteManager } from './logic/RouteManager';
451
+ import type { RoutePoint } from './logic/RouteManager';
452
+ import type { RoutesUI } from './ui';
453
+
454
+ const root = document.querySelector<HTMLElement>('.rut-root');
455
+ const ui = JSON.parse(root?.dataset.ui ?? '{}') as RoutesUI;
456
+
457
+ const geocoding = new GeocodingService({
458
+ addressUnknown: ui.errorAddress,
459
+ errorAddress: ui.errorAddress,
460
+ errorAddressName: ui.errorAddressName,
461
+ });
462
+
463
+ const routing = new RouteService();
464
+
465
+ const manager = new RouteManager(geocoding, routing, {
466
+ labelLoadingAddress: ui.labelLoadingAddress,
467
+ labelPoint: ui.labelPoint,
468
+ labelDeleteAria: ui.labelDeleteAria,
469
+ errorMinPoints: ui.errorMinPoints,
470
+ errorCalculate: ui.errorCalculate,
471
+ });
472
+
473
+ const els = {
474
+ list: document.getElementById('rut-points-list'),
475
+ empty: document.getElementById('rut-empty-state'),
476
+ btnOptimize: document.getElementById('rut-btn-optimize') as HTMLButtonElement | null,
477
+ btnClear: document.getElementById('rut-btn-clear'),
478
+ stats: document.getElementById('rut-stats-panel'),
479
+ distance: document.getElementById('rut-stats-value'),
480
+ };
481
+
482
+ manager.initMap('rut-map');
483
+
484
+ manager.addEventListener('update', ((e: CustomEvent<RoutePoint[]>) => {
485
+ renderList(e.detail);
486
+ updateOptimizeBtn(e.detail);
487
+ }) as EventListener);
488
+
489
+ manager.addEventListener('loading', ((e: CustomEvent<boolean>) => {
490
+ if (!els.btnOptimize) return;
491
+ const textEl = els.btnOptimize.querySelector<HTMLSpanElement>('.rut-btn-text');
492
+ if (textEl) textEl.textContent = e.detail ? ui.btnCalculating : ui.btnOptimize;
493
+ els.btnOptimize.disabled = e.detail;
494
+ if (e.detail) els.btnOptimize.setAttribute('data-loading', '');
495
+ else els.btnOptimize.removeAttribute('data-loading');
496
+ }) as EventListener);
497
+
498
+ manager.addEventListener('routeCalculated', ((e: CustomEvent<{ distance: number }>) => {
499
+ if (!els.stats || !els.distance) return;
500
+ els.stats.classList.remove('rut-hidden');
501
+ els.distance.textContent = `${(e.detail.distance / 1000).toFixed(2)} km`;
502
+ }) as EventListener);
503
+
504
+ manager.addEventListener('routeCleared', () => {
505
+ els.stats?.classList.add('rut-hidden');
506
+ });
507
+
508
+ manager.addEventListener('error', ((e: CustomEvent<string>) => {
509
+ alert(e.detail);
510
+ }) as EventListener);
511
+
512
+ els.btnOptimize?.addEventListener('click', () => { manager.optimizeRoute(); });
513
+ els.btnClear?.addEventListener('click', () => { manager.clearAll(); });
514
+
515
+ function renderList(points: RoutePoint[]) {
516
+ if (!els.list || !els.empty) return;
517
+ if (points.length === 0) {
518
+ els.empty.style.display = 'block';
519
+ els.list.innerHTML = '';
520
+ els.list.appendChild(els.empty);
521
+ return;
522
+ }
523
+ els.empty.style.display = 'none';
524
+ els.list.innerHTML = '';
525
+ points.forEach((point, index) => { els.list?.appendChild(createPointItem(point, index)); });
526
+ }
527
+
528
+ function createPointItem(point: RoutePoint, index: number): HTMLLIElement {
529
+ const li = document.createElement('li');
530
+ li.className = 'rut-point-item';
531
+ li.innerHTML = `<span class="rut-point-badge">${index + 1}</span><span class="rut-point-address" title="${point.address ?? point.name}">${point.address ?? point.name}</span><button class="rut-point-delete" aria-label="${ui.labelDeleteAria}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg></button>`;
532
+ li.querySelector('.rut-point-delete')?.addEventListener('click', (e) => {
533
+ e.stopPropagation();
534
+ manager.deletePoint(point.id);
535
+ });
536
+ li.addEventListener('click', () => { manager.panToPoint(point.id); });
537
+ return li;
538
+ }
539
+
540
+ function updateOptimizeBtn(points: RoutePoint[]) {
541
+ if (els.btnOptimize) els.btnOptimize.disabled = points.length < 2;
542
+ }
543
+ </script>
@@ -0,0 +1,157 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
3
+ import type { RoutesUI } from '../ui';
4
+
5
+ const faqData = [
6
+ {
7
+ question: 'What optimization algorithm does it use?',
8
+ answer: 'It uses an advanced resolution of the Travelling Salesman Problem (TSP). The algorithm analyzes all stops and determines the sequential order that minimizes the total distance travelled, saving time and fuel.',
9
+ },
10
+ {
11
+ question: 'Is it safe to share my location?',
12
+ answer: 'Yes. The tool processes geographic data locally in your browser. We do not store your routes, stops, or location history on our servers.',
13
+ },
14
+ {
15
+ question: 'How many stops can I optimize at once?',
16
+ answer: 'Our free version allows you to optimize up to 10 stops instantly. For larger professional routes, the system is optimized to maintain high performance without blocking the browser.',
17
+ },
18
+ {
19
+ question: 'Can I use the route directly in Google Maps?',
20
+ answer: 'Absolutely! Once the route is optimized, the tool generates a navigation link compatible with Google Maps so you can start the journey directly from your phone.',
21
+ },
22
+ ];
23
+
24
+ const howToData = [
25
+ {
26
+ name: 'Add starting point',
27
+ text: 'Type the starting address or click on the map to set where your route begins.',
28
+ },
29
+ {
30
+ name: 'Enter destination stops',
31
+ text: 'Add all the locations you need to visit. The order does not matter, as the system will reorder them for you.',
32
+ },
33
+ {
34
+ name: 'Optimize the sequence',
35
+ text: 'Click the optimize button. The algorithm will calculate in seconds the most efficient order to cover all stops.',
36
+ },
37
+ {
38
+ name: 'Open in navigator',
39
+ text: 'Use the navigation button to take the optimized route to your favourite maps application.',
40
+ },
41
+ ];
42
+
43
+ const faqSchema: WithContext<FAQPage> = {
44
+ '@context': 'https://schema.org',
45
+ '@type': 'FAQPage',
46
+ mainEntity: faqData.map((item) => ({
47
+ '@type': 'Question',
48
+ name: item.question,
49
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
50
+ })),
51
+ };
52
+
53
+ const howToSchema: WithContext<HowTo> = {
54
+ '@context': 'https://schema.org',
55
+ '@type': 'HowTo',
56
+ name: 'How to optimize a route with multiple stops',
57
+ step: howToData.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })),
58
+ };
59
+
60
+ const appSchema: WithContext<SoftwareApplication> = {
61
+ '@context': 'https://schema.org',
62
+ '@type': 'SoftwareApplication',
63
+ name: 'Optimal Route Calculator',
64
+ applicationCategory: 'UtilitiesApplication',
65
+ operatingSystem: 'Web',
66
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
67
+ description: 'Optimize your delivery or travel routes for free. Automatically reorder stops to find the shortest and most efficient path.',
68
+ };
69
+
70
+ const ui: RoutesUI = {
71
+ titleSidebar: 'Route Points',
72
+ descriptionSidebar: 'Click on the map to add stops. The first point is the start.',
73
+ emptyState: 'No points added',
74
+ btnOptimize: 'Calculate Optimal Route',
75
+ btnCalculating: 'Calculating...',
76
+ btnClear: 'Clear All',
77
+ labelDistance: 'Estimated Total Distance',
78
+ labelDeleteAria: 'Delete',
79
+ labelLoadingAddress: 'Loading address...',
80
+ labelPoint: 'Point',
81
+ errorMinPoints: 'At least two points are needed to calculate a route.',
82
+ errorCalculate: 'Error calculating the route.',
83
+ errorAddress: 'Error fetching address',
84
+ errorAddressName: 'Unknown point',
85
+ };
86
+
87
+ export const content: ToolLocaleContent<RoutesUI> = {
88
+ slug: 'optimal-routes',
89
+ title: 'Free Optimal Route Calculator',
90
+ description: 'Optimize your delivery or travel routes for free. Our tool automatically reorders your stops to find the shortest and most efficient path.',
91
+ ui,
92
+ faqTitle: 'Frequently Asked Questions',
93
+ faq: faqData,
94
+ howTo: howToData,
95
+ bibliographyTitle: 'Technologies and Sources',
96
+ bibliography: [
97
+ { name: 'Leaflet — Open-source interactive maps library', url: 'https://leafletjs.com/' },
98
+ { name: 'OpenStreetMap — Open cartographic data', url: 'https://www.openstreetmap.org/' },
99
+ { name: 'CARTO — Voyager map tiles', url: 'https://carto.com/basemaps/' },
100
+ { name: 'Nominatim — Reverse geocoding service (OpenStreetMap)', url: 'https://nominatim.org/' },
101
+ { name: 'OSRM — Open Source Routing Machine (route optimization)', url: 'http://project-osrm.org/' },
102
+ ],
103
+ schemas: [faqSchema, howToSchema, appSchema],
104
+ seo: [
105
+ {
106
+ type: 'title',
107
+ text: 'Smart Route Optimiser: TSP for Deliveries and Travel',
108
+ level: 2,
109
+ },
110
+ {
111
+ type: 'paragraph',
112
+ html: 'The <strong>online route optimiser</strong> is a free tool that solves the classic <em>Travelling Salesman Problem (TSP)</em>. Add all your stops in any order and the algorithm will automatically calculate the most efficient sequence to minimise the total distance travelled.',
113
+ },
114
+ {
115
+ type: 'title',
116
+ text: 'How does the route optimisation algorithm work?',
117
+ level: 2,
118
+ },
119
+ {
120
+ type: 'paragraph',
121
+ html: 'The tool uses the <strong>OSRM (Open Source Routing Machine)</strong> API, a high-performance routing engine based on OpenStreetMap data. The process is: first it calculates the optimal circular route between all points (TSP algorithm), then determines the best cut point to convert it into a one-way linear journey, and finally draws the route on the map with the estimated total distance.',
122
+ },
123
+ {
124
+ type: 'title',
125
+ text: 'Use cases: deliveries, sales reps, and travel',
126
+ level: 2,
127
+ },
128
+ {
129
+ type: 'paragraph',
130
+ html: 'The route optimiser is ideal for <strong>independent delivery drivers</strong> who need to organise multiple daily deliveries, <strong>field sales representatives</strong> visiting clients in an area, or <strong>travellers</strong> wanting to visit several cities or landmarks in the most efficient order. The tool processes all information in the browser, without sharing your data with any proprietary server.',
131
+ },
132
+ {
133
+ type: 'stats',
134
+ columns: 2,
135
+ items: [
136
+ { value: 'TSP', label: 'Algorithm', icon: 'mdi:chart-line' },
137
+ { value: 'OSRM', label: 'Routing engine', icon: 'mdi:map-marker-path' },
138
+ { value: 'Local-First', label: 'Privacy', icon: 'mdi:lock-check' },
139
+ { value: 'Free', label: 'Cost', icon: 'mdi:currency-eur-off' },
140
+ ],
141
+ },
142
+ {
143
+ type: 'title',
144
+ text: 'Privacy and local processing',
145
+ level: 2,
146
+ },
147
+ {
148
+ type: 'paragraph',
149
+ html: 'All the tool logic runs directly in your browser. Your stop coordinates are only sent to the public OSRM and Nominatim APIs to calculate routes and get address names, but <strong>are not stored on any proprietary server</strong>. You can safely use the tool to plan business routes with sensitive information.',
150
+ },
151
+ {
152
+ type: 'tip',
153
+ title: 'Usage tip',
154
+ html: '<strong>For best results</strong>, add stops by clicking directly on the map rather than searching for addresses. Markers are draggable, so you can adjust the exact position of each point after placing it.',
155
+ },
156
+ ],
157
+ };