@jjlmoya/utils-sports 1.21.0 → 1.23.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 (59) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +7 -1
  3. package/src/tests/locale_completeness.test.ts +1 -1
  4. package/src/tests/tool_validation.test.ts +2 -2
  5. package/src/tool/padelScoreKeeper/bibliography.astro +15 -0
  6. package/src/tool/padelScoreKeeper/bibliography.ts +10 -0
  7. package/src/tool/padelScoreKeeper/component.astro +142 -0
  8. package/src/tool/padelScoreKeeper/entry.ts +30 -0
  9. package/src/tool/padelScoreKeeper/game-logic.ts +160 -0
  10. package/src/tool/padelScoreKeeper/i18n/de.ts +219 -0
  11. package/src/tool/padelScoreKeeper/i18n/en.ts +219 -0
  12. package/src/tool/padelScoreKeeper/i18n/es.ts +219 -0
  13. package/src/tool/padelScoreKeeper/i18n/fr.ts +219 -0
  14. package/src/tool/padelScoreKeeper/i18n/id.ts +219 -0
  15. package/src/tool/padelScoreKeeper/i18n/it.ts +219 -0
  16. package/src/tool/padelScoreKeeper/i18n/ja.ts +219 -0
  17. package/src/tool/padelScoreKeeper/i18n/ko.ts +219 -0
  18. package/src/tool/padelScoreKeeper/i18n/nl.ts +219 -0
  19. package/src/tool/padelScoreKeeper/i18n/pl.ts +219 -0
  20. package/src/tool/padelScoreKeeper/i18n/pt.ts +219 -0
  21. package/src/tool/padelScoreKeeper/i18n/ru.ts +219 -0
  22. package/src/tool/padelScoreKeeper/i18n/sv.ts +219 -0
  23. package/src/tool/padelScoreKeeper/i18n/tr.ts +219 -0
  24. package/src/tool/padelScoreKeeper/i18n/zh.ts +218 -0
  25. package/src/tool/padelScoreKeeper/index.ts +9 -0
  26. package/src/tool/padelScoreKeeper/logic.ts +227 -0
  27. package/src/tool/padelScoreKeeper/padel-scorekeeper.css +864 -0
  28. package/src/tool/padelScoreKeeper/render.ts +227 -0
  29. package/src/tool/padelScoreKeeper/seo.astro +4 -0
  30. package/src/tool/padelScoreKeeper/ui.ts +26 -0
  31. package/src/tool/streetballScoreKeeper/audio.ts +64 -0
  32. package/src/tool/streetballScoreKeeper/bibliography.astro +15 -0
  33. package/src/tool/streetballScoreKeeper/bibliography.ts +10 -0
  34. package/src/tool/streetballScoreKeeper/component.astro +163 -0
  35. package/src/tool/streetballScoreKeeper/entry.ts +30 -0
  36. package/src/tool/streetballScoreKeeper/game-logic.ts +165 -0
  37. package/src/tool/streetballScoreKeeper/i18n/de.ts +178 -0
  38. package/src/tool/streetballScoreKeeper/i18n/en.ts +178 -0
  39. package/src/tool/streetballScoreKeeper/i18n/es.ts +178 -0
  40. package/src/tool/streetballScoreKeeper/i18n/fr.ts +178 -0
  41. package/src/tool/streetballScoreKeeper/i18n/id.ts +178 -0
  42. package/src/tool/streetballScoreKeeper/i18n/it.ts +178 -0
  43. package/src/tool/streetballScoreKeeper/i18n/ja.ts +178 -0
  44. package/src/tool/streetballScoreKeeper/i18n/ko.ts +178 -0
  45. package/src/tool/streetballScoreKeeper/i18n/nl.ts +178 -0
  46. package/src/tool/streetballScoreKeeper/i18n/pl.ts +178 -0
  47. package/src/tool/streetballScoreKeeper/i18n/pt.ts +178 -0
  48. package/src/tool/streetballScoreKeeper/i18n/ru.ts +178 -0
  49. package/src/tool/streetballScoreKeeper/i18n/sv.ts +178 -0
  50. package/src/tool/streetballScoreKeeper/i18n/tr.ts +178 -0
  51. package/src/tool/streetballScoreKeeper/i18n/zh.ts +178 -0
  52. package/src/tool/streetballScoreKeeper/index.ts +9 -0
  53. package/src/tool/streetballScoreKeeper/logic.ts +269 -0
  54. package/src/tool/streetballScoreKeeper/particles.ts +34 -0
  55. package/src/tool/streetballScoreKeeper/render.ts +226 -0
  56. package/src/tool/streetballScoreKeeper/seo.astro +4 -0
  57. package/src/tool/streetballScoreKeeper/streetball-3x3-basketball-scorekeeper.css +1127 -0
  58. package/src/tool/streetballScoreKeeper/ui.ts +25 -0
  59. package/src/tools.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-sports",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
