@jjlmoya/utils-sports 1.51.0 → 1.52.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-sports",
3
- "version": "1.51.0",
3
+ "version": "1.52.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readdirSync, readFileSync, existsSync } from 'fs';
3
+ import { join, relative } from 'path';
4
+
5
+ function findBibliographyAstroFiles(dir: string): string[] {
6
+ const files: string[] = [];
7
+ if (!existsSync(dir)) return files;
8
+ const entries = readdirSync(dir, { withFileTypes: true });
9
+
10
+ for (const entry of entries) {
11
+ const fullPath = join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ files.push(...findBibliographyAstroFiles(fullPath));
14
+ } else if (entry.isFile() && entry.name === 'bibliography.astro') {
15
+ files.push(fullPath);
16
+ }
17
+ }
18
+
19
+ return files;
20
+ }
21
+
22
+ const toolDir = join(process.cwd(), 'src', 'tool');
23
+ const bibliographyFiles = findBibliographyAstroFiles(toolDir);
24
+
25
+ describe('Bibliography Component Wellformed Export', () => {
26
+ it('found tool bibliography.astro components', () => {
27
+ expect(bibliographyFiles.length).toBeGreaterThan(0);
28
+ });
29
+
30
+ bibliographyFiles.forEach((file) => {
31
+ const relativePath = relative(process.cwd(), file);
32
+
33
+ it(`${relativePath} should use SharedBibliography from @jjlmoya/utils-shared`, () => {
34
+ const content = readFileSync(file, 'utf-8');
35
+
36
+ const usesSharedComponent =
37
+ content.includes('@jjlmoya/utils-shared') &&
38
+ (content.includes('Bibliography') || content.includes('SharedBibliography'));
39
+
40
+ expect(
41
+ usesSharedComponent,
42
+ `File "${relativePath}" does not use the standard Bibliography component from @jjlmoya/utils-shared, resulting in unstyled or raw bibliography output.`,
43
+ ).toBe(true);
44
+ });
45
+ });
46
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readdirSync, readFileSync, existsSync } from 'fs';
3
+ import { join, relative } from 'path';
4
+
5
+ function findSeoAstroFiles(dir: string): string[] {
6
+ const files: string[] = [];
7
+ if (!existsSync(dir)) return files;
8
+ const entries = readdirSync(dir, { withFileTypes: true });
9
+
10
+ for (const entry of entries) {
11
+ const fullPath = join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ files.push(...findSeoAstroFiles(fullPath));
14
+ } else if (entry.isFile() && entry.name === 'seo.astro') {
15
+ files.push(fullPath);
16
+ }
17
+ }
18
+
19
+ return files;
20
+ }
21
+
22
+ const toolDir = join(process.cwd(), 'src', 'tool');
23
+ const seoFiles = findSeoAstroFiles(toolDir);
24
+
25
+ describe('SEO Component Wellformed Export', () => {
26
+ it('found tool seo.astro components', () => {
27
+ expect(seoFiles.length).toBeGreaterThan(0);
28
+ });
29
+
30
+ seoFiles.forEach((file) => {
31
+ const relativePath = relative(process.cwd(), file);
32
+
33
+ it(`${relativePath} should dynamically load SEO sections and use SEORenderer`, () => {
34
+ const content = readFileSync(file, 'utf-8');
35
+
36
+ const usesSeoRenderer =
37
+ content.includes('@jjlmoya/utils-shared') &&
38
+ (content.includes('SEORenderer') || content.includes('SEOArticle'));
39
+
40
+ const acquiresDynamicContent =
41
+ content.includes('.i18n') ||
42
+ content.includes('loader') ||
43
+ content.includes('await loader') ||
44
+ content.includes('.seo');
45
+
46
+ const isBrokenPattern =
47
+ /sections\s*=\s*\[\s*\]/.test(content) && !acquiresDynamicContent;
48
+
49
+ expect(
50
+ usesSeoRenderer,
51
+ `File "${relativePath}" does not import or use SEORenderer from @jjlmoya/utils-shared.`,
52
+ ).toBe(true);
53
+
54
+ expect(
55
+ isBrokenPattern,
56
+ `File "${relativePath}" relies on a static sections=[] prop default without fetching content from entry.i18n, resulting in empty SEO text on consumers.`,
57
+ ).toBe(false);
58
+
59
+ expect(
60
+ acquiresDynamicContent,
61
+ `File "${relativePath}" does not fetch dynamic i18n content for SEO rendering.`,
62
+ ).toBe(true);
63
+ });
64
+ });
65
+ });
@@ -1,15 +1,6 @@
1
1
  ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
