@jjlmoya/utils-sports 1.35.0 → 1.40.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 +4 -1
- package/src/entries.ts +32 -1
- package/src/index.ts +3 -0
- package/src/tests/locale_completeness.test.ts +24 -3
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/penaltyShootout/bibliography.astro +15 -0
- package/src/tool/penaltyShootout/bibliography.ts +8 -0
- package/src/tool/penaltyShootout/component.astro +228 -0
- package/src/tool/penaltyShootout/entry.ts +30 -0
- package/src/tool/penaltyShootout/i18n/de.ts +193 -0
- package/src/tool/penaltyShootout/i18n/en.ts +193 -0
- package/src/tool/penaltyShootout/i18n/es.ts +193 -0
- package/src/tool/penaltyShootout/i18n/fr.ts +193 -0
- package/src/tool/penaltyShootout/i18n/id.ts +193 -0
- package/src/tool/penaltyShootout/i18n/it.ts +193 -0
- package/src/tool/penaltyShootout/i18n/ja.ts +193 -0
- package/src/tool/penaltyShootout/i18n/ko.ts +193 -0
- package/src/tool/penaltyShootout/i18n/nl.ts +193 -0
- package/src/tool/penaltyShootout/i18n/pl.ts +193 -0
- package/src/tool/penaltyShootout/i18n/pt.ts +193 -0
- package/src/tool/penaltyShootout/i18n/ru.ts +193 -0
- package/src/tool/penaltyShootout/i18n/sv.ts +193 -0
- package/src/tool/penaltyShootout/i18n/tr.ts +193 -0
- package/src/tool/penaltyShootout/i18n/zh.ts +193 -0
- package/src/tool/penaltyShootout/index.ts +9 -0
- package/src/tool/penaltyShootout/logic.test.ts +51 -0
- package/src/tool/penaltyShootout/logic.ts +128 -0
- package/src/tool/penaltyShootout/penalty-shootout.css +414 -0
- package/src/tool/penaltyShootout/seo.astro +5 -0
- package/src/tool/penaltyShootout/ui.ts +19 -0
- package/src/tool/runningPacePredictor/bibliography.astro +6 -0
- package/src/tool/runningPacePredictor/bibliography.ts +14 -0
- package/src/tool/runningPacePredictor/component.astro +301 -0
- package/src/tool/runningPacePredictor/entry.ts +30 -0
- package/src/tool/runningPacePredictor/i18n/de.ts +248 -0
- package/src/tool/runningPacePredictor/i18n/en.ts +257 -0
- package/src/tool/runningPacePredictor/i18n/es.ts +257 -0
- package/src/tool/runningPacePredictor/i18n/fr.ts +248 -0
- package/src/tool/runningPacePredictor/i18n/id.ts +248 -0
- package/src/tool/runningPacePredictor/i18n/it.ts +248 -0
- package/src/tool/runningPacePredictor/i18n/ja.ts +248 -0
- package/src/tool/runningPacePredictor/i18n/ko.ts +248 -0
- package/src/tool/runningPacePredictor/i18n/nl.ts +248 -0
- package/src/tool/runningPacePredictor/i18n/pl.ts +248 -0
- package/src/tool/runningPacePredictor/i18n/pt.ts +248 -0
- package/src/tool/runningPacePredictor/i18n/ru.ts +248 -0
- package/src/tool/runningPacePredictor/i18n/sv.ts +248 -0
- package/src/tool/runningPacePredictor/i18n/tr.ts +248 -0
- package/src/tool/runningPacePredictor/i18n/zh.ts +248 -0
- package/src/tool/runningPacePredictor/index.ts +9 -0
- package/src/tool/runningPacePredictor/logic.test.ts +70 -0
- package/src/tool/runningPacePredictor/logic.ts +161 -0
- package/src/tool/runningPacePredictor/running-pace-predictor.css +330 -0
- package/src/tool/runningPacePredictor/seo.astro +224 -0
- package/src/tool/runningPacePredictor/ui.ts +79 -0
- package/src/tool/swimCssCalculator/bibliography.astro +6 -0
- package/src/tool/swimCssCalculator/bibliography.ts +14 -0
- package/src/tool/swimCssCalculator/component.astro +350 -0
- package/src/tool/swimCssCalculator/entry.ts +29 -0
- package/src/tool/swimCssCalculator/i18n/de.ts +233 -0
- package/src/tool/swimCssCalculator/i18n/en.ts +241 -0
- package/src/tool/swimCssCalculator/i18n/es.ts +233 -0
- package/src/tool/swimCssCalculator/i18n/fr.ts +233 -0
- package/src/tool/swimCssCalculator/i18n/id.ts +233 -0
- package/src/tool/swimCssCalculator/i18n/it.ts +233 -0
- package/src/tool/swimCssCalculator/i18n/ja.ts +233 -0
- package/src/tool/swimCssCalculator/i18n/ko.ts +233 -0
- package/src/tool/swimCssCalculator/i18n/nl.ts +233 -0
- package/src/tool/swimCssCalculator/i18n/pl.ts +233 -0
- package/src/tool/swimCssCalculator/i18n/pt.ts +233 -0
- package/src/tool/swimCssCalculator/i18n/ru.ts +233 -0
- package/src/tool/swimCssCalculator/i18n/sv.ts +233 -0
- package/src/tool/swimCssCalculator/i18n/tr.ts +233 -0
- package/src/tool/swimCssCalculator/i18n/zh.ts +233 -0
- package/src/tool/swimCssCalculator/index.ts +9 -0
- package/src/tool/swimCssCalculator/logic.test.ts +78 -0
- package/src/tool/swimCssCalculator/logic.ts +137 -0
- package/src/tool/swimCssCalculator/seo.astro +224 -0
- package/src/tool/swimCssCalculator/swim-css-calculator.css +437 -0
- package/src/tool/swimCssCalculator/ui.ts +85 -0
- package/src/tools.ts +7 -5
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { eloRatingCalculator } from '../tool/eloRatingCalculator/entry';
|
|
|
9
9
|
import { pingPongScoreKeeper } from '../tool/pingPongScoreKeeper/entry';
|
|
10
10
|
import { rugbyScoreKeeper } from '../tool/rugbyScoreKeeper/entry';
|
|
11
11
|
import { baseballScoreKeeper } from '../tool/baseballScoreKeeper/entry';
|
|
12
|
+
import { penaltyShootout } from '../tool/penaltyShootout/entry';
|
|
13
|
+
import { swimCssCalculator } from '../tool/swimCssCalculator/entry';
|
|
12
14
|
|
|
13
15
|
export const sportsCategory: SportsCategoryEntry = {
|
|
14
16
|
icon: 'mdi:soccer',
|
|
@@ -23,6 +25,8 @@ export const sportsCategory: SportsCategoryEntry = {
|
|
|
23
25
|
pingPongScoreKeeper,
|
|
24
26
|
rugbyScoreKeeper,
|
|
25
27
|
baseballScoreKeeper,
|
|
28
|
+
penaltyShootout,
|
|
29
|
+
swimCssCalculator,
|
|
26
30
|
] as unknown as SportsToolEntry<Record<string, string>>[],
|
|
27
31
|
i18n: {
|
|
28
32
|
es: () => import('./i18n/es').then((m) => m.content),
|
|
@@ -42,4 +46,3 @@ export const sportsCategory: SportsCategoryEntry = {
|
|
|
42
46
|
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
43
47
|
},
|
|
44
48
|
};
|
|
45
|
-
|
package/src/entries.ts
CHANGED
|
@@ -30,7 +30,15 @@ export { rugbyScoreKeeper } from './tool/rugbyScoreKeeper/entry';
|
|
|
30
30
|
export type { RugbyScoreKeeperLocaleContent } from './tool/rugbyScoreKeeper/entry';
|
|
31
31
|
export { baseballScoreKeeper } from './tool/baseballScoreKeeper/entry';
|
|
32
32
|
export type { BaseballScoreKeeperLocaleContent } from './tool/baseballScoreKeeper/entry';
|
|
33
|
+
export { penaltyShootout } from './tool/penaltyShootout/entry';
|
|
34
|
+
export type { PenaltyShootoutLocaleContent } from './tool/penaltyShootout/entry';
|
|
35
|
+
export { runningPacePredictor } from './tool/runningPacePredictor/entry';
|
|
36
|
+
export type { RunningPacePredictorLocaleContent } from './tool/runningPacePredictor/entry';
|
|
37
|
+
export { swimCssCalculator } from './tool/swimCssCalculator/entry';
|
|
38
|
+
export type { SwimCssCalculatorLocaleContent } from './tool/swimCssCalculator/entry';
|
|
39
|
+
|
|
33
40
|
export { sportsCategory } from './category';
|
|
41
|
+
|
|
34
42
|
import { basketScoreKeeper } from './tool/basketScoreKeeper/entry';
|
|
35
43
|
import { footballScoreKeeper } from './tool/footballScoreKeeper/entry';
|
|
36
44
|
import { gymTracker } from './tool/gymTracker/entry';
|
|
@@ -47,5 +55,28 @@ import { beachVolleyballScoreKeeper } from './tool/beachVolleyballScoreKeeper/en
|
|
|
47
55
|
import { snookerScoreKeeper } from './tool/snookerScoreKeeper/entry';
|
|
48
56
|
import { rugbyScoreKeeper } from './tool/rugbyScoreKeeper/entry';
|
|
49
57
|
import { baseballScoreKeeper } from './tool/baseballScoreKeeper/entry';
|
|
50
|
-
|
|
58
|
+
import { penaltyShootout } from './tool/penaltyShootout/entry';
|
|
59
|
+
import { runningPacePredictor } from './tool/runningPacePredictor/entry';
|
|
60
|
+
import { swimCssCalculator } from './tool/swimCssCalculator/entry';
|
|
51
61
|
|
|
62
|
+
export const ALL_ENTRIES = [
|
|
63
|
+
basketScoreKeeper,
|
|
64
|
+
footballScoreKeeper,
|
|
65
|
+
gymTracker,
|
|
66
|
+
pingPongScoreKeeper,
|
|
67
|
+
reactionTester,
|
|
68
|
+
eloRatingCalculator,
|
|
69
|
+
scoreKeeper,
|
|
70
|
+
tournamentBracket,
|
|
71
|
+
tennisScoreKeeper,
|
|
72
|
+
dartsScoreKeeper,
|
|
73
|
+
padelScoreKeeper,
|
|
74
|
+
streetballScoreKeeper,
|
|
75
|
+
beachVolleyballScoreKeeper,
|
|
76
|
+
snookerScoreKeeper,
|
|
77
|
+
rugbyScoreKeeper,
|
|
78
|
+
baseballScoreKeeper,
|
|
79
|
+
penaltyShootout,
|
|
80
|
+
runningPacePredictor,
|
|
81
|
+
swimCssCalculator,
|
|
82
|
+
];
|
package/src/index.ts
CHANGED
|
@@ -31,3 +31,6 @@ export type { ReactionTesterUI, ReactionTesterLocaleContent } from './tool/react
|
|
|
31
31
|
|
|
32
32
|
export { ELO_RATING_CALCULATOR_TOOL, eloRatingCalculator } from './tool/eloRatingCalculator/index';
|
|
33
33
|
export type { EloRatingCalculatorUI, EloRatingCalculatorLocaleContent } from './tool/eloRatingCalculator/index';
|
|
34
|
+
|
|
35
|
+
export { PENALTY_SHOOTOUT_TOOL, penaltyShootout } from './tool/penaltyShootout/index';
|
|
36
|
+
export type { PenaltyShootoutUI, PenaltyShootoutLocaleContent } from './tool/penaltyShootout/index';
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
import type { ToolLocaleContent } from '../types';
|
|
3
4
|
|
|
4
|
-
describe('Locale Completeness Validation', () => {
|
|
5
|
-
it('all tools registered', () => {
|
|
6
|
-
expect(ALL_TOOLS.length).
|
|
5
|
+
describe('Locale Completeness and Slug Validation', () => {
|
|
6
|
+
it('all tools registered in ALL_TOOLS', () => {
|
|
7
|
+
expect(ALL_TOOLS.length).toBeGreaterThan(0);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
ALL_TOOLS.forEach((tool) => {
|
|
11
|
+
describe(`Tool entry: ${tool.entry.id}`, () => {
|
|
12
|
+
it('every registered locale content must have a valid non-empty slug', async () => {
|
|
13
|
+
const locales = Object.keys(tool.entry.i18n);
|
|
14
|
+
expect(locales.length).toBeGreaterThan(0);
|
|
15
|
+
|
|
16
|
+
for (const locale of locales) {
|
|
17
|
+
const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
|
|
18
|
+
expect(loader).toBeDefined();
|
|
19
|
+
const content = (await loader?.()) as ToolLocaleContent;
|
|
20
|
+
expect(content, `Locale "${locale}" for tool "${tool.entry.id}" failed to load content object`).toBeDefined();
|
|
21
|
+
expect(content.slug, `Locale "${locale}" for tool "${tool.entry.id}" has undefined or empty slug`).toBeDefined();
|
|
22
|
+
expect(typeof content.slug).toBe('string');
|
|
23
|
+
expect(content.slug.length).toBeGreaterThan(0);
|
|
24
|
+
expect(content.title, `Locale "${locale}" for tool "${tool.entry.id}" has undefined title`).toBeDefined();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
7
28
|
});
|
|
8
29
|
});
|
|
@@ -4,8 +4,8 @@ import { sportsCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).
|
|
7
|
+
it('should have tools registered in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBeGreaterThan(0);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('sportsCategory should be defined', () => {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { bibliographyEntries } from './bibliography';
|
|
3
|
+
---
|
|
4
|
+
|
|
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>
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { PenaltyShootoutUI } from './ui';
|
|
3
|
+
import './penalty-shootout.css';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
ui: PenaltyShootoutUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui: t } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="penalty-shootout-card" id="penalty-shootout-app">
|
|
13
|
+
<div class="ps-bar">
|
|
14
|
+
<span class="ps-bar-title">{t.roundLabel} <span id="ps-round-num">1</span></span>
|
|
15
|
+
<span class="ps-bar-title" id="ps-phase-label">{t.regularRounds}</span>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="ps-team ps-team-a ps-active-turn" id="ps-team-a-box">
|
|
19
|
+
<input type="text" class="ps-name" id="ps-team-a-name" value={t.teamALabel} aria-label={t.teamALabel} />
|
|
20
|
+
<div class="ps-score ps-score-a" id="ps-team-a-score">0</div>
|
|
21
|
+
<div class="ps-indicator-row" id="ps-team-a-indicators"></div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="ps-team ps-team-b" id="ps-team-b-box">
|
|
25
|
+
<input type="text" class="ps-name" id="ps-team-b-name" value={t.teamBLabel} aria-label={t.teamBLabel} />
|
|
26
|
+
<div class="ps-score ps-score-b" id="ps-team-b-score">0</div>
|
|
27
|
+
<div class="ps-indicator-row" id="ps-team-b-indicators"></div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="ps-status-banner" id="ps-status-banner">
|
|
31
|
+
<span id="ps-status-text">{t.turnLabel}: {t.teamALabel}</span>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="ps-controls" id="ps-action-controls">
|
|
35
|
+
<button class="ps-btn-shoot ps-btn-goal" id="ps-btn-goal">{t.scoreGoal}</button>
|
|
36
|
+
<button class="ps-btn-shoot ps-btn-miss" id="ps-btn-miss">{t.scoreMiss}</button>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="ps-secondary-row">
|
|
40
|
+
<button class="ps-btn-sub" id="ps-btn-undo">{t.undo}</button>
|
|
41
|
+
<button class="ps-btn-sub" id="ps-btn-reset">{t.reset}</button>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="ps-particles" id="ps-particles"></div>
|
|
45
|
+
|
|
46
|
+
<div class="ps-winner-overlay" id="ps-winner-overlay">
|
|
47
|
+
<div class="ps-winner-backdrop"></div>
|
|
48
|
+
<div class="ps-winner-card">
|
|
49
|
+
<div class="ps-trophy">CHAMPION</div>
|
|
50
|
+
<div class="ps-winner-title">{t.winnerTitle}</div>
|
|
51
|
+
<div class="ps-winner-name" id="ps-winner-name">Team Name</div>
|
|
52
|
+
<div class="ps-winner-details" id="ps-winner-details">Score: 5 - 3</div>
|
|
53
|
+
<button class="ps-winner-close-btn" id="ps-winner-close-btn">{t.reset}</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<script>
|
|
59
|
+
import { createInitialState, recordShot, undoShot } from './logic';
|
|
60
|
+
|
|
61
|
+
let state = createInitialState();
|
|
62
|
+
|
|
63
|
+
const teamABox = document.getElementById('ps-team-a-box');
|
|
64
|
+
const teamBBox = document.getElementById('ps-team-b-box');
|
|
65
|
+
const teamANameInput = document.getElementById('ps-team-a-name') as HTMLInputElement | null;
|
|
66
|
+
const teamBNameInput = document.getElementById('ps-team-b-name') as HTMLInputElement | null;
|
|
67
|
+
const teamAScore = document.getElementById('ps-team-a-score');
|
|
68
|
+
const teamBScore = document.getElementById('ps-team-b-score');
|
|
69
|
+
const teamAIndicators = document.getElementById('ps-team-a-indicators');
|
|
70
|
+
const teamBIndicators = document.getElementById('ps-team-b-indicators');
|
|
71
|
+
const statusBanner = document.getElementById('ps-status-banner');
|
|
72
|
+
const statusText = document.getElementById('ps-status-text');
|
|
73
|
+
const roundNum = document.getElementById('ps-round-num');
|
|
74
|
+
const phaseLabel = document.getElementById('ps-phase-label');
|
|
75
|
+
const particlesContainer = document.getElementById('ps-particles');
|
|
76
|
+
|
|
77
|
+
const winnerOverlay = document.getElementById('ps-winner-overlay');
|
|
78
|
+
const winnerName = document.getElementById('ps-winner-name');
|
|
79
|
+
const winnerDetails = document.getElementById('ps-winner-details');
|
|
80
|
+
const winnerCloseBtn = document.getElementById('ps-winner-close-btn');
|
|
81
|
+
|
|
82
|
+
const btnGoal = document.getElementById('ps-btn-goal') as HTMLButtonElement | null;
|
|
83
|
+
const btnMiss = document.getElementById('ps-btn-miss') as HTMLButtonElement | null;
|
|
84
|
+
const btnUndo = document.getElementById('ps-btn-undo');
|
|
85
|
+
const btnReset = document.getElementById('ps-btn-reset');
|
|
86
|
+
|
|
87
|
+
teamANameInput?.addEventListener('input', (e) => {
|
|
88
|
+
state.teamAName = (e.target as HTMLInputElement).value || 'Home Team';
|
|
89
|
+
render();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
teamBNameInput?.addEventListener('input', (e) => {
|
|
93
|
+
state.teamBName = (e.target as HTMLInputElement).value || 'Away Team';
|
|
94
|
+
render();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
function spawnParticle(text: string, type: 'goal' | 'miss') {
|
|
98
|
+
if (!particlesContainer) return;
|
|
99
|
+
const p = document.createElement('div');
|
|
100
|
+
p.className = `ps-p ps-p-${type}`;
|
|
101
|
+
p.textContent = text;
|
|
102
|
+
p.style.left = `${30 + Math.random() * 40}%`;
|
|
103
|
+
p.style.top = `${40 + Math.random() * 20}%`;
|
|
104
|
+
particlesContainer.appendChild(p);
|
|
105
|
+
setTimeout(() => p.remove(), 800);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function updateScoreDisplay() {
|
|
109
|
+
if (!teamAScore || !teamBScore || !roundNum || !phaseLabel) return;
|
|
110
|
+
teamAScore.textContent = String(state.teamAScore);
|
|
111
|
+
teamBScore.textContent = String(state.teamBScore);
|
|
112
|
+
roundNum.textContent = String(state.currentRound);
|
|
113
|
+
phaseLabel.textContent = state.currentRound > 5 ? 'Sudden Death' : 'Regular Round';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getShotSymbol(shot: string, index: number) {
|
|
117
|
+
if (shot === 'scored') return 'O';
|
|
118
|
+
if (shot === 'missed') return 'X';
|
|
119
|
+
return String(index + 1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderIndicators() {
|
|
123
|
+
if (!teamAIndicators || !teamBIndicators) return;
|
|
124
|
+
teamAIndicators.innerHTML = '';
|
|
125
|
+
state.teamAShoots.forEach((shot, i) => {
|
|
126
|
+
const circle = document.createElement('div');
|
|
127
|
+
circle.className = `ps-circle ps-${shot}`;
|
|
128
|
+
circle.textContent = getShotSymbol(shot, i);
|
|
129
|
+
teamAIndicators.appendChild(circle);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
teamBIndicators.innerHTML = '';
|
|
133
|
+
state.teamBShoots.forEach((shot, i) => {
|
|
134
|
+
const circle = document.createElement('div');
|
|
135
|
+
circle.className = `ps-circle ps-${shot}`;
|
|
136
|
+
circle.textContent = getShotSymbol(shot, i);
|
|
137
|
+
teamBIndicators.appendChild(circle);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function updateFinishedElements(winner: string) {
|
|
142
|
+
if (statusText) statusText.textContent = `${winner} Wins! (${state.winReason})`;
|
|
143
|
+
if (winnerName) winnerName.textContent = winner;
|
|
144
|
+
if (winnerDetails) winnerDetails.textContent = `Score: ${state.teamAScore} - ${state.teamBScore} (${state.winReason})`;
|
|
145
|
+
if (winnerOverlay) winnerOverlay.classList.add('ps-winner-show');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function renderFinishedState() {
|
|
149
|
+
teamABox?.classList.remove('ps-active-turn');
|
|
150
|
+
teamBBox?.classList.remove('ps-active-turn');
|
|
151
|
+
if (statusBanner) statusBanner.className = 'ps-status-banner ps-winner';
|
|
152
|
+
const winner = state.winner === 'A' ? state.teamAName : state.teamBName;
|
|
153
|
+
if (btnGoal) btnGoal.disabled = true;
|
|
154
|
+
if (btnMiss) btnMiss.disabled = true;
|
|
155
|
+
|
|
156
|
+
updateFinishedElements(winner);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function setActiveTurnBox(isTurnA: boolean) {
|
|
160
|
+
if (isTurnA) {
|
|
161
|
+
teamABox?.classList.add('ps-active-turn');
|
|
162
|
+
teamBBox?.classList.remove('ps-active-turn');
|
|
163
|
+
} else {
|
|
164
|
+
teamBBox?.classList.add('ps-active-turn');
|
|
165
|
+
teamABox?.classList.remove('ps-active-turn');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function renderActiveState() {
|
|
170
|
+
if (statusBanner) statusBanner.className = 'ps-status-banner';
|
|
171
|
+
if (btnGoal) btnGoal.disabled = false;
|
|
172
|
+
if (btnMiss) btnMiss.disabled = false;
|
|
173
|
+
winnerOverlay?.classList.remove('ps-winner-show');
|
|
174
|
+
|
|
175
|
+
const isTurnA = state.currentTurn === 'A';
|
|
176
|
+
setActiveTurnBox(isTurnA);
|
|
177
|
+
const activeName = isTurnA ? state.teamAName : state.teamBName;
|
|
178
|
+
if (statusText) statusText.textContent = `Turn: ${activeName}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function render() {
|
|
182
|
+
updateScoreDisplay();
|
|
183
|
+
renderIndicators();
|
|
184
|
+
|
|
185
|
+
if (state.isFinished) {
|
|
186
|
+
renderFinishedState();
|
|
187
|
+
} else {
|
|
188
|
+
renderActiveState();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
btnGoal?.addEventListener('click', () => {
|
|
193
|
+
const turn = state.currentTurn;
|
|
194
|
+
state = recordShot(state, 'scored');
|
|
195
|
+
spawnParticle('GOAL!', 'goal');
|
|
196
|
+
const targetScore = turn === 'A' ? teamAScore : teamBScore;
|
|
197
|
+
targetScore?.classList.add('ps-score-boom');
|
|
198
|
+
setTimeout(() => targetScore?.classList.remove('ps-score-boom'), 500);
|
|
199
|
+
render();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
btnMiss?.addEventListener('click', () => {
|
|
203
|
+
state = recordShot(state, 'missed');
|
|
204
|
+
spawnParticle('MISS!', 'miss');
|
|
205
|
+
render();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
btnUndo?.addEventListener('click', () => {
|
|
209
|
+
state = undoShot(state);
|
|
210
|
+
render();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
btnReset?.addEventListener('click', () => {
|
|
214
|
+
const nameA = teamANameInput?.value || 'Home Team';
|
|
215
|
+
const nameB = teamBNameInput?.value || 'Away Team';
|
|
216
|
+
state = createInitialState(nameA, nameB);
|
|
217
|
+
render();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
winnerCloseBtn?.addEventListener('click', () => {
|
|
221
|
+
const nameA = teamANameInput?.value || 'Home Team';
|
|
222
|
+
const nameB = teamBNameInput?.value || 'Away Team';
|
|
223
|
+
state = createInitialState(nameA, nameB);
|
|
224
|
+
render();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
render();
|
|
228
|
+
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SportsToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { PenaltyShootoutUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type { PenaltyShootoutUI };
|
|
5
|
+
export type PenaltyShootoutLocaleContent = ToolLocaleContent<PenaltyShootoutUI>;
|
|
6
|
+
|
|
7
|
+
export const penaltyShootout: SportsToolEntry<PenaltyShootoutUI> = {
|
|
8
|
+
id: 'penalty-shootout-calculator',
|
|
9
|
+
icons: {
|
|
10
|
+
bg: 'mdi:soccer',
|
|
11
|
+
fg: 'mdi:scoreboard-outline',
|
|
12
|
+
},
|
|
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
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { bibliographyEntries } from '../bibliography';
|
|
2
|
+
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
4
|
+
import type { PenaltyShootoutUI } from '../ui';
|
|
5
|
+
|
|
6
|
+
const slug = 'elfmeterschiessen-rechner';
|
|
7
|
+
const title = 'Elfmeterschießen Rechner: Live Fußball Ergebnisstander';
|
|
8
|
+
const description =
|
|
9
|
+
'Verfolgen Sie Fußball-Elfmeterschießen in Echtzeit. Mit 5-Schuss-Anzeige, mathematischem Ausscheidungsmodus, Sudden Death und Sieger-Animation.';
|
|
10
|
+
|
|
11
|
+
const faqData = [
|
|
12
|
+
{
|
|
13
|
+
question: 'Wann endet ein Elfmeterschießen vorzeitig?',
|
|
14
|
+
answer:
|
|
15
|
+
'Ein Elfmeterschießen endet, sobald eine Mannschaft einen Vorsprung erzielt, den die andere Mannschaft mit den verbleibenden Schüssen mathematisch nicht mehr einholen kann.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
question: 'Wie funktioniert Sudden Death beim Elfmeterschießen?',
|
|
19
|
+
answer:
|
|
20
|
+
'Steht es nach jeweils 5 Schüssen unentschieden, wird das Schießen einzeln fortgesetzt, bis ein Team trifft und das andere im selben Durchgang verschießt.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
question: 'Wer schießt beim Elfmeterschießen zuerst?',
|
|
24
|
+
answer:
|
|
25
|
+
'Der Schiedsrichter wirft eine Münze zur Wahl des Tors und eine zweite Münze zur Entscheidung, welches Team zuerst antritt.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
question: 'Kann der Torwart während des Elfmeterschießens ausgewechselt werden?',
|
|
29
|
+
answer:
|
|
30
|
+
'Ein verletzter Torwart kann durch einen benannten Auswechselspieler ersetzt werden, sofern das Team sein Auswechselkontingent noch nicht ausgeschöpft hat.',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const howToData = [
|
|
35
|
+
{
|
|
36
|
+
name: 'Teamnamen eingeben',
|
|
37
|
+
text: 'Geben Sie die Wunschnamen der beiden Mannschaften in die Eingabefelder ein.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Schüsse protokollieren',
|
|
41
|
+
text: 'Klicken Sie nach jedem Schuss auf TOR oder FEHL. Die App aktualisiert Spielstand, Rundenanzeige und Schussrecht automatisch.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'Sudden-Death-Modus',
|
|
45
|
+
text: 'Bei Gleichstand nach 5 Schüssen wechselt der Rechner automatisch in den Sudden-Death-Modus.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Sieger verkünden',
|
|
49
|
+
text: 'Bei rechnerischem Sieg oder Entscheidung im Sudden Death verkündet ein Pokal-Overlay das Siegerteam.',
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const faqSchema: WithContext<FAQPage> = {
|
|
54
|
+
'@context': 'https://schema.org',
|
|
55
|
+
'@type': 'FAQPage',
|
|
56
|
+
mainEntity: faqData.map((item) => ({
|
|
57
|
+
'@type': 'Question',
|
|
58
|
+
name: item.question,
|
|
59
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
60
|
+
})),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const howToSchema: WithContext<HowTo> = {
|
|
64
|
+
'@context': 'https://schema.org',
|
|
65
|
+
'@type': 'HowTo',
|
|
66
|
+
name: title,
|
|
67
|
+
description,
|
|
68
|
+
step: howToData.map((step, i) => ({
|
|
69
|
+
'@type': 'HowToStep',
|
|
70
|
+
position: i + 1,
|
|
71
|
+
name: step.name,
|
|
72
|
+
text: step.text,
|
|
73
|
+
})),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const appSchema: WithContext<SoftwareApplication> = {
|
|
77
|
+
'@context': 'https://schema.org',
|
|
78
|
+
'@type': 'SoftwareApplication',
|
|
79
|
+
name: title,
|
|
80
|
+
description,
|
|
81
|
+
applicationCategory: 'SportsApplication',
|
|
82
|
+
operatingSystem: 'All',
|
|
83
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
|
84
|
+
inLanguage: 'de',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const content: ToolLocaleContent<PenaltyShootoutUI> = {
|
|
88
|
+
slug,
|
|
89
|
+
title,
|
|
90
|
+
description,
|
|
91
|
+
faq: faqData,
|
|
92
|
+
bibliography: bibliographyEntries,
|
|
93
|
+
howTo: howToData,
|
|
94
|
+
schemas: [faqSchema, howToSchema, appSchema],
|
|
95
|
+
seo: [
|
|
96
|
+
{
|
|
97
|
+
type: 'title',
|
|
98
|
+
text: 'Offizielle IFAB-Regeln für das Elfmeterschießen',
|
|
99
|
+
level: 2,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: 'paragraph',
|
|
103
|
+
html: 'Das Elfmeterschießen (offiziell <em>Schüsse von der Strafstoßmarke</em>) entscheidet nach Regel 10 der IFAB-Spielregeln über den Sieger eines unentschiedenen K.-o.-Spiels.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: 'stats',
|
|
107
|
+
columns: 4,
|
|
108
|
+
items: [
|
|
109
|
+
{ value: '5', label: 'Schüsse zu Beginn' },
|
|
110
|
+
{ value: '11m', label: 'Entfernung zum Tor' },
|
|
111
|
+
{ value: '1v1', label: 'Schütze vs Torwart' },
|
|
112
|
+
{ value: 'ABBA / AB', label: 'Schussreihenfolge' },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
type: 'tip',
|
|
117
|
+
title: 'Mathematische Ausscheidungsregel',
|
|
118
|
+
html: 'Hat ein Team mehr Tore erzielt als dem gegnerischen Team Schüsse verbleiben, beendet der Schiedsrichter das Schießen sofort.',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
type: 'title',
|
|
122
|
+
text: 'Reguläre Phase vs Sudden Death im Vergleich',
|
|
123
|
+
level: 2,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: 'comparative',
|
|
127
|
+
columns: 2,
|
|
128
|
+
items: [
|
|
129
|
+
{
|
|
130
|
+
title: 'Reguläre Phase (5 Schüsse)',
|
|
131
|
+
description: 'Jedes Team führt 5 Schüsse abwechselnd aus. Vorzeitiges Ende nur bei mathematischer Unmöglichkeit.',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
title: 'Sudden Death Phase',
|
|
135
|
+
description: 'Einzelne Schussdurchgänge ab Runden 6. Jede Tordifferenz nach gleicher Schusszahl bringt die Entscheidung.',
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'title',
|
|
141
|
+
text: 'Wichtige IFAB-Bestimmungen',
|
|
142
|
+
level: 2,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: 'table',
|
|
146
|
+
headers: ['Regel / Bestimmung', 'Offizieller IFAB-Standard'],
|
|
147
|
+
rows: [
|
|
148
|
+
['Teilnahmeberechtigt', 'Nur Spieler, die bei Spielende auf dem Feld stehen, dürfen Schüsse ausführen.'],
|
|
149
|
+
['Position des Torwarts', 'Muss im Moment des Schusses mit mindestens einem Teil eines Fußes die Torlinie berühren.'],
|
|
150
|
+
['Täuschen beim Anlauf', 'Täuschen im Anlauf erlaubt; Täuschen nach Abschluss des Anlaufs wird geahndet.'],
|
|
151
|
+
['Gleichzahl der Spieler', 'Wurde ein Spieler des Gegners feldverwiesen, muss das andere Team seine Spielerzahl anpassen.'],
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
type: 'title',
|
|
156
|
+
text: 'Vor und Nachteile des Elfmeterschießens',
|
|
157
|
+
level: 2,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
type: 'proscons',
|
|
161
|
+
title: 'Bewertung des Formats',
|
|
162
|
+
items: [
|
|
163
|
+
{
|
|
164
|
+
pro: 'Garantiert einen eindeutigen Sieger in einem planbaren Zeitrahmen.',
|
|
165
|
+
con: 'Hoher psychologischer Druck kann die Leistung der 120 Spielminuten in den Schatten stellen.',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
pro: 'Bietet höchste Spannung für Fans und Zuschauer.',
|
|
169
|
+
con: 'Fehlschüsse einzelner Akteure werden oft übermäßig belastet.',
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
ui: {
|
|
175
|
+
teamALabel: 'Heimmannschaft',
|
|
176
|
+
teamBLabel: 'Auswärtsmannschaft',
|
|
177
|
+
scoreGoal: 'TOR',
|
|
178
|
+
scoreMiss: 'FEHL',
|
|
179
|
+
undo: 'Rückgängig',
|
|
180
|
+
reset: 'Zurücksetzen',
|
|
181
|
+
suddenDeath: 'Sudden Death',
|
|
182
|
+
regularRounds: 'Reguläre Runde',
|
|
183
|
+
roundLabel: 'Runde',
|
|
184
|
+
turnLabel: 'Am Schuss',
|
|
185
|
+
winnerTitle: 'Sieger steht fest',
|
|
186
|
+
unreachableLead: 'Unerreichbarer Vorsprung',
|
|
187
|
+
regularRoundsWin: 'Sieg nach 5 regulären Runden',
|
|
188
|
+
suddenDeathWin: 'Sieg im Sudden Death',
|
|
189
|
+
statusPending: 'Offen',
|
|
190
|
+
statusScored: 'Treffer',
|
|
191
|
+
statusMissed: 'Verschossen',
|
|
192
|
+
},
|
|
193
|
+
};
|