package/src/entries.ts CHANGED
@@ -16,6 +16,10 @@ export { tennisScoreKeeper } from './tool/tennisScoreKeeper/entry';
16
16
  export type { TennisScoreKeeperLocaleContent } from './tool/tennisScoreKeeper/entry';
17
17
  export { dartsScoreKeeper } from './tool/dartsScoreKeeper/entry';
18
18
  export type { DartsScoreKeeperLocaleContent } from './tool/dartsScoreKeeper/entry';
19
+ export { padelScoreKeeper } from './tool/padelScoreKeeper/entry';
20
+ export type { PadelScoreKeeperLocaleContent } from './tool/padelScoreKeeper/entry';
21
+ export { streetballScoreKeeper } from './tool/streetballScoreKeeper/entry';
22
+ export type { StreetballLocaleContent } from './tool/streetballScoreKeeper/entry';
19
23
  export { sportsCategory } from './category';
20
24
  import { basketScoreKeeper } from './tool/basketScoreKeeper/entry';
21
25
  import { footballScoreKeeper } from './tool/footballScoreKeeper/entry';
@@ -26,4 +30,6 @@ import { scoreKeeper } from './tool/scoreKeeper/entry';
26
30
  import { tournamentBracket } from './tool/tournamentBracket/entry';
27
31
  import { tennisScoreKeeper } from './tool/tennisScoreKeeper/entry';
28
32
  import { dartsScoreKeeper } from './tool/dartsScoreKeeper/entry';
29
- export const ALL_ENTRIES = [basketScoreKeeper, footballScoreKeeper, gymTracker, pingPongScoreKeeper, reactionTester, scoreKeeper, tournamentBracket, tennisScoreKeeper, dartsScoreKeeper];
33
+ import { padelScoreKeeper } from './tool/padelScoreKeeper/entry';
34
+ import { streetballScoreKeeper } from './tool/streetballScoreKeeper/entry';
35
+ export const ALL_ENTRIES = [basketScoreKeeper, footballScoreKeeper, gymTracker, pingPongScoreKeeper, reactionTester, scoreKeeper, tournamentBracket, tennisScoreKeeper, dartsScoreKeeper, padelScoreKeeper, streetballScoreKeeper];
@@ -3,6 +3,6 @@ import { ALL_TOOLS } from '../tools';
3
3
 
4
4
  describe('Locale Completeness Validation', () => {
5
5
  it('all tools registered', () => {
6
- expect(ALL_TOOLS.length).toBe(9);
6
+ expect(ALL_TOOLS.length).toBe(11);
7
7
  });
8
8
  });
@@ -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 9 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(9);
7
+ it('should have 11 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(11);
9
9
  });
10
10
 