2
3
  import { bibliography } from './bibliography';
3
4
  ---
4
5
 
5
- <ul class="tn-bib-list">
6
- {
7
- bibliography.map((entry) => (
8
- <li>
9
- <a href={entry.url} target="_blank" rel="noopener noreferrer">
10
- {entry.name}
11
- </a>
12
- </li>
13
- ))
14
- }
15
- </ul>
6
+ <SharedBibliography links={bibliography} />
@@ -1,4 +1,15 @@
1
1
  ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { dartsScoreKeeper } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const loader = dartsScoreKeeper.i18n[locale as KnownLocale] ?? dartsScoreKeeper.i18n.es ?? dartsScoreKeeper.i18n.en;
12
+ const content = loader ? await loader() : null;
2
13
  ---
3
14
 
4
- <meta name="robots" content="index, follow" />
15
+ {content?.seo && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -1,15 +1,6 @@
1
1
  ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
2
3
  import { bibliographyEntries } from './bibliography';
3
4
  ---
4
5
 
5
- <div class="ps-bibliography">
6
- <ul>
7
- {bibliographyEntries.map((item) => (
8
- <li>
9
- <a href={item.url} target="_blank" rel="noopener noreferrer">
10
- {item.name}
11
- </a>
12
- </li>
13
- ))}
14
- </ul>
15
- </div>
6
+ <SharedBibliography links={bibliographyEntries} />
@@ -1,5 +1,15 @@
1
1
  ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { penaltyShootout } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const loader = penaltyShootout.i18n[locale as KnownLocale] ?? penaltyShootout.i18n.es ?? penaltyShootout.i18n.en;
12
+ const content = loader ? await loader() : null;
2
13
  ---
3
14
 
4
- <div class="ps-seo-container">
5
- </div>
15
+ {content?.seo && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -1,14 +1,15 @@
1
1
  ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { volleyballScoreKeeper } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const loader = volleyballScoreKeeper.i18n[locale as KnownLocale] ?? volleyballScoreKeeper.i18n.es ?? volleyballScoreKeeper.i18n.en;
12
+ const content = loader ? await loader() : null;
2
13
  ---
3
14
 
4
- <div class="volleyball-seo-content">
5
- <p>
6
- An interactive official FIVB indoor volleyball scoreboard and tactical rotation tracker designed for players, coaches, referees, and scorekeepers. Track rally scoring, set points, side-outs, clockwise player rotations, timeouts, and substitutions with precision and real-time visual feedback.
7
- </p>
8
- <h3>FIVB Volleyball Scoring and Rotation Rules</h3>
9
- <ul>
10
- <li><strong>Rally Point System:</strong> Standard sets are played to 25 points with a mandatory 2-point advantage. If the match goes to a deciding 5th set (or 3rd set in best-of-3 format), it is played to 15 points with a 2-point advantage.</li>
11
- <li><strong>Side-Out and Rotation:</strong> When the receiving team wins a rally, they gain the serve and their six court players must rotate one position clockwise (Position 2 moves to Position 1 to serve).</li>
12
- <li><strong>Team Match Limits:</strong> Each team is permitted a maximum of two 30-second timeouts and six player substitutions per set.</li>
13
- </ul>
14
- </div>
15
+ {content?.seo && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}