@jjlmoya/utils-drones 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 (56) hide show
  1. package/package.json +61 -0
  2. package/src/category/i18n/en.ts +107 -0
  3. package/src/category/i18n/es.ts +106 -0
  4. package/src/category/i18n/fr.ts +107 -0
  5. package/src/category/index.ts +15 -0
  6. package/src/category/seo.astro +92 -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 +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 +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/seo_section_types.test.ts +75 -0
  22. package/src/tests/tool_validation.test.ts +17 -0
  23. package/src/tool/antenna-length-calculator/AntennaLengthCalculator.css +684 -0
  24. package/src/tool/antenna-length-calculator/bibliography.astro +14 -0
  25. package/src/tool/antenna-length-calculator/component.astro +360 -0
  26. package/src/tool/antenna-length-calculator/i18n/en.ts +204 -0
  27. package/src/tool/antenna-length-calculator/i18n/es.ts +204 -0
  28. package/src/tool/antenna-length-calculator/i18n/fr.ts +204 -0
  29. package/src/tool/antenna-length-calculator/index.ts +27 -0
  30. package/src/tool/antenna-length-calculator/seo.astro +39 -0
  31. package/src/tool/drone-flight-time/FlightTimeCalculator.css +363 -0
  32. package/src/tool/drone-flight-time/bibliography.astro +14 -0
  33. package/src/tool/drone-flight-time/component.astro +262 -0
  34. package/src/tool/drone-flight-time/components/AutonomyChart.astro +13 -0
  35. package/src/tool/drone-flight-time/components/BatterySpecs.astro +46 -0
  36. package/src/tool/drone-flight-time/components/ConsumptionStats.astro +33 -0
  37. package/src/tool/drone-flight-time/components/FlightDashboard.astro +33 -0
  38. package/src/tool/drone-flight-time/i18n/en.ts +200 -0
  39. package/src/tool/drone-flight-time/i18n/es.ts +200 -0
  40. package/src/tool/drone-flight-time/i18n/fr.ts +200 -0
  41. package/src/tool/drone-flight-time/index.ts +27 -0
  42. package/src/tool/drone-flight-time/seo.astro +31 -0
  43. package/src/tool/gps-coordinates-converter/GpsCoordinatesConverter.css +310 -0
  44. package/src/tool/gps-coordinates-converter/bibliography.astro +14 -0
  45. package/src/tool/gps-coordinates-converter/component.astro +355 -0
  46. package/src/tool/gps-coordinates-converter/components/GpsHistory.astro +36 -0
  47. package/src/tool/gps-coordinates-converter/components/GpsInputs.astro +92 -0
  48. package/src/tool/gps-coordinates-converter/components/GpsMap.astro +18 -0
  49. package/src/tool/gps-coordinates-converter/components/GpsResults.astro +50 -0
  50. package/src/tool/gps-coordinates-converter/i18n/en.ts +201 -0
  51. package/src/tool/gps-coordinates-converter/i18n/es.ts +201 -0
  52. package/src/tool/gps-coordinates-converter/i18n/fr.ts +201 -0
  53. package/src/tool/gps-coordinates-converter/index.ts +27 -0
  54. package/src/tool/gps-coordinates-converter/seo.astro +39 -0
  55. package/src/tools.ts +16 -0
  56. package/src/types.ts +72 -0
