@jjlmoya/utils-astronomy 1.28.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.
- package/package.json +1 -1
- package/src/category/index.ts +29 -29
- package/src/entries.ts +5 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +144 -143
- package/src/tool/meteorShowerObservingPlanner/bibliography.astro +14 -0
- package/src/tool/meteorShowerObservingPlanner/bibliography.ts +12 -0
- package/src/tool/meteorShowerObservingPlanner/component.astro +165 -0
- package/src/tool/meteorShowerObservingPlanner/controller.ts +178 -0
- package/src/tool/meteorShowerObservingPlanner/dom-views.ts +187 -0
- package/src/tool/meteorShowerObservingPlanner/entry.ts +29 -0
- package/src/tool/meteorShowerObservingPlanner/evaluator.ts +20 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/de.ts +223 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/en.ts +223 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/es.ts +223 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/fr.ts +223 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/id.ts +227 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/it.ts +227 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/ja.ts +223 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/ko.ts +223 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/nl.ts +227 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/pl.ts +227 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/pt.ts +227 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/ru.ts +223 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/sv.ts +227 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/tr.ts +227 -0
- package/src/tool/meteorShowerObservingPlanner/i18n/zh.ts +223 -0
- package/src/tool/meteorShowerObservingPlanner/index.ts +11 -0
- package/src/tool/meteorShowerObservingPlanner/logic.test.ts +117 -0
- package/src/tool/meteorShowerObservingPlanner/logic.ts +263 -0
- package/src/tool/meteorShowerObservingPlanner/meteor-shower-observing-planner.css +404 -0
- package/src/tool/meteorShowerObservingPlanner/seo.astro +14 -0
- package/src/tool/meteorShowerObservingPlanner/storage.ts +41 -0
- package/src/tool/meteorShowerObservingPlanner/ui.ts +91 -0
- package/src/tool/starExposureCalculator/index.ts +3 -0
- package/src/tool/telescopeResolution/index.ts +3 -0
- package/src/tools.ts +4 -0
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import type { AstronomyCategoryEntry } from '../types';
|
|
2
|
-
import { bortleVisualizer } from '../tool/bortleVisualizer/entry';
|
|
3
|
-
import { deepSpaceScope } from '../tool/deepSpaceScope/entry';
|
|
4
|
-
import { starExposureCalculator } from '../tool/starExposureCalculator/entry';
|
|
5
|
-
import { telescopeResolution } from '../tool/telescopeResolution/entry';
|
|
1
|
+
import type { AstronomyCategoryEntry } from '../types';
|
|
2
|
+
import { bortleVisualizer } from '../tool/bortleVisualizer/entry';
|
|
3
|
+
import { deepSpaceScope } from '../tool/deepSpaceScope/entry';
|
|
4
|
+
import { starExposureCalculator } from '../tool/starExposureCalculator/entry';
|
|
5
|
+
import { telescopeResolution } from '../tool/telescopeResolution/entry';
|
|
6
6
|
import { eyepieceCalculator } from '../tool/smartEyepieceCalculator/entry';
|
|
7
7
|
import { telescopeExitPupilPlanner } from '../tool/telescopeExitPupilPlanner/entry';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
8
|
+
import { meteorShowerObservingPlanner } from '../tool/meteorShowerObservingPlanner/entry';
|
|
9
|
+
|
|
10
|
+
export const toolsCategory: AstronomyCategoryEntry = {
|
|
11
|
+
icon: 'mdi:telescope',
|
|
12
|
+
tools: [bortleVisualizer, deepSpaceScope, starExposureCalculator, telescopeResolution, eyepieceCalculator, telescopeExitPupilPlanner, meteorShowerObservingPlanner],
|
|
13
|
+
i18n: {
|
|
14
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
15
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
16
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
17
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
18
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
19
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
20
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
21
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
22
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
23
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
24
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
25
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
26
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
27
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
28
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export const astronomyCategory = toolsCategory;
|
package/src/entries.ts
CHANGED
|
@@ -10,6 +10,8 @@ export { eyepieceCalculator } from './tool/smartEyepieceCalculator/entry';
|
|
|
10
10
|
export type { EyepieceCalculatorUI, EyepieceCalculatorLocaleContent } from './tool/smartEyepieceCalculator/entry';
|
|
11
11
|
export { telescopeExitPupilPlanner } from './tool/telescopeExitPupilPlanner/entry';
|
|
12
12
|
export type { TelescopeExitPupilPlannerUI, TelescopeExitPupilPlannerLocaleContent } from './tool/telescopeExitPupilPlanner/entry';
|
|
13
|
+
export { meteorShowerObservingPlanner } from './tool/meteorShowerObservingPlanner/entry';
|
|
14
|
+
export type { MeteorShowerObservingPlannerUI, MeteorShowerObservingPlannerLocaleContent } from './tool/meteorShowerObservingPlanner/entry';
|
|
13
15
|
export { toolsCategory, astronomyCategory } from './category';
|
|
14
16
|
import { bortleVisualizer } from './tool/bortleVisualizer/entry';
|
|
15
17
|
import { deepSpaceScope } from './tool/deepSpaceScope/entry';
|
|
@@ -17,4 +19,6 @@ import { starExposureCalculator } from './tool/starExposureCalculator/entry';
|
|
|
17
19
|
import { telescopeResolution } from './tool/telescopeResolution/entry';
|
|
18
20
|
import { eyepieceCalculator } from './tool/smartEyepieceCalculator/entry';
|
|
19
21
|
import { telescopeExitPupilPlanner } from './tool/telescopeExitPupilPlanner/entry';
|
|
20
|
-
|
|
22
|
+
import { meteorShowerObservingPlanner } from './tool/meteorShowerObservingPlanner/entry';
|
|
23
|
+
export const ALL_ENTRIES = [bortleVisualizer, deepSpaceScope, starExposureCalculator, telescopeResolution, eyepieceCalculator, telescopeExitPupilPlanner, meteorShowerObservingPlanner];
|
|
24
|
+
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './tool/bortleVisualizer';
|
|
|
2
2
|
export * from './tool/deepSpaceScope';
|
|
3
3
|
export * from './tool/starExposureCalculator';
|
|
4
4
|
export * from './tool/telescopeResolution';
|
|
5
|
+
export * from './tool/meteorShowerObservingPlanner';
|
|
5
6
|
|
|
6
7
|
export { toolsCategory as astronomyCategory } from './category';
|
|
7
8
|
export const AstronomyCategorySEO = () => import('./category/seo.astro').then((m) => m.default);
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { ALL_TOOLS } from '../tools';
|
|
3
3
|
|
|
4
4
|
describe('Locale Completeness Validation', () => {
|
|
5
|
-
it('all
|
|
6
|
-
expect(ALL_TOOLS.length).toBe(
|
|
5
|
+
it('all 7 tools registered', () => {
|
|
6
|
+
expect(ALL_TOOLS.length).toBe(7);
|
|
7
7
|
});
|
|
8
8
|
});
|
|
@@ -1,147 +1,148 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { ALL_TOOLS } from '../tools';
|
|
5
|
-
import { astronomyCategory } from '../
|
|
6
|
-
import type { ToolDefinition } from '../types';
|
|
7
|
-
|
|
8
|
-
function extractSeoText(sections: any[]): string {
|
|
9
|
-
return sections
|
|
10
|
-
.map((section) => extractSectionText(section))
|
|
11
|
-
.join(' ');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function extractHtmlOrText(section: any): string {
|
|
15
|
-
const hasContent = 'html' in section || 'text' in section;
|
|
16
|
-
const isNotTable = section.type !== 'table';
|
|
17
|
-
return hasContent && isNotTable ? (section.html || section.text || '') : '';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function extractListItems(section: any): string {
|
|
21
|
-
return 'items' in section && Array.isArray(section.items) ? section.items.join(' ') : '';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function extractTableRows(section: any): string {
|
|
25
|
-
return 'rows' in section && Array.isArray(section.rows) ? section.rows.flat().join(' ') : '';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function extractSectionText(section: any): string {
|
|
29
|
-
let text = '';
|
|
30
|
-
text += extractHtmlOrText(section);
|
|
31
|
-
text += extractListItems(section);
|
|
32
|
-
text += extractTableRows(section);
|
|
33
|
-
return text;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function countWords(text: string): number {
|
|
37
|
-
const cleanText = text.replace(/<[^>]*>/g, '').trim();
|
|
38
|
-
const standardWords = cleanText.split(/\s+/).filter((w) => w.length > 0).length;
|
|
39
|
-
const cjkChars = cleanText.match(/[\u4e00-\u9fa5\u3040-\u30ff\uac00-\ud7af]/g)?.length || 0;
|
|
40
|
-
const cjkBlocks = cleanText.split(/[^\u4e00-\u9fa5\u3040-\u30ff\uac00-\ud7af]+/).filter(w => w.length > 0).length;
|
|
41
|
-
return standardWords - cjkBlocks + cjkChars;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
describe('Tool Validation Suite', () => {
|
|
45
|
-
describe('Strict Tool Definition Validation', () => {
|
|
46
|
-
ALL_TOOLS.forEach((tool: ToolDefinition) => {
|
|
47
|
-
const { entry } = tool;
|
|
48
|
-
|
|
49
|
-
describe(`${entry.id} structure`, () => {
|
|
50
|
-
it('should fulfill ToolDefinition interface', () => {
|
|
51
|
-
expect(tool.entry).toBeDefined();
|
|
52
|
-
expect(tool.Component).toBeDefined();
|
|
53
|
-
expect(tool.SEOComponent).toBeDefined();
|
|
54
|
-
expect(tool.BibliographyComponent).toBeDefined();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should have valid ID in kebab-case', () => {
|
|
58
|
-
expect(entry.id).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should have valid icons with bg and fg', () => {
|
|
62
|
-
expect(entry.icons.bg).toMatch(/^mdi:/);
|
|
63
|
-
expect(entry.icons.fg).toMatch(/^mdi:/);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('i18n Content Validation', () => {
|
|
67
|
-
Object.entries(entry.i18n).forEach(([locale, loader]) => {
|
|
68
|
-
it(`should load ${locale} content and follow slug rules`, async () => {
|
|
69
|
-
const content = await loader();
|
|
70
|
-
expect(content.slug).toBeDefined();
|
|
71
|
-
expect(content.slug).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
|
|
72
|
-
|
|
73
|
-
if (locale === 'es') {
|
|
74
|
-
const validSlugs = [
|
|
75
|
-
'simulador-cielo-oscuro',
|
|
76
|
-
'alcance-telescopio',
|
|
77
|
-
'calculadora-regla-500',
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { ALL_TOOLS } from '../tools';
|
|
5
|
+
import { astronomyCategory } from '../category';
|
|
6
|
+
import type { ToolDefinition } from '../types';
|
|
7
|
+
|
|
8
|
+
function extractSeoText(sections: any[]): string {
|
|
9
|
+
return sections
|
|
10
|
+
.map((section) => extractSectionText(section))
|
|
11
|
+
.join(' ');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function extractHtmlOrText(section: any): string {
|
|
15
|
+
const hasContent = 'html' in section || 'text' in section;
|
|
16
|
+
const isNotTable = section.type !== 'table';
|
|
17
|
+
return hasContent && isNotTable ? (section.html || section.text || '') : '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function extractListItems(section: any): string {
|
|
21
|
+
return 'items' in section && Array.isArray(section.items) ? section.items.join(' ') : '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function extractTableRows(section: any): string {
|
|
25
|
+
return 'rows' in section && Array.isArray(section.rows) ? section.rows.flat().join(' ') : '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractSectionText(section: any): string {
|
|
29
|
+
let text = '';
|
|
30
|
+
text += extractHtmlOrText(section);
|
|
31
|
+
text += extractListItems(section);
|
|
32
|
+
text += extractTableRows(section);
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function countWords(text: string): number {
|
|
37
|
+
const cleanText = text.replace(/<[^>]*>/g, '').trim();
|
|
38
|
+
const standardWords = cleanText.split(/\s+/).filter((w) => w.length > 0).length;
|
|
39
|
+
const cjkChars = cleanText.match(/[\u4e00-\u9fa5\u3040-\u30ff\uac00-\ud7af]/g)?.length || 0;
|
|
40
|
+
const cjkBlocks = cleanText.split(/[^\u4e00-\u9fa5\u3040-\u30ff\uac00-\ud7af]+/).filter((w) => w.length > 0).length;
|
|
41
|
+
return standardWords - cjkBlocks + cjkChars;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('Tool Validation Suite', () => {
|
|
45
|
+
describe('Strict Tool Definition Validation', () => {
|
|
46
|
+
ALL_TOOLS.forEach((tool: ToolDefinition) => {
|
|
47
|
+
const { entry } = tool;
|
|
48
|
+
|
|
49
|
+
describe(`${entry.id} structure`, () => {
|
|
50
|
+
it('should fulfill ToolDefinition interface', () => {
|
|
51
|
+
expect(tool.entry).toBeDefined();
|
|
52
|
+
expect(tool.Component).toBeDefined();
|
|
53
|
+
expect(tool.SEOComponent).toBeDefined();
|
|
54
|
+
expect(tool.BibliographyComponent).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should have valid ID in kebab-case', () => {
|
|
58
|
+
expect(entry.id).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should have valid icons with bg and fg', () => {
|
|
62
|
+
expect(entry.icons.bg).toMatch(/^mdi:/);
|
|
63
|
+
expect(entry.icons.fg).toMatch(/^mdi:/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('i18n Content Validation', () => {
|
|
67
|
+
Object.entries(entry.i18n).forEach(([locale, loader]) => {
|
|
68
|
+
it(`should load ${locale} content and follow slug rules`, async () => {
|
|
69
|
+
const content = await loader();
|
|
70
|
+
expect(content.slug).toBeDefined();
|
|
71
|
+
expect(content.slug).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
|
|
72
|
+
|
|
73
|
+
if (locale === 'es') {
|
|
74
|
+
const validSlugs = [
|
|
75
|
+
'simulador-cielo-oscuro',
|
|
76
|
+
'alcance-telescopio',
|
|
77
|
+
'calculadora-regla-500',
|
|
78
78
|
'calculadora-resolucion-telescopio',
|
|
79
79
|
'calculadora-oculares',
|
|
80
80
|
'planificador-pupila-salida-aumentos-telescopio',
|
|
81
|
+
'planificador-observacion-lluvia-meteoros',
|
|
82
|
+
'meteor-shower-observing-planner',
|
|
81
83
|
];
|
|
82
|
-
expect(validSlugs).toContain(content.slug);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it(`should have valid basic content in ${locale}`, async () => {
|
|
87
|
-
const content = await loader();
|
|
88
|
-
expect(content.title.length).toBeGreaterThan(0);
|
|
89
|
-
expect(content.description.length).toBeGreaterThan(0);
|
|
90
|
-
expect(Object.keys(content.ui).length).toBeGreaterThan(0);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it(`should have SEO content with > 300 words in ${locale}`, async () => {
|
|
94
|
-
const content = await loader();
|
|
95
|
-
const seoText = extractSeoText(content.seo);
|
|
96
|
-
const wordCount = countWords(seoText);
|
|
97
|
-
expect(wordCount).toBeGreaterThanOrEqual(300);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe('Library Registration', () => {
|
|
106
|
-
it('should have
|
|
107
|
-
expect(ALL_TOOLS.length).toBe(
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should have all tools in astronomyCategory', () => {
|
|
111
|
-
expect(astronomyCategory.tools.length).toBe(
|
|
112
|
-
ALL_TOOLS.forEach(({ entry }) => {
|
|
113
|
-
const exists = astronomyCategory.tools.some((t: any) => t.id === entry.id);
|
|
114
|
-
expect(exists).toBe(true);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('Binary Consistency', () => {
|
|
120
|
-
ALL_TOOLS.forEach(({ entry }) => {
|
|
121
|
-
it(`${entry.id} files should exist in the tool folder`, () => {
|
|
122
|
-
const toolVarName = (entry.id ?? '').replace(/-([a-z])/g, (_: string, g: string) => g.toUpperCase());
|
|
123
|
-
const toolDir = path.join('src', 'tool', toolVarName);
|
|
124
|
-
|
|
125
|
-
expect(fs.existsSync(path.join(toolDir, 'component.astro'))).toBe(true);
|
|
126
|
-
expect(fs.existsSync(path.join(toolDir, 'seo.astro'))).toBe(true);
|
|
127
|
-
expect(fs.existsSync(path.join(toolDir, 'bibliography.astro'))).toBe(true);
|
|
128
|
-
expect(fs.existsSync(path.join(toolDir, 'index.ts'))).toBe(true);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('Bibliography Components Structure', () => {
|
|
134
|
-
ALL_TOOLS.forEach(({ entry }) => {
|
|
135
|
-
it(`${entry.id} should have a standard bibliography structure`, () => {
|
|
136
|
-
const toolVarName = (entry.id ?? '').replace(/-([a-z])/g, (_: string, g: string) => g.toUpperCase());
|
|
137
|
-
const componentPath = path.join('src', 'tool', toolVarName, 'bibliography.astro');
|
|
138
|
-
const content = fs.readFileSync(componentPath, 'utf-8');
|
|
139
|
-
|
|
140
|
-
expect(content).toContain("import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared'");
|
|
141
|
-
expect(content).toContain(`import { ${toolVarName} } from './index'`);
|
|
142
|
-
expect(content).toContain('<SharedBibliography links={content.bibliography} />');
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
84
|
+
expect(validSlugs).toContain(content.slug);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it(`should have valid basic content in ${locale}`, async () => {
|
|
89
|
+
const content = await loader();
|
|
90
|
+
expect(content.title.length).toBeGreaterThan(0);
|
|
91
|
+
expect(content.description.length).toBeGreaterThan(0);
|
|
92
|
+
expect(Object.keys(content.ui).length).toBeGreaterThan(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it(`should have SEO content with > 300 words in ${locale}`, async () => {
|
|
96
|
+
const content = await loader();
|
|
97
|
+
const seoText = extractSeoText(content.seo);
|
|
98
|
+
const wordCount = countWords(seoText);
|
|
99
|
+
expect(wordCount).toBeGreaterThanOrEqual(300);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('Library Registration', () => {
|
|
108
|
+
it('should have 7 tools in ALL_TOOLS', () => {
|
|
109
|
+
expect(ALL_TOOLS.length).toBe(7);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should have all tools in astronomyCategory', () => {
|
|
113
|
+
expect(astronomyCategory.tools.length).toBe(7);
|
|
114
|
+
ALL_TOOLS.forEach(({ entry }) => {
|
|
115
|
+
const exists = astronomyCategory.tools.some((t: any) => t.id === entry.id);
|
|
116
|
+
expect(exists).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('Binary Consistency', () => {
|
|
122
|
+
ALL_TOOLS.forEach(({ entry }) => {
|
|
123
|
+
it(`${entry.id} files should exist in the tool folder`, () => {
|
|
124
|
+
const toolVarName = (entry.id ?? '').replace(/-([a-z])/g, (_: string, g: string) => g.toUpperCase());
|
|
125
|
+
const toolDir = path.join('src', 'tool', toolVarName);
|
|
126
|
+
|
|
127
|
+
expect(fs.existsSync(path.join(toolDir, 'component.astro'))).toBe(true);
|
|
128
|
+
expect(fs.existsSync(path.join(toolDir, 'seo.astro'))).toBe(true);
|
|
129
|
+
expect(fs.existsSync(path.join(toolDir, 'bibliography.astro'))).toBe(true);
|
|
130
|
+
expect(fs.existsSync(path.join(toolDir, 'index.ts'))).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('Bibliography Components Structure', () => {
|
|
136
|
+
ALL_TOOLS.forEach(({ entry }) => {
|
|
137
|
+
it(`${entry.id} should have a standard bibliography structure`, () => {
|
|
138
|
+
const toolVarName = (entry.id ?? '').replace(/-([a-z])/g, (_: string, g: string) => g.toUpperCase());
|
|
139
|
+
const componentPath = path.join('src', 'tool', toolVarName, 'bibliography.astro');
|
|
140
|
+
const content = fs.readFileSync(componentPath, 'utf-8');
|
|
141
|
+
|
|
142
|
+
expect(content).toContain("import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared'");
|
|
143
|
+
expect(content).toContain(`import { ${toolVarName} } from './index'`);
|
|
144
|
+
expect(content).toContain('<SharedBibliography links={content.bibliography} />');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { meteorShowerObservingPlanner } from './index';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
locale: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { locale } = Astro.props;
|
|
10
|
+
const entry = await meteorShowerObservingPlanner.i18n[locale as keyof typeof meteorShowerObservingPlanner.i18n]();
|
|
11
|
+
const content = entry();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<SharedBibliography links={content.bibliography} />
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const METEOR_PLANNER_BIBLIOGRAPHY: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'International Meteor Organization (IMO) - Meteor Shower Calendar',
|
|
6
|
+
url: 'https://www.imo.net/resources/calendar/',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'NASA Space Place - Meteor Showers and Their Parent Comets',
|
|
10
|
+
url: 'https://spaceplace.nasa.gov/meteor-shower/en/',
|
|
11
|
+
},
|
|
12
|
+
];
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { meteorShowerObservingPlanner } from './index';
|
|
3
|
+
import type { MeteorShowerObservingPlannerUI } from './ui';
|
|
4
|
+
import './meteor-shower-observing-planner.css';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
ui?: MeteorShowerObservingPlannerUI;
|
|
8
|
+
locale?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { ui: passedUi, locale } = Astro.props;
|
|
12
|
+
|
|
13
|
+
let ui = passedUi;
|
|
14
|
+
if (!ui) {
|
|
15
|
+
const currentLocale = locale || 'en';
|
|
16
|
+
const entryLoader = meteorShowerObservingPlanner.i18n[currentLocale as keyof typeof meteorShowerObservingPlanner.i18n] || meteorShowerObservingPlanner.i18n.en;
|
|
17
|
+
const localeModule = await entryLoader();
|
|
18
|
+
const content = typeof localeModule === 'function' ? localeModule() : localeModule;
|
|
19
|
+
ui = content.ui;
|
|
20
|
+
}
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<div class="meteor-shower-planner-card" id="meteor-shower-planner-root">
|
|
24
|
+
<div class="planner-layout-grid">
|
|
25
|
+
<div class="planner-control-section">
|
|
26
|
+
<div class="input-control-group">
|
|
27
|
+
<span class="input-label-row">{ui.presetLabel}</span>
|
|
28
|
+
<div class="presets-chip-grid">
|
|
29
|
+
<button type="button" class="preset-chip-btn active" data-shower-id="perseids">{ui.presets['perseids'] || 'Perseids'}</button>
|
|
30
|
+
<button type="button" class="preset-chip-btn" data-shower-id="geminids">{ui.presets['geminids'] || 'Geminids'}</button>
|
|
31
|
+
<button type="button" class="preset-chip-btn" data-shower-id="quadrantids">{ui.presets['quadrantids'] || 'Quadrantids'}</button>
|
|
32
|
+
<button type="button" class="preset-chip-btn" data-shower-id="lyrids">{ui.presets['lyrids'] || 'Lyrids'}</button>
|
|
33
|
+
<button type="button" class="preset-chip-btn" data-shower-id="orionids">{ui.presets['orionids'] || 'Orionids'}</button>
|
|
34
|
+
<button type="button" class="preset-chip-btn" data-shower-id="eta-aquariids">{ui.presets['eta-aquariids'] || 'Eta Aquariids'}</button>
|
|
35
|
+
<button type="button" class="preset-chip-btn" data-shower-id="custom">{ui.presets['custom'] || 'Custom'}</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="input-control-group custom-zhr-group" style="display: none;">
|
|
40
|
+
<label for="planner-custom-zhr-input" class="input-label-row">{ui.customZhrLabel}</label>
|
|
41
|
+
<input type="number" id="planner-custom-zhr-input" class="hours-select-input" value="100" min="1" max="1000" aria-label={ui.customZhrLabel} />
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="input-control-group">
|
|
45
|
+
<label for="planner-latitude-input" class="input-label-row">
|
|
46
|
+
<span>{ui.latitudeLabel}</span>
|
|
47
|
+
<span class="input-val-badge" id="planner-latitude-val">+40°</span>
|
|
48
|
+
</label>
|
|
49
|
+
<input type="range" id="planner-latitude-input" class="planner-slider-range" min="-80" max="80" value="40" step="1" aria-label={ui.latitudeLabel} />
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="input-control-group">
|
|
53
|
+
<label for="planner-bortle-input" class="input-label-row">
|
|
54
|
+
<span>{ui.bortleLabel}</span>
|
|
55
|
+
<span class="input-val-badge" id="planner-bortle-val">{ui.classLabel || 'Class'} 4</span>
|
|
56
|
+
</label>
|
|
57
|
+
<input type="range" id="planner-bortle-input" class="planner-slider-range" min="1" max="9" value="4" step="1" aria-label={ui.bortleLabel} />
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="input-control-group">
|
|
61
|
+
<label for="planner-moon-input" class="input-label-row">
|
|
62
|
+
<span>{ui.moonPhaseLabel}</span>
|
|
63
|
+
<span class="input-val-badge" id="planner-moon-val">15%</span>
|
|
64
|
+
</label>
|
|
65
|
+
<input type="range" id="planner-moon-input" class="planner-slider-range" min="0" max="1" value="0.15" step="0.05" aria-label={ui.moonPhaseLabel} />
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div class="input-control-group">
|
|
69
|
+
<span class="input-label-row">{ui.sessionHoursLabel}</span>
|
|
70
|
+
<div class="hours-select-row">
|
|
71
|
+
<div class="custom-select-wrap">
|
|
72
|
+
<button type="button" class="custom-select-trigger" id="planner-start-hour-btn" aria-haspopup="listbox" aria-expanded="false" aria-label="Start Hour">
|
|
73
|
+
<span id="planner-start-hour-label">22:00</span>
|
|
74
|
+
<svg class="select-arrow-icon" viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
|
75
|
+
</button>
|
|
76
|
+
<div class="custom-select-menu" id="planner-start-hour-menu" role="listbox" hidden>
|
|
77
|
+
{[20, 21, 22, 23, 0, 1, 2].map((h) => (
|
|
78
|
+
<button type="button" class={`custom-select-opt ${h === 22 ? 'active' : ''}`} data-val={h}>
|
|
79
|
+
{String(h).padStart(2, '0')}:00
|
|
80
|
+
</button>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<span class="hours-sep-txt">{ui.toLabel || 'to'}</span>
|
|
85
|
+
<div class="custom-select-wrap">
|
|
86
|
+
<button type="button" class="custom-select-trigger" id="planner-end-hour-btn" aria-haspopup="listbox" aria-expanded="false" aria-label="End Hour">
|
|
87
|
+
<span id="planner-end-hour-label">05:00</span>
|
|
88
|
+
<svg class="select-arrow-icon" viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
|
89
|
+
</button>
|
|
90
|
+
<div class="custom-select-menu" id="planner-end-hour-menu" role="listbox" hidden>
|
|
91
|
+
{[3, 4, 5, 6, 7].map((h) => (
|
|
92
|
+
<button type="button" class={`custom-select-opt ${h === 5 ? 'active' : ''}`} data-val={h}>
|
|
93
|
+
{String(h).padStart(2, '0')}:00
|
|
94
|
+
</button>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="planner-results-section">
|
|
103
|
+
<div class="results-metrics-grid">
|
|
104
|
+
<div class="metric-card-box">
|
|
105
|
+
<div class="metric-lbl-txt">{ui.bestWindowLabel}</div>
|
|
106
|
+
<div class="metric-val-txt res-best-window">02:00 - 04:00</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="metric-card-box">
|
|
109
|
+
<div class="metric-lbl-txt">{ui.maxRateLabel}</div>
|
|
110
|
+
<div class="metric-val-txt res-max-rate">45.0 {ui.hrUnit || '/ hr'}</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="metric-card-box">
|
|
113
|
+
<div class="metric-lbl-txt">{ui.skyQualityLabel}</div>
|
|
114
|
+
<div class="metric-val-txt res-score-val badge-excellent">85/100</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="res-badges-list"></div>
|
|
119
|
+
|
|
120
|
+
<div class="sky-dome-container"></div>
|
|
121
|
+
|
|
122
|
+
<div class="hourly-timeline-panel">
|
|
123
|
+
<div class="panel-title-lbl">{ui.hourlyChartTitle}</div>
|
|
124
|
+
<div class="hourly-bars-track"></div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div class="checklist-card-panel">
|
|
130
|
+
<div class="panel-title-lbl">{ui.checklistTitle}</div>
|
|
131
|
+
<ul class="checklist-ul">
|
|
132
|
+
{ui.checklistItems.map((item) => (
|
|
133
|
+
<li class="checklist-li">
|
|
134
|
+
<span class="checklist-icon-bullet"></span>
|
|
135
|
+
<span>{item}</span>
|
|
136
|
+
</li>
|
|
137
|
+
))}
|
|
138
|
+
</ul>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<script is:inline type="application/json" id="meteor-planner-ui-data" set:html={JSON.stringify(ui)}></script>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<script>
|
|
145
|
+
import { MeteorShowerPlannerController } from './controller';
|
|
146
|
+
import { loadSavedInput } from './storage';
|
|
147
|
+
|
|
148
|
+
function initPlanner() {
|
|
149
|
+
const root = document.getElementById('meteor-shower-planner-root');
|
|
150
|
+
const dataEl = document.getElementById('meteor-planner-ui-data');
|
|
151
|
+
if (root && dataEl) {
|
|
152
|
+
try {
|
|
153
|
+
const ui = JSON.parse(dataEl.textContent || '{}');
|
|
154
|
+
const input = loadSavedInput();
|
|
155
|
+
new MeteorShowerPlannerController(root, input, ui);
|
|
156
|
+
} catch {}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (document.readyState === 'loading') {
|
|
161
|
+
document.addEventListener('DOMContentLoaded', initPlanner);
|
|
162
|
+
} else {
|
|
163
|
+
initPlanner();
|
|
164
|
+
}
|
|
165
|
+
</script>
|