11
11
  it('sportsCategory should be defined', () => {
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { bibliography } from './bibliography';
3
+ ---
4
+
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>
@@ -0,0 +1,10 @@
1
+ export const bibliography = [
2
+ {
3
+ name: 'FIP Padel Rules',
4
+ url: 'https://www.padelfip.com/wp-content/uploads/2025/12/FIP_Rules-of-Padel.pdf',
5
+ },
6
+ {
7
+ name: 'World Padel Tour Regulations',
8
+ url: 'https://www.padelfip.com/es/documents/',
9
+ },
10
+ ];
@@ -0,0 +1,142 @@
1
+ ---
2
+ import type { KnownLocale } from '../../types';
3
+ import type { PadelScoreKeeperUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: KnownLocale;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const { ui } = Astro.props;
11
+ const t = (ui ?? {}) as PadelScoreKeeperUI;
12
+ ---
13
+
14
+ <div class="tn-console" id="tn-card" data-tn-ui={JSON.stringify(t)}>
15
+ <div class="tn-header-panel">
16
+ <div class="tn-header-actions">
17
+ <button class="tn-action-btn" data-tn-reset title={t.reset}>
18
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
19
+ <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
20
+ <path d="M16 3h5v5M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
21
+ <path d="M8 21H3v-5"/>
22
+ </svg>
23
+ </button>
24
+ <button class="tn-action-btn" data-tn-fs title={t.fullscreen}>
25
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
26
+ <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
27
+ </svg>
28
+ </button>
29
+ </div>
30
+ </div>
31
+
32
+ <div class="tn-scoreboard">
33
+ <div class="tn-player-card tn-player-card-active" id="tn-card-a">
34
+ <div class="tn-name-section">
35
+ <input type="text" value={t.playerA} class="tn-player-input" id="tn-name-a" />
36
+ <span class="tn-server-ball" id="tn-ball-a"></span>
37
+ </div>
38
+ <div class="tn-big-score" id="tn-score-a">0</div>
39
+ <div class="tn-sets-display" id="tn-sets-a"></div>
40
+ </div>
41
+
42
+ <div class="tn-player-card" id="tn-card-b">
43
+ <div class="tn-name-section">
44
+ <input type="text" value={t.playerB} class="tn-player-input" id="tn-name-b" />
45
+ <span class="tn-server-ball" id="tn-ball-b"></span>
46
+ </div>
47
+ <div class="tn-big-score" id="tn-score-b">0</div>
48
+ <div class="tn-sets-display" id="tn-sets-b"></div>
49
+ </div>
50
+ </div>
51
+
52
+ <div class="tn-court-panel">
53
+ <div class="tn-court-side-label tn-side-label-top" id="tn-court-label-top">{t.playerB}</div>
54
+ <div class="tn-court-wrapper">
55
+ <div class="tn-padel-court" id="tn-court">
56
+ <div class="tn-court-half tn-court-top" id="tn-court-half-b" data-court-half="top">
57
+ <div class="tn-court-quadrant tn-quad-left" data-quad-side="top" data-quad="left">
58
+ <div class="tn-quad-role tn-role-server">S</div>
59
+ <div class="tn-quad-role tn-role-receiver">R</div>
60
+ </div>
61
+ <div class="tn-court-quadrant tn-quad-right" data-quad-side="top" data-quad="right">
62
+ <div class="tn-quad-role tn-role-server">S</div>
63
+ <div class="tn-quad-role tn-role-receiver">R</div>
64
+ </div>
65
+ </div>
66
+ <div class="tn-net-line"></div>
67
+ <div class="tn-court-half tn-court-bottom" id="tn-court-half-a" data-court-half="bottom">
68
+ <div class="tn-court-quadrant tn-quad-left" data-quad-side="bottom" data-quad="left">
69
+ <div class="tn-quad-role tn-role-server">S</div>
70
+ <div class="tn-quad-role tn-role-receiver">R</div>
71
+ </div>
72
+ <div class="tn-court-quadrant tn-quad-right" data-quad-side="bottom" data-quad="right">
73
+ <div class="tn-quad-role tn-role-server">S</div>
74
+ <div class="tn-quad-role tn-role-receiver">R</div>
75
+ </div>
76
+ </div>
77
+ <svg class="tn-serve-arrow-svg" viewBox="0 0 200 300">
78
+ <path id="tn-serve-path" d="" fill="none" stroke="#cf0" stroke-width="3" stroke-dasharray="6,4" />
79
+ </svg>
80
+ </div>
81
+ </div>
82
+ <div class="tn-court-side-label tn-side-label-bottom" id="tn-court-label-bottom">{t.playerA}</div>
83
+ </div>
84
+
85
+ <div class="tn-action-controls">
86
+ <button class="tn-score-action-btn tn-score-btn-a" id="tn-btn-score-a">
87
+ <span class="tn-score-btn-lbl">+</span>
88
+ <span class="tn-score-btn-name" id="tn-btn-name-a">{t.playerA}</span>
89
+ </button>
90
+ <button class="tn-score-action-btn tn-score-btn-b" id="tn-btn-score-b">
91
+ <span class="tn-score-btn-lbl">+</span>
92
+ <span class="tn-score-btn-name" id="tn-btn-name-b">{t.playerB}</span>
93
+ </button>
94
+ </div>
95
+
96
+ <div class="tn-bottom-toolbar">
97
+ <button class="tn-toolbar-btn" data-dt-undo>Undo</button>
98
+ <div class="tn-control-group">
99
+ <button class="tn-control-btn tn-format-btn tn-format-active" data-dt-format="standard">{t.formatStandard}</button>
100
+ <button class="tn-control-btn tn-format-btn" data-dt-format="golden">{t.formatGoldenSet}</button>
101
+ </div>
102
+ </div>
103
+
104
+ <div id="tn-winner" class="tn-celebration-screen">
105
+ <div class="tn-celebration-box">
106
+ <svg class="tn-celebration-cup" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
107
+ <path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6m12 5h1.5a2.5 2.5 0 0 0 0-5H18M6 4h12M12 18v3m-4 0h8m-4-17v14a4 4 0 0 1-4-4"/>
108
+ </svg>
109
+ <div class="tn-celebration-label">{t.matchWon}</div>
110
+ <div id="tn-winner-team" class="tn-celebration-name">Team 1</div>
111
+ <button class="tn-celebration-btn" data-close-winner>&times;</button>
112
+ </div>
113
+ </div>
114
+
115
+ <div id="tn-confetti" class="tn-confetti-field"></div>
116
+
117
+ <div id="tn-modal" class="tn-inline-modal">
118
+ <div class="tn-inline-modal-box">
119
+ <p class="tn-inline-modal-text">{t.resetConfirm}</p>
120
+ <div class="tn-inline-modal-btns">
121
+ <button id="tn-modal-cancel" class="tn-inline-modal-btn tn-inline-btn-cancel">{t.cancel}</button>
122
+ <button id="tn-modal-confirm" class="tn-inline-modal-btn tn-inline-btn-reset">{t.reset}</button>
123
+ </div>
124
+ </div>
125
+ </div>
126
+
127
+ <div id="tn-goldpoint-modal" class="tn-goldpoint-modal">
128
+ <div class="tn-goldpoint-box">
129
+ <div class="tn-goldpoint-title">{t.goldPointTitle}</div>
130
+ <div class="tn-goldpoint-desc">{t.selectReceiver}</div>
131
+ <div class="tn-goldpoint-btns">
132
+ <button class="tn-gp-btn" data-gp-side="left">{t.leftReceiver}</button>
133
+ <button class="tn-gp-btn" data-gp-side="right">{t.rightReceiver}</button>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <script>
140
+ import { initPadelScoreKeeper } from './logic';
141
+ initPadelScoreKeeper();
142
+ </script>
@@ -0,0 +1,30 @@
1
+ import type { SportsToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { PadelScoreKeeperUI } from './ui';
3
+
4
+ export type { PadelScoreKeeperUI };
5
+ export type PadelScoreKeeperLocaleContent = ToolLocaleContent<PadelScoreKeeperUI>;
6
+
7
+ export const padelScoreKeeper: SportsToolEntry<PadelScoreKeeperUI> = {
8
+ id: 'padel-scorekeeper',
9
+ icons: {
10
+ bg: 'mdi:tennis-ball',
11
+ fg: 'mdi:scoreboard-outline',
12
+ },
13
+ i18n: {
14
+ en: () => import('./i18n/en').then((m) => m.content),
15
+ es: () => import('./i18n/es').then((m) => m.content),
16
+ fr: () => import('./i18n/fr').then((m) => m.content),
17
+ de: () => import('./i18n/de').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,160 @@
1
+ export type TeamKey = 'a' | 'b';
2
+ export type MatchFormat = 'standard' | 'golden';
3
+
4
+ export interface TeamState {
5
+ score: string;
6
+ games: number;
7
+ sets: number[];
8
+ tiebreakPoints: number;
9
+ }
10
+
11
+ export interface PadelMatchState {
12
+ format: MatchFormat;
13
+ teamA: TeamState;
14
+ teamB: TeamState;
15
+ isTiebreak: boolean;
16
+ isSuperTiebreak: boolean;
17
+ isGoldPoint: boolean;
18
+ receiverSelectionSide: 'left' | 'right' | null;
19
+ serverTeam: TeamKey;
20
+ serverPosition: 'deuce' | 'ad';
21
+ endsSwapped: boolean;
22
+ winner: TeamKey | null;
23
+ }
24
+
25
+ export function createInitialTeam(): TeamState {
26
+ return {
27
+ score: '0',
28
+ games: 0,
29
+ sets: [],
30
+ tiebreakPoints: 0,
31
+ };
32
+ }
33
+
34
+ export function createInitialMatch(format: MatchFormat = 'standard'): PadelMatchState {
35
+ return {
36
+ format,
37
+ teamA: createInitialTeam(),
38
+ teamB: createInitialTeam(),
39
+ isTiebreak: false,
40
+ isSuperTiebreak: false,
41
+ isGoldPoint: false,
42
+ receiverSelectionSide: null,
43
+ serverTeam: 'a',
44
+ serverPosition: 'deuce',
45
+ endsSwapped: false,
46
+ winner: null,
47
+ };
48
+ }
49
+
50
+ function alternateServer(state: PadelMatchState): void {
51
+ state.serverTeam = state.serverTeam === 'a' ? 'b' : 'a';
52
+ state.serverPosition = 'deuce';
53
+ }
54
+
55
+ function checkEndsSwapAlert(state: PadelMatchState): boolean {
56
+ if (state.isTiebreak) {
57
+ const totalTbPoints = state.teamA.tiebreakPoints + state.teamB.tiebreakPoints;
58
+ return totalTbPoints > 0 && totalTbPoints % 6 === 0;
59
+ }
60
+ const totalGames = state.teamA.games + state.teamB.games;
61
+ return totalGames % 2 === 1;
62
+ }
63
+
64
+ function updateTiebreakServer(state: PadelMatchState): void {
65
+ const totalTb = state.teamA.tiebreakPoints + state.teamB.tiebreakPoints;
66
+ if (totalTb % 2 === 1) {
67
+ state.serverTeam = state.serverTeam === 'a' ? 'b' : 'a';
68
+ state.serverPosition = 'ad';
69
+ } else {
70
+ state.serverPosition = state.serverPosition === 'deuce' ? 'ad' : 'deuce';
71
+ }
72
+ }
73
+
74
+ function concludeSet(state: PadelMatchState): void {
75
+ state.teamA.sets.push(state.isTiebreak ? 7 : state.teamA.games);
76
+ state.teamB.sets.push(state.isTiebreak ? 6 : state.teamB.games);
77
+ state.teamA.games = 0;
78
+ state.teamB.games = 0;
79
+ state.teamA.tiebreakPoints = 0;
80
+ state.teamB.tiebreakPoints = 0;
81
+ state.isTiebreak = false;
82
+ state.isSuperTiebreak = false;
83
+ const setsWonA = state.teamA.sets.reduce((acc, v, idx) => acc + (v > state.teamB.sets[idx] ? 1 : 0), 0);
84
+ const setsWonB = state.teamB.sets.reduce((acc, v, idx) => acc + (v > state.teamA.sets[idx] ? 1 : 0), 0);
85
+ if (setsWonA >= 2) {
86
+ state.winner = 'a';
87
+ } else if (setsWonB >= 2) {
88
+ state.winner = 'b';
89
+ } else if (setsWonA === 1 && setsWonB === 1 && state.teamA.sets.length === 2) {
90
+ state.isSuperTiebreak = true;
91
+ state.isTiebreak = true;
92
+ }
93
+ }
94
+
95
+ function handleTiebreakPoint(state: PadelMatchState, winner: TeamKey): void {
96
+ const team = winner === 'a' ? state.teamA : state.teamB;
97
+ const other = winner === 'a' ? state.teamB : state.teamA;
98
+ team.tiebreakPoints += 1;
99
+ updateTiebreakServer(state);
100
+ const target = state.isSuperTiebreak ? 10 : 7;
101
+ if (team.tiebreakPoints >= target && team.tiebreakPoints - other.tiebreakPoints >= 2) {
102
+ concludeSet(state);
103
+ }
104
+ }
105
+
106
+ function handleGameWin(state: PadelMatchState, winner: TeamKey): void {
107
+ const team = winner === 'a' ? state.teamA : state.teamB;
108
+ const other = winner === 'a' ? state.teamB : state.teamA;
109
+ team.games += 1;
110
+ state.teamA.score = '0';
111
+ state.teamB.score = '0';
112
+ state.isGoldPoint = false;
113
+ state.receiverSelectionSide = null;
114
+ alternateServer(state);
115
+ const targetGames = state.format === 'golden' ? 4 : 6;
116
+ if (team.games >= targetGames && team.games - other.games >= 2) {
117
+ concludeSet(state);
118
+ } else if (team.games === targetGames && other.games === targetGames) {
119
+ state.isTiebreak = true;
120
+ }
121
+ }
122
+
123
+ export function processReceiverSelection(state: PadelMatchState, side: 'left' | 'right'): PadelMatchState {
124
+ const next = { ...state, teamA: { ...state.teamA }, teamB: { ...state.teamB } };
125
+ next.receiverSelectionSide = side;
126
+ return next;
127
+ }
128
+
129
+ function getNextScore(current: string): string {
130
+ if (current === '0') return '15';
131
+ if (current === '15') return '30';
132
+ return '40';
133
+ }
134
+
135
+ function handleStandardPoint(next: PadelMatchState, winner: TeamKey): void {
136
+ const team = winner === 'a' ? next.teamA : next.teamB;
137
+ if (team.score === '40') {
138
+ handleGameWin(next, winner);
139
+ } else {
140
+ team.score = getNextScore(team.score);
141
+ }
142
+ if (next.teamA.score === '40' && next.teamB.score === '40') {
143
+ next.isGoldPoint = true;
144
+ }
145
+ if (!next.isGoldPoint && next.teamA.score !== '0') {
146
+ next.serverPosition = next.serverPosition === 'deuce' ? 'ad' : 'deuce';
147
+ }
148
+ }
149
+
150
+ export function processPoint(state: PadelMatchState, winner: TeamKey): PadelMatchState {
151
+ if (state.winner) return state;
152
+ const next = JSON.parse(JSON.stringify(state)) as PadelMatchState;
153
+ if (next.isTiebreak) {
154
+ handleTiebreakPoint(next, winner);
155
+ } else {
156
+ handleStandardPoint(next, winner);
157
+ }
158
+ next.endsSwapped = checkEndsSwapAlert(next);
159
+ return next;
160
+ }
@@ -0,0 +1,219 @@
1
+ import { bibliography } from '../bibliography';
2
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
3
+ import type { PadelScoreKeeperLocaleContent } from '../entry';
4
+
5
+ const slug = 'padel-spielstand';
6
+ const title = 'Premium Padel Spielstand : Gold Point & Aufschlagrotation';
7
+ const description = 'Verfolge Padel-Punkte mit der offiziellen Punto de Oro (Gold Point) Regel, Aufschlagrotations-Alarmen, Tiebreaks und dynamischer Seitenwechsel-Animation.';
8
+
9
+ const faq = [
10
+ {
11
+ question: 'Was ist der Gold Point (Punto de Oro) beim Padel?',
12
+ answer: 'Der Gold Point ist ein Entscheidungspunkt, der gespielt wird, wenn der Spielstand 40-40 (Einstand) erreicht. Es gibt kein Vorteilsspiel. Das aufschlagende Team wählt, ob es den Aufschlag auf der linken oder rechten Seite annimmt, und wer diesen einen Punkt gewinnt, gewinnt das gesamte Spiel.',
13
+ },
14
+ {
15
+ question: 'Wie funktionieren Satzformate beim Padel?',
16
+ answer: 'Standardspiele werden im Best-of-3-Sätzen ausgetragen, wobei jeder Satz vom ersten Team gewonnen wird, das 6 Spiele erreicht (mit 2 Spielen Vorsprung). Bei 6-6 wird ein 7-Punkte-Tiebreak gespielt. Ein optionales Golden-Set-Format endet bei 4 Spielen mit einem Tiebreak bei 4-4.',
17
+ },
18
+ {
19
+ question: 'Wann wechseln Spieler die Seiten beim Padel?',
20
+ answer: 'Die Spieler wechseln die Seiten nach dem ersten Spiel und dann alle 2 Spiele (wenn die Summe der Spiele im aktuellen Satz ungerade ist, z. B. 1, 3, 5). Während Tiebreaks wechseln die Spieler alle 6 Punkte die Seiten.',
21
+ },
22
+ ];
23
+
24
+ const howTo = [
25
+ {
26
+ name: 'Satzformat konfigurieren',
27
+ text: 'Wähle das Standardformat (erster auf 6 Spiele) oder das kürzere Golden-Set-Format (erster auf 4 Spiele).',
28
+ },
29
+ {
30
+ name: 'Spielernamen eingeben',
31
+ text: 'Gib Teamnamen ein, um die Anzeigetafel zu personalisieren. Deine Einstellungen werden automatisch gespeichert.',
32
+ },
33
+ {
34
+ name: 'Punkte auf dem Platz erfassen',
35
+ text: 'Tippe auf eine Seite des visuellen isometrischen Padelplatzes, um Punkte zu erzielen. Aufschlaganzeiger führen dich durch die diagonalen Rotationen.',
36
+ },
37
+ {
38
+ name: 'Gold Points entscheiden',
39
+ text: 'Wenn der Einstand erreicht ist, wähle die Rückschlagseite (linker oder rechter Annahmespieler) und klicke auf das gewinnende Team, um das Spiel zu beenden.',
40
+ },
41
+ ];
42
+
43
+ const faqSchema: WithContext<FAQPage> = {
44
+ '@context': 'https://schema.org',
45
+ '@type': 'FAQPage',
46
+ mainEntity: faq.map((item) => ({
47
+ '@type': 'Question',
48
+ name: item.question,
49
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
50
+ })),
51
+ };
52
+
53
+ const howToSchema: WithContext<HowTo> = {
54
+ '@context': 'https://schema.org',
55
+ '@type': 'HowTo',
56
+ name: title,
57
+ description,
58
+ step: howTo.map((step, i) => ({
59
+ '@type': 'HowToStep',
60
+ position: i + 1,
61
+ name: step.name,
62
+ text: step.text,
63
+ })),
64
+ };
65
+
66
+ const appSchema: WithContext<SoftwareApplication> = {
67
+ '@context': 'https://schema.org',
68
+ '@type': 'SoftwareApplication',
69
+ name: title,
70
+ description,
71
+ applicationCategory: 'SportsApplication',
72
+ operatingSystem: 'All',
73
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
74
+ inLanguage: 'de',
75
+ };
76
+
77
+ export const content: PadelScoreKeeperLocaleContent = {
78
+ slug,
79
+ title,
80
+ description,
81
+ faq,
82
+ bibliography,
83
+ howTo,
84
+ schemas: [faqSchema, howToSchema, appSchema],
85
+ seo: [
86
+ {
87
+ type: 'title',
88
+ text: 'Kostenlose Online Padel Anzeigetafel & Spielverfolgung',
89
+ level: 2,
90
+ },
91
+ {
92
+ type: 'paragraph',
93
+ html: 'Das Verfolgen der Punkte beim Padel kann mit schnellen Ballwechseln, Tiebreaks, Seitenwechseln und der offiziellen Punto de Oro (Gold Point) Regel verwirrend sein. Diese kostenlose Online-Padel-Anzeigetafel nimmt dir die Mühe der Punkteverfolgung ab. Tippe einfach auf das visuelle Spielfeld, um Punkte zu erfassen, und lass das Tool automatisch Aufschlagrotationen, Annahmeseiten, Satzverläufe und Seitenwechsel in Echtzeit verwalten.',
94
+ },
95
+ {
96
+ type: 'title',
97
+ text: 'Padel-Punktesystem, Gold Points und Rotationen verstehen',
98
+ level: 2,
99
+ },
100
+ {
101
+ type: 'paragraph',
102
+ html: 'Padel verwendet ein dem Tennis ähnliches Punktesystem (15, 30, 40, Spiel), führt aber spezifische Regeln für schnelleres Spiel ein. Nach den professionellen FIP-Regeln wird bei einem Spielstand von 40-40 ein entscheidender Gold Point (Punto de Oro) gespielt. Das annehmende Team wählt aus, welche Seite (links oder rechts) den Aufschlag erhält, und der Gewinner dieses einzigen Punktes gewinnt das Spiel. Außerdem müssen die Teams die Seiten wechseln, wenn die Gesamtzahl der Spiele in einem Satz ungerade ist, und alle 6 Punkte während eines Tiebreaks.',
103
+ },
104
+ {
105
+ type: 'comparative',
106
+ columns: 3,
107
+ items: [
108
+ {
109
+ title: 'Freundschaftsspiele',
110
+ description: 'Schnelle und saubere Punkteverfolgung für Freundschaftsspiele mit deinen Padel-Partnern.',
111
+ icon: 'mdi:tennis',
112
+ points: ['Punkte mit einem Klick', 'Optimiert für Mobilgeräte', 'Funktioniert offline'],
113
+ },
114
+ {
115
+ title: 'Verein & Liga',
116
+ description: 'Verfolge mühelos wettbewerbsorientierte Vereinsspiele und lokale Turniere.',
117
+ icon: 'mdi:trophy-outline',
118
+ points: ['Satzverlaufsarchiv', '6-Spiele oder 4-Spiele Sätze', 'Gold Point Unterstützung'],
119
+ },
120
+ {
121
+ title: 'Schiedsrichter Modus',
122
+ description: 'Voll ausgestattetes Tool zum Leiten offizieller Spiele oder Trainingseinheiten.',
123
+ icon: 'mdi:school',
124
+ points: ['Aktive Aufschlag- & Annahmemarker', 'Interaktive Platzrotation', 'Vollbild-Konsolenmodus'],
125
+ },
126
+ ],
127
+ },
128
+ {
129
+ type: 'title',
130
+ text: 'Erweiterte Digitale Funktionen für Padel-Spieler',
131
+ level: 2,
132
+ },
133
+ {
134
+ type: 'list',
135
+ items: [
136
+ '<strong>Offizielle Gold Point Logik</strong> erlaubt dem annehmenden Team, die Annahmeseite bei Einstand zu wählen und zeigt den Aufschlagweg an.',
137
+ '<strong>Visueller Platzanzeiger</strong> zeigt die Positionen von Aufschläger (S) und Annahmespieler (R) dynamisch an, um Rotationsfehler zu vermeiden.',
138
+ '<strong>Automatischer Seitenwechsel</strong> dreht das Platzlayout bei ungeraden Spielen oder Tiebreak-Intervallen, damit es immer mit deiner physischen Ansicht übereinstimmt.',
139
+ '<strong>Anpassbare Satzformate</strong> unterstützt Standardsätze mit 6 Spielen oder schnelle Golden Sets mit 4 Spielen.',
140
+ '<strong>Lokale Browser-Autospeicherung</strong> bewahrt Spielernamen und aktuelle Spielstände, selbst wenn du die Seite aktualisierst.',
141
+ ],
142
+ },
143
+ {
144
+ type: 'title',
145
+ text: 'Padel Tiebreak-Regeln: Standard vs. Super Tiebreak',
146
+ level: 2,
147
+ },
148
+ {
149
+ type: 'paragraph',
150
+ html: 'In standardmäßigen Padel-Sätzen wird bei einem Spielstand von 6-6 ein 7-Punkte-Tiebreak gespielt. Bei einem Tiebreak werden die Punkte numerisch gezählt (1, 2, 3, usw.). Das erste Team, das 7 Punkte mit einem Vorsprung von 2 erreicht, gewinnt den Satz. Der Spieler, der an der Reihe ist aufzuschlagen, serviert den ersten Punkt von der rechten (Einstand-)Seite. Danach schlägt jeder Spieler zwei aufeinanderfolgende Punkte auf, beginnend von der linken (Vorteils-)Seite. In einigen Turnierformaten wird bei einem Satzstand von 1-1 ein 10-Punkte-Super-Tiebreak anstelle eines vollständigen dritten Satzes gespielt, um das Spiel zu entscheiden.',
151
+ },
152
+ {
153
+ type: 'title',
154
+ text: 'Platzwechsel und Rotationen: Fairness beim Padel',
155
+ level: 2,
156
+ },
157
+ {
158
+ type: 'paragraph',
159
+ html: 'Der Seitenwechsel ist beim Padel unerlässlich, um sicherzustellen, dass Umgebungsfaktoren wie Sonne, Wind oder platsspezifische Unebenheiten kein Team bevorzugen. Die Spieler müssen die Seiten nach dem ersten Spiel jedes Satzes und dann nach jeweils zwei Spielen wechseln (z. B. bei 1-0, 2-1, 3-2, 4-3, 5-4). Unsere digitale Padel-Anzeigetafel verfügt über eine dynamische Seitenwechsel-Animation, die das visuelle Platzlayout automatisch um 180 Grad dreht, wenn die Spieler physisch die Seiten wechseln müssen. Dadurch wird sichergestellt, dass das oben auf deinem Bildschirm angezeigte Team immer mit dem Team übereinstimmt, das am anderen Ende des physischen Platzes spielt.',
160
+ },
161
+ {
162
+ type: 'title',
163
+ text: 'Standard-Sätze vs. Golden Sets Format',
164
+ level: 2,
165
+ },
166
+ {
167
+ type: 'paragraph',
168
+ html: 'Während Standardspiele mit 6 Spielen pro Satz gespielt werden, verwenden viele Freizeitligen und schnelle Turniere das "Golden Set"-Format, bei dem Sätze nur bis 4 Spiele gespielt werden (mit Tiebreak bei 4-4). Diese Anzeigetafel ermöglicht es dir, mit einem einzigen Tipp in der Symbolleiste zwischen diesen Formaten zu wechseln. Unabhängig vom ausgewählten Format verwaltet die Anzeigetafel alle Tiebreaks, Aufschlagrotationen und Punkteberechnungen automatisch.',
169
+ },
170
+ {
171
+ type: 'title',
172
+ text: 'Tipps für effektive Punkteverfolgung auf dem Platz',
173
+ level: 2,
174
+ },
175
+ {
176
+ type: 'list',
177
+ items: [
178
+ '<strong>Verwende einen Platzständer oder Handyhalter:</strong> Befestige dein Handy oder Tablet am Padelplatzzaun in Netzhöhe. So können Spieler auf beiden Seiten die aktuelle Punktzahl und die Aufschlaganzeiger leicht sehen.',
179
+ '<strong>Personalisiere Namen vor Spielbeginn:</strong> Nimm dir 10 Sekunden Zeit, um die tatsächlichen Namen der Spieler oder Teams einzugeben. Das macht die Sprachansagen (falls aktiviert) und die visuelle Anzeigetafel viel ansprechender und offizieller.',
180
+ '<strong>Aktiviere den Vollbildmodus:</strong> Klicke auf die Vollbild-Schaltfläche im Kopfbereich. Dies maximiert die Anzeigetafel-Oberfläche und verhindert, dass sich der Bildschirm bei langen Ballwechseln automatisch ausschaltet.',
181
+ ],
182
+ },
183
+ {
184
+ type: 'title',
185
+ text: 'Warum einen digitalen Padel-Spielstand verwenden?',
186
+ level: 2,
187
+ },
188
+ {
189
+ type: 'paragraph',
190
+ html: 'Anstatt ständig darüber zu diskutieren, wer aufschlägt, wer an der Reihe ist anzunehmen oder wie der Spielstand ist, sorgt ein digitaler Spielstand dafür, dass alle auf dem gleichen Stand sind. Durch die visuelle Darstellung der Aufschläger- und Annahmepositionen direkt auf dem Bildschirm können die Spieler schnell auf das Handy auf der Bank schauen und genau wissen, wo sie stehen müssen. Das verbessert das Spieltempo und verhindert Rotationsfehler.',
191
+ },
192
+ ],
193
+ ui: {
194
+ playerA: 'Team 1',
195
+ playerB: 'Team 2',
196
+ game: 'Spiel',
197
+ set: 'Satz',
198
+ tiebreak: 'Tiebreak',
199
+ goldPoint: 'Gold Point',
200
+ selectReceiver: 'Annahmespieler wählen',
201
+ leftReceiver: 'Linker Annahmespieler',
202
+ rightReceiver: 'Rechter Annahmespieler',
203
+ server: 'Aufschläger',
204
+ receiver: 'Annahmespieler',
205
+ changeEnds: 'Seiten wechseln',
206
+ matchWon: 'Gewonnen',
207
+ reset: 'Zurücksetzen',
208
+ resetConfirm: 'Spiel zurücksetzen? Alle Daten gehen verloren.',
209
+ cancel: 'Abbrechen',
210
+ fullscreen: 'Vollbild',
211
+ exitFullscreen: 'Vollbild beenden',
212
+ deuce: 'Einstand',
213
+ advantage: 'Vorteil',
214
+ formatStandard: '6 Spiele',
215
+ formatGoldenSet: '4 Spiele',
216
+ goldenSet: 'Golden Set',
217
+ goldPointTitle: 'Gold Point Entscheidungspunkt',
218
+ },
219
+ };