@jjlmoya/utils-sports 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.
- package/package.json +62 -0
- package/src/category/i18n/en.ts +108 -0
- package/src/category/i18n/es.ts +108 -0
- package/src/category/i18n/fr.ts +95 -0
- package/src/category/index.ts +21 -0
- package/src/category/seo.astro +15 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +11 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +55 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +146 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +19 -0
- package/src/tests/locale_completeness.test.ts +42 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/schemas_fulfillment.test.ts +23 -0
- package/src/tests/seo_length.test.ts +22 -0
- package/src/tests/title_quality.test.ts +55 -0
- package/src/tests/tool_validation.test.ts +17 -0
- package/src/tool/gymTracker/bibliography.astro +15 -0
- package/src/tool/gymTracker/component.astro +835 -0
- package/src/tool/gymTracker/exercises.ts +28 -0
- package/src/tool/gymTracker/i18n/en.ts +225 -0
- package/src/tool/gymTracker/i18n/es.ts +225 -0
- package/src/tool/gymTracker/i18n/fr.ts +225 -0
- package/src/tool/gymTracker/index.ts +34 -0
- package/src/tool/gymTracker/logic.ts +169 -0
- package/src/tool/gymTracker/seo.astro +15 -0
- package/src/tool/gymTracker/storage.ts +43 -0
- package/src/tool/gymTracker/timer.ts +126 -0
- package/src/tool/gymTracker/types.ts +11 -0
- package/src/tool/gymTracker/ui-utils.ts +59 -0
- package/src/tool/gymTracker/ui.ts +27 -0
- package/src/tool/reactionTester/bibliography.astro +2 -0
- package/src/tool/reactionTester/component.astro +1074 -0
- package/src/tool/reactionTester/i18n/en.ts +144 -0
- package/src/tool/reactionTester/i18n/es.ts +144 -0
- package/src/tool/reactionTester/i18n/fr.ts +144 -0
- package/src/tool/reactionTester/index.ts +34 -0
- package/src/tool/reactionTester/seo.astro +12 -0
- package/src/tool/reactionTester/ui.ts +43 -0
- package/src/tool/scoreKeeper/bibliography.astro +14 -0
- package/src/tool/scoreKeeper/component.astro +858 -0
- package/src/tool/scoreKeeper/i18n/en.ts +207 -0
- package/src/tool/scoreKeeper/i18n/es.ts +207 -0
- package/src/tool/scoreKeeper/i18n/fr.ts +207 -0
- package/src/tool/scoreKeeper/index.ts +35 -0
- package/src/tool/scoreKeeper/logic.ts +275 -0
- package/src/tool/scoreKeeper/seo.astro +15 -0
- package/src/tool/scoreKeeper/sports.ts +70 -0
- package/src/tool/scoreKeeper/ui.ts +19 -0
- package/src/tool/tournamentBracket/bibliography.astro +10 -0
- package/src/tool/tournamentBracket/component.astro +1092 -0
- package/src/tool/tournamentBracket/i18n/en.ts +160 -0
- package/src/tool/tournamentBracket/i18n/es.ts +178 -0
- package/src/tool/tournamentBracket/i18n/fr.ts +160 -0
- package/src/tool/tournamentBracket/index.ts +34 -0
- package/src/tool/tournamentBracket/logic/active.controller.ts +106 -0
- package/src/tool/tournamentBracket/logic/generator.ts +71 -0
- package/src/tool/tournamentBracket/logic/manager.ts +165 -0
- package/src/tool/tournamentBracket/logic/setup.controller.ts +84 -0
- package/src/tool/tournamentBracket/logic/sharing.ts +81 -0
- package/src/tool/tournamentBracket/logic/storage.ts +56 -0
- package/src/tool/tournamentBracket/models.ts +34 -0
- package/src/tool/tournamentBracket/seo.astro +12 -0
- package/src/tool/tournamentBracket/tournament.controller.ts +65 -0
- package/src/tool/tournamentBracket/tournament.renderer.ts +45 -0
- package/src/tool/tournamentBracket/ui/bracket-desktop.ts +143 -0
- package/src/tool/tournamentBracket/ui/bracket-mobile.ts +82 -0
- package/src/tool/tournamentBracket/ui/mediator.ts +96 -0
- package/src/tool/tournamentBracket/ui/navigator.ts +84 -0
- package/src/tool/tournamentBracket/ui/setup.ts +120 -0
- package/src/tool/tournamentBracket/ui.ts +42 -0
- package/src/tools.ts +13 -0
- package/src/types.ts +72 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { SPORTS, SPORT_NAME_KEYS } from './sports';
|
|
2
|
+
import type { SportConfig, SportId } from './sports';
|
|
3
|
+
import type { ScoreKeeperUI } from './ui';
|
|
4
|
+
|
|
5
|
+
const g = (id: string) => document.getElementById(id);
|
|
6
|
+
class GameManager {
|
|
7
|
+
currentSport: SportConfig;
|
|
8
|
+
t: ScoreKeeperUI;
|
|
9
|
+
scoreA = 0; scoreB = 0;
|
|
10
|
+
setsA = 0; setsB = 0;
|
|
11
|
+
gamesA = 0; gamesB = 0;
|
|
12
|
+
isTieBreak = false;
|
|
13
|
+
server: 'A' | 'B' | null = null;
|
|
14
|
+
pointsSinceServeChange = 0; hasShownWin = false;
|
|
15
|
+
ui = {
|
|
16
|
+
select: g('sport-select') as HTMLSelectElement,
|
|
17
|
+
scoreA: g('score-a'), scoreB: g('score-b'),
|
|
18
|
+
touchLayerA: g('touch-layer-a'), touchLayerB: g('touch-layer-b'),
|
|
19
|
+
statsA: g('stats-a'), statsB: g('stats-b'),
|
|
20
|
+
setsA: g('sets-a'), setsB: g('sets-b'),
|
|
21
|
+
gamesA: g('games-a'), gamesB: g('games-b'),
|
|
22
|
+
serveA: g('serve-a'), serveB: g('serve-b'),
|
|
23
|
+
modal: g('winner-modal'), winnerName: g('winner-name'),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
const root = g('scoreboard-app');
|
|
28
|
+
this.t = JSON.parse(root?.dataset.skUi ?? '{}') as ScoreKeeperUI;
|
|
29
|
+
this.currentSport = SPORTS.simple;
|
|
30
|
+
this.populateSportSelect();
|
|
31
|
+
this.bindEvents();
|
|
32
|
+
this.setSport('simple');
|
|
33
|
+
(window as unknown as Record<string, unknown>).game = this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private populateSportSelect() {
|
|
37
|
+
Object.entries(SPORTS).forEach(([id, s]) => {
|
|
38
|
+
const o = document.createElement('option');
|
|
39
|
+
o.value = s.id;
|
|
40
|
+
o.text = this.t[SPORT_NAME_KEYS[id as SportId]] || id;
|
|
41
|
+
this.ui.select.appendChild(o);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private bindEvents() {
|
|
46
|
+
this.ui.select.addEventListener('change', (e) => {
|
|
47
|
+
this.setSport((e.target as HTMLSelectElement).value as SportId);
|
|
48
|
+
});
|
|
49
|
+
g('btn-reset')?.addEventListener('click', () => {
|
|
50
|
+
if (confirm(this.t.resetConfirm)) this.reset();
|
|
51
|
+
});
|
|
52
|
+
g('btn-swap')?.addEventListener('click', () => this.swap());
|
|
53
|
+
g('btn-modal-reset')?.addEventListener('click', () => {
|
|
54
|
+
this.reset(); this.ui.modal?.classList.add('sk-hidden');
|
|
55
|
+
});
|
|
56
|
+
g('btn-modal-continue')?.addEventListener('click', () => {
|
|
57
|
+
this.ui.modal?.classList.add('sk-hidden');
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setSport(id: string) {
|
|
62
|
+
this.currentSport = SPORTS[id as SportId] || SPORTS.simple;
|
|
63
|
+
this.reset();
|
|
64
|
+
this.updateLayout();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private updateLayout() {
|
|
68
|
+
this.setupTouchLayer(this.ui.touchLayerA, 'A');
|
|
69
|
+
this.setupTouchLayer(this.ui.touchLayerB, 'B');
|
|
70
|
+
this.toggleStats(this.currentSport.hasSets ?? this.currentSport.hasGames ?? false);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private setupTouchLayer(layer: HTMLElement | null, team: 'A' | 'B') {
|
|
74
|
+
if (!layer) return;
|
|
75
|
+
layer.innerHTML = '';
|
|
76
|
+
const incs = this.currentSport.increments;
|
|
77
|
+
if (incs.length === 1) {
|
|
78
|
+
layer.className = 'sk-touch-layer';
|
|
79
|
+
layer.appendChild(this.buildTouchBtn(team, incs[0]!, false));
|
|
80
|
+
} else {
|
|
81
|
+
layer.className = 'sk-touch-layer sk-touch-layer-multi';
|
|
82
|
+
incs.forEach((val) => layer.appendChild(this.buildTouchBtn(team, val, true)));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private buildTouchBtn(team: 'A' | 'B', val: number, multi: boolean): HTMLButtonElement {
|
|
87
|
+
const btn = document.createElement('button');
|
|
88
|
+
if (multi) {
|
|
89
|
+
btn.className = 'sk-plus-btn'; btn.innerText = `+${val}`;
|
|
90
|
+
btn.onclick = (e) => { e.stopPropagation(); this.addScore(team, val); };
|
|
91
|
+
} else {
|
|
92
|
+
btn.className = 'sk-touch-btn'; btn.ariaLabel = `+${val}`;
|
|
93
|
+
btn.onclick = () => this.addScore(team, val);
|
|
94
|
+
}
|
|
95
|
+
return btn;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private toggleStats(show: boolean) {
|
|
99
|
+
this.ui.statsA?.classList.toggle('sk-hidden', !show);
|
|
100
|
+
this.ui.statsB?.classList.toggle('sk-hidden', !show);
|
|
101
|
+
}
|
|
102
|
+
private findNumericWinner(): 'A' | 'B' | null {
|
|
103
|
+
const max = this.currentSport.maxScore!;
|
|
104
|
+
const by = this.currentSport.winBy ?? 1;
|
|
105
|
+
if (this.scoreA >= max && this.scoreA - this.scoreB >= by) return 'A';
|
|
106
|
+
if (this.scoreB >= max && this.scoreB - this.scoreA >= by) return 'B';
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
private checkWinCondition() {
|
|
110
|
+
if (this.hasShownWin || !this.currentSport.maxScore) return;
|
|
111
|
+
const w = this.findNumericWinner();
|
|
112
|
+
if (w) this.triggerWin(w);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private triggerWin(winner: 'A' | 'B') {
|
|
116
|
+
this.hasShownWin = true;
|
|
117
|
+
const inputs = document.querySelectorAll('input[type="text"]');
|
|
118
|
+
let name = winner === 'A' ? this.t.playerA : this.t.playerB;
|
|
119
|
+
const el = inputs.length === 2 ? inputs[winner === 'A' ? 0 : 1] as HTMLInputElement : null;
|
|
120
|
+
if (el?.value.trim()) name = el.value;
|
|
121
|
+
if (this.ui.winnerName) this.ui.winnerName.innerText = name;
|
|
122
|
+
this.ui.modal?.classList.remove('sk-hidden');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
reset() {
|
|
126
|
+
this.scoreA = 0; this.scoreB = 0;
|
|
127
|
+
this.setsA = 0; this.setsB = 0;
|
|
128
|
+
this.gamesA = 0; this.gamesB = 0;
|
|
129
|
+
this.isTieBreak = false;
|
|
130
|
+
this.pointsSinceServeChange = 0; this.hasShownWin = false;
|
|
131
|
+
this.ui.modal?.classList.add('sk-hidden');
|
|
132
|
+
this.render();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
addScore(team: 'A' | 'B', val: number) {
|
|
136
|
+
if (this.currentSport.type === 'tennis') {
|
|
137
|
+
this.handleTennisPoint(team);
|
|
138
|
+
} else {
|
|
139
|
+
this.addNumericScore(team, val);
|
|
140
|
+
}
|
|
141
|
+
this.render();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private addNumericScore(team: 'A' | 'B', val: number) {
|
|
145
|
+
if (team === 'A') this.scoreA += val; else this.scoreB += val;
|
|
146
|
+
this.animate(team === 'A' ? this.ui.scoreA : this.ui.scoreB);
|
|
147
|
+
this.checkWinCondition();
|
|
148
|
+
if (this.currentSport.serviceRotationPoints && this.server) this.tickServiceRotation();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private tickServiceRotation() {
|
|
152
|
+
this.pointsSinceServeChange++;
|
|
153
|
+
let limit = this.currentSport.serviceRotationPoints!;
|
|
154
|
+
if (this.currentSport.id === 'pingpong' && this.scoreA >= 10 && this.scoreB >= 10) limit = 1;
|
|
155
|
+
if (this.pointsSinceServeChange >= limit) this.swapServe();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private handleTennisPoint(winner: 'A' | 'B') {
|
|
159
|
+
this.animate(winner === 'A' ? this.ui.scoreA : this.ui.scoreB);
|
|
160
|
+
if (this.isTieBreak) { this.handleTieBreakPoint(winner); }
|
|
161
|
+
else { this.handleNormalGamePoint(winner); }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private handleTieBreakPoint(winner: 'A' | 'B') {
|
|
165
|
+
if (winner === 'A') this.scoreA++; else this.scoreB++;
|
|
166
|
+
if ((this.scoreA + this.scoreB) % 2 === 1) this.swapServe();
|
|
167
|
+
const wScore = winner === 'A' ? this.scoreA : this.scoreB;
|
|
168
|
+
const lScore = winner === 'A' ? this.scoreB : this.scoreA;
|
|
169
|
+
if (wScore >= 7 && wScore >= lScore + 2) this.winSet(winner);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private handleNormalGamePoint(winner: 'A' | 'B') {
|
|
173
|
+
const wv = winner === 'A' ? this.scoreA : this.scoreB;
|
|
174
|
+
const lv = winner === 'A' ? this.scoreB : this.scoreA;
|
|
175
|
+
if (lv >= 4) { this.scoreA = 3; this.scoreB = 3; return; }
|
|
176
|
+
if (wv === 4 || (wv === 3 && lv < 3)) { this.winGame(winner); return; }
|
|
177
|
+
if (winner === 'A') this.scoreA++; else this.scoreB++;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private tryWinSet(team: 'A' | 'B', w: number, l: number): boolean {
|
|
181
|
+
if ((w === 6 && l <= 4) || (w === 7 && l === 5)) {
|
|
182
|
+
if (team === 'A') this.gamesA = w; else this.gamesB = w;
|
|
183
|
+
this.winSet(team); return true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private winGame(winner: 'A' | 'B') {
|
|
189
|
+
const gA = winner === 'A' ? this.gamesA + 1 : this.gamesA;
|
|
190
|
+
const gB = winner === 'B' ? this.gamesB + 1 : this.gamesB;
|
|
191
|
+
this.scoreA = 0; this.scoreB = 0; this.swapServe();
|
|
192
|
+
if (this.tryWinSet('A', gA, gB) || this.tryWinSet('B', gB, gA)) return;
|
|
193
|
+
if (gA === 6 && gB === 6) {
|
|
194
|
+
this.gamesA = 6; this.gamesB = 6; this.isTieBreak = true; this.render(); return;
|
|
195
|
+
}
|
|
196
|
+
this.gamesA = gA; this.gamesB = gB;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
winSet(winner: 'A' | 'B') {
|
|
200
|
+
if (winner === 'A') this.setsA++; else this.setsB++;
|
|
201
|
+
this.gamesA = 0; this.gamesB = 0; this.scoreA = 0; this.scoreB = 0;
|
|
202
|
+
this.isTieBreak = false; this.swapServe();
|
|
203
|
+
}
|
|
204
|
+
private swapServe() {
|
|
205
|
+
if (!this.server) return;
|
|
206
|
+
this.server = this.server === 'A' ? 'B' : 'A';
|
|
207
|
+
this.pointsSinceServeChange = 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
adjScore(team: 'A' | 'B', val: number) {
|
|
211
|
+
if (team === 'A') this.scoreA = Math.max(0, this.scoreA + val);
|
|
212
|
+
else this.scoreB = Math.max(0, this.scoreB + val);
|
|
213
|
+
this.render();
|
|
214
|
+
}
|
|
215
|
+
adjSet(team: 'A' | 'B', val: number) {
|
|
216
|
+
if (team === 'A') this.setsA = Math.max(0, this.setsA + val);
|
|
217
|
+
else this.setsB = Math.max(0, this.setsB + val);
|
|
218
|
+
this.render();
|
|
219
|
+
}
|
|
220
|
+
adjGame(team: 'A' | 'B', val: number) {
|
|
221
|
+
if (team === 'A') this.gamesA = Math.max(0, this.gamesA + val);
|
|
222
|
+
else this.gamesB = Math.max(0, this.gamesB + val);
|
|
223
|
+
this.render();
|
|
224
|
+
}
|
|
225
|
+
setServe(team: 'A' | 'B') {
|
|
226
|
+
this.server = team; this.pointsSinceServeChange = 0; this.render();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
swap() {
|
|
230
|
+
[this.scoreA, this.scoreB] = [this.scoreB, this.scoreA];
|
|
231
|
+
[this.setsA, this.setsB] = [this.setsB, this.setsA];
|
|
232
|
+
[this.gamesA, this.gamesB] = [this.gamesB, this.gamesA];
|
|
233
|
+
if (this.server) this.server = this.server === 'A' ? 'B' : 'A';
|
|
234
|
+
const inputs = document.querySelectorAll('input[type="text"]');
|
|
235
|
+
if (inputs.length === 2) {
|
|
236
|
+
const [i1, i2] = [inputs[0] as HTMLInputElement, inputs[1] as HTMLInputElement];
|
|
237
|
+
[i1.value, i2.value] = [i2.value, i1.value];
|
|
238
|
+
}
|
|
239
|
+
this.render();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private animate(el: HTMLElement | null) {
|
|
243
|
+
if (!el) return;
|
|
244
|
+
el.classList.add('sk-score-pop');
|
|
245
|
+
setTimeout(() => el.classList.remove('sk-score-pop'), 100);
|
|
246
|
+
}
|
|
247
|
+
private getTennisLabel(val: number): string | number {
|
|
248
|
+
if (this.isTieBreak) return val;
|
|
249
|
+
return (['0', '15', '30', '40', 'AD'] as const)[val] ?? val;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private renderScores() {
|
|
253
|
+
const isTennis = this.currentSport.type === 'tennis';
|
|
254
|
+
const lA = isTennis ? String(this.getTennisLabel(this.scoreA)) : String(this.scoreA);
|
|
255
|
+
const lB = isTennis ? String(this.getTennisLabel(this.scoreB)) : String(this.scoreB);
|
|
256
|
+
if (this.ui.scoreA) this.ui.scoreA.innerText = lA;
|
|
257
|
+
if (this.ui.scoreB) this.ui.scoreB.innerText = lB;
|
|
258
|
+
const tiebreak = isTennis && this.isTieBreak;
|
|
259
|
+
this.ui.scoreA?.classList.toggle('sk-score-tiebreak', tiebreak);
|
|
260
|
+
this.ui.scoreB?.classList.toggle('sk-score-tiebreak', tiebreak);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
render() {
|
|
264
|
+
this.renderScores();
|
|
265
|
+
if (this.ui.setsA) this.ui.setsA.innerText = String(this.setsA);
|
|
266
|
+
if (this.ui.setsB) this.ui.setsB.innerText = String(this.setsB);
|
|
267
|
+
if (this.ui.gamesA) this.ui.gamesA.innerText = String(this.gamesA);
|
|
268
|
+
if (this.ui.gamesB) this.ui.gamesB.innerText = String(this.gamesB);
|
|
269
|
+
this.ui.serveA?.classList.toggle('sk-serve-hidden', this.server !== 'A');
|
|
270
|
+
this.ui.serveB?.classList.toggle('sk-serve-hidden', this.server !== 'B');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
export function initScoreKeeper() {
|
|
274
|
+
document.addEventListener('DOMContentLoaded', () => new GameManager());
|
|
275
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { scoreKeeper } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'es' } = Astro.props;
|
|
11
|
+
const content = await scoreKeeper.i18n[locale]?.();
|
|
12
|
+
if (!content) return;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<SEORenderer content={{ locale: locale as string, sections: content.seo }} />
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type SportId = 'simple' | 'tennis' | 'padel' | 'pingpong' | 'volleyball' | 'basket';
|
|
2
|
+
|
|
3
|
+
export type ScoreType = 'numeric' | 'tennis';
|
|
4
|
+
|
|
5
|
+
export interface SportConfig {
|
|
6
|
+
id: SportId;
|
|
7
|
+
type: ScoreType;
|
|
8
|
+
maxScore?: number;
|
|
9
|
+
winBy?: number;
|
|
10
|
+
hasSets?: boolean;
|
|
11
|
+
hasGames?: boolean;
|
|
12
|
+
serviceRotationPoints?: number;
|
|
13
|
+
increments: number[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SPORT_NAME_KEYS: Record<SportId, string> = {
|
|
17
|
+
simple: 'sportSimple',
|
|
18
|
+
tennis: 'sportTennis',
|
|
19
|
+
padel: 'sportPadel',
|
|
20
|
+
pingpong: 'sportPingpong',
|
|
21
|
+
volleyball: 'sportVolleyball',
|
|
22
|
+
basket: 'sportBasket',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const SPORTS: Record<SportId, SportConfig> = {
|
|
26
|
+
simple: {
|
|
27
|
+
id: 'simple',
|
|
28
|
+
type: 'numeric',
|
|
29
|
+
increments: [1],
|
|
30
|
+
serviceRotationPoints: 0,
|
|
31
|
+
},
|
|
32
|
+
tennis: {
|
|
33
|
+
id: 'tennis',
|
|
34
|
+
type: 'tennis',
|
|
35
|
+
hasSets: true,
|
|
36
|
+
hasGames: true,
|
|
37
|
+
increments: [1],
|
|
38
|
+
serviceRotationPoints: 0,
|
|
39
|
+
},
|
|
40
|
+
padel: {
|
|
41
|
+
id: 'padel',
|
|
42
|
+
type: 'tennis',
|
|
43
|
+
hasSets: true,
|
|
44
|
+
hasGames: true,
|
|
45
|
+
increments: [1],
|
|
46
|
+
serviceRotationPoints: 0,
|
|
47
|
+
},
|
|
48
|
+
pingpong: {
|
|
49
|
+
id: 'pingpong',
|
|
50
|
+
type: 'numeric',
|
|
51
|
+
maxScore: 11,
|
|
52
|
+
winBy: 2,
|
|
53
|
+
increments: [1],
|
|
54
|
+
serviceRotationPoints: 2,
|
|
55
|
+
},
|
|
56
|
+
volleyball: {
|
|
57
|
+
id: 'volleyball',
|
|
58
|
+
type: 'numeric',
|
|
59
|
+
maxScore: 25,
|
|
60
|
+
winBy: 2,
|
|
61
|
+
increments: [1],
|
|
62
|
+
serviceRotationPoints: 1,
|
|
63
|
+
},
|
|
64
|
+
basket: {
|
|
65
|
+
id: 'basket',
|
|
66
|
+
type: 'numeric',
|
|
67
|
+
increments: [1, 2, 3],
|
|
68
|
+
serviceRotationPoints: 0,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ScoreKeeperUI extends Record<string, string> {
|
|
2
|
+
playerA: string;
|
|
3
|
+
playerB: string;
|
|
4
|
+
swapSides: string;
|
|
5
|
+
reset: string;
|
|
6
|
+
serve: string;
|
|
7
|
+
sets: string;
|
|
8
|
+
games: string;
|
|
9
|
+
victory: string;
|
|
10
|
+
newGame: string;
|
|
11
|
+
continueGame: string;
|
|
12
|
+
resetConfirm: string;
|
|
13
|
+
sportSimple: string;
|
|
14
|
+
sportTennis: string;
|
|
15
|
+
sportPadel: string;
|
|
16
|
+
sportPingpong: string;
|
|
17
|
+
sportVolleyball: string;
|
|
18
|
+
sportBasket: string;
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { tournamentBracket } from './index';
|
|
3
|
+
import type { KnownLocale } from '../../types';
|
|
4
|
+
|
|
5
|
+
interface Props { locale: KnownLocale }
|
|
6
|
+
const { locale } = Astro.props;
|
|
7
|
+
const content = await tournamentBracket.i18n[locale]?.();
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
{content && content.bibliography.length > 0 && null}
|