@@ -0,0 +1,19 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type * as DATA from '../data';
3
+
4
+ const TOOLS: typeof DATA.dronesCategory[] = [];
5
+
6
+ describe('FAQ Content Validation', () => {
7
+ TOOLS.forEach((entry) => {
8
+ describe(`Tool: ${entry.icon}`, () => {
9
+ it('placeholder', () => {
10
+ expect(true).toBe(true);
11
+ });
12
+ });
13
+ });
14
+
15
+ it('no tools registered yet', () => {
16
+ expect(TOOLS.length).toBe(0);
17
+ });
18
+ });
19
+
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import type { ToolLocaleContent } from '../types';
4
+
5
+ describe('Locale Completeness Validation', () => {
6
+ ALL_TOOLS.forEach((tool) => {
7
+ describe(`Tool: ${tool.entry.id}`, () => {
8
+ Object.keys(tool.entry.i18n).forEach((locale) => {
9
+ describe(`Locale: ${locale}`, () => {
10
+ it('faqTitle should be defined when faq items exist', async () => {
11
+ const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
12
+ const content = (await loader?.()) as ToolLocaleContent;
13
+
14
+ if (content.faq.length > 0) {
15
+ expect(
16
+ content.faqTitle,
17
+ `Tool "${tool.entry.id}" locale "${locale}" has ${content.faq.length} FAQ items but is missing faqTitle`,
18
+ ).toBeTruthy();
19
+ }
20
+ });
21
+
22
+ it('bibliographyTitle should be defined when bibliography items exist', async () => {
23
+ const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
24
+ const content = (await loader?.()) as ToolLocaleContent;
25
+
26
+ if (content.bibliography.length > 0) {
27
+ expect(
28
+ content.bibliographyTitle,
29
+ `Tool "${tool.entry.id}" locale "${locale}" has ${content.bibliography.length} bibliography items but is missing bibliographyTitle`,
30
+ ).toBeTruthy();
31
+ }
32
+ });
33
+ });
34
+ });
35
+ });
36
+ });
37
+
38
+ it('all tools are registered', () => {
39
+ expect(ALL_TOOLS.length).toBeGreaterThan(0);
40
+ });
41
+ });
42
+
@@ -0,0 +1,2 @@
1
+ export default function () { return null; }
2
+
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readdirSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ const EXCLUDED_DIRS = ['node_modules', 'pages', 'layouts'];
6
+
7
+ function findAstroFiles(dir: string): string[] {
8
+ const files: string[] = [];
9
+ const entries = readdirSync(dir, { withFileTypes: true });
10
+
11
+ for (const entry of entries) {
12
+ const fullPath = join(dir, entry.name);
13
+ if (entry.isDirectory() && !EXCLUDED_DIRS.includes(entry.name)) {
14
+ files.push(...findAstroFiles(fullPath));
15
+ } else if (entry.isFile() && entry.name.endsWith('.astro')) {
16
+ files.push(fullPath);
17
+ }
18
+ }
19
+
20
+ return files;
21
+ }
22
+
23
+ function hasH1(content: string): boolean {
24
+ return /<h1[\s>]/i.test(content);
25
+ }
26
+
27
+ const srcDir = join(process.cwd(), 'src');
28
+ const astroFiles = findAstroFiles(srcDir);
29
+
30
+ describe('No H1 in Components', () => {
31
+ if (astroFiles.length === 0) {
32
+ it('no astro components found', () => {
33
+ expect(true).toBe(true);
34
+ });
35
+ }
36
+
37
+ astroFiles.forEach((file) => {
38
+ const relativePath = file.replace(process.cwd(), '');
39
+ it(`${relativePath} should not contain <h1>`, () => {
40
+ const content = readFileSync(file, 'utf-8');
41
+ expect(
42
+ hasH1(content),
43
+ `File "${relativePath}" contains a <h1> element. Use <h2> or lower inside components — h1 belongs to the page layout.`,
44
+ ).toBe(false);
45
+ });
46
+ });
47
+ });
48
+
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as DATA from '../data';
3
+
4
+ const ENTRIES = [
5
+ { id: 'dronesCategory', i18n: DATA.dronesCategory.i18n },
6
+ ];
7
+
8
+ describe('SEO Content Length Validation', () => {
9
+ ENTRIES.forEach((entry) => {
10
+ describe(`Tool: ${entry.id}`, () => {
11
+ Object.keys(entry.i18n).forEach((locale) => {
12
+ it(`${locale}: SEO section should exist`, async () => {
13
+ const loader = (entry.i18n as Record<string, () => Promise<{ seo?: unknown[] }>>)[locale];
14
+ const content = await loader();
15
+ if (!content.seo) return;
16
+ expect(Array.isArray(content.seo)).toBe(true);
17
+ });
18
+ });
19
+ });
20
+ });
21
+ });
22
+
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { dronesCategory } from '../category';
3
+ import { droneFlightTime } from '../tool/drone-flight-time';
4
+ import { antennaLengthCalculator } from '../tool/antenna-length-calculator';
5
+ import { gpsCoordinatesConverter } from '../tool/gps-coordinates-converter';
6
+
7
+ const SUPPORTED_TYPES = new Set([
8
+ 'title',
9
+ 'paragraph',
10
+ 'list',
11
+ 'table',
12
+ 'tip',
13
+ 'card',
14
+ 'stats',
15
+ 'glossary',
16
+ 'code',
17
+ 'comparative',
18
+ 'diagnostic',
19
+ 'proscons',
20
+ 'summary',
21
+ 'grid',
22
+ 'message',
23
+ ]);
24
+
25
+ const ENTRIES = [
26
+ { id: 'dronesCategory', i18n: dronesCategory.i18n, isTool: false },
27
+ { id: 'droneFlightTime', i18n: droneFlightTime.i18n, isTool: true },
28
+ { id: 'antennaLengthCalculator', i18n: antennaLengthCalculator.i18n, isTool: true },
29
+ { id: 'gpsCoordinatesConverter', i18n: gpsCoordinatesConverter.i18n, isTool: true },
30
+ ];
31
+
32
+ describe('SEO Section Types Validation', () => {
33
+ ENTRIES.forEach((entry) => {
34
+ describe(`${entry.isTool ? 'Tool' : 'Category'}: ${entry.id}`, () => {
35
+ Object.keys(entry.i18n).forEach((locale) => {
36
+ it(`${locale}: All SEO section types should be supported and mapped`, async () => {
37
+ const loader = (entry.i18n as Record<string, () => Promise<{ seo?: unknown[] }>>)[locale];
38
+ const content = await loader();
39
+
40
+ if (!content.seo || !Array.isArray(content.seo)) {
41
+ return;
42
+ }
43
+
44
+ const foundTypes = new Set<string>();
45
+ const unsupportedTypes: { type: string; section: unknown }[] = [];
46
+
47
+ content.seo.forEach((section: any) => {
48
+ const sectionType = section.type;
49
+ if (!sectionType) {
50
+ unsupportedTypes.push({ type: 'UNDEFINED', section });
51
+ return;
52
+ }
53
+
54
+ foundTypes.add(sectionType);
55
+
56
+ if (!SUPPORTED_TYPES.has(sectionType)) {
57
+ unsupportedTypes.push({ type: sectionType, section });
58
+ }
59
+ });
60
+
61
+ if (unsupportedTypes.length > 0) {
62
+ const typesList = unsupportedTypes.map((u) => `"${u.type}"`).join(', ');
63
+ expect.fail(
64
+ `Found unmapped SEO section types in ${entry.id} (${locale}): ${typesList}\n` +
65
+ `Please add handlers for these types in seo.astro or update SUPPORTED_TYPES if they're intentionally missing.\n` +
66
+ `Unmapped sections:\n${JSON.stringify(unsupportedTypes, null, 2)}`
67
+ );
68
+ }
69
+
70
+ expect(foundTypes.size).toBeGreaterThan(0);
71
+ });
72
+ });
73
+ });
74
+ });
75
+ });
@@ -0,0 +1,17 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import { dronesCategory } from '../data';
4
+
5
+ describe('Tool Validation Suite', () => {
6
+ describe('Library Registration', () => {
7
+ it('should have 3 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(3);
9
+ });
10
+
11
+ it('dronesCategory should be defined', () => {
12
+ expect(dronesCategory).toBeDefined();
13
+ expect(dronesCategory.i18n).toBeDefined();
14
+ });
15
+ });
16
+ });
17
+