11<script setup lang="ts">
22import { formatPlacement } from " @/helpers/common" ;
3- import type { Match , MatchTeam , Tournament , Ref , MatchStatus } from " @/types/tournament" ;
3+ import type { Match , MatchTeam , Tournament , Ref , MatchStatus , SetScore } from " @/types/tournament" ;
44import { computed , ref } from " vue" ;
55import SegmentPicker from " ../SegmentPicker.vue" ;
6+ import { calculateScoresFromSets } from " @/helpers/scoring" ;
67
78const props = defineProps <{
89 modelValue: Match ;
@@ -24,9 +25,59 @@ const onStatusChanged = () => {
2425const scores = ref (match .value .teams .map ((team ) => team .score ) ?? []);
2526const onScoreChanged = (teamIndex : number ) => {
2627 match .value .teams [teamIndex ]! .score = scores .value [teamIndex ]! ;
28+ if (useSets .value && sets .value .length > 0 ) {
29+ scoreInSyncWithSets .value = false ;
30+ }
2731 onChanged ();
2832};
2933
34+ const useSets = computed (() => props .tournament .config .useSets ?? false );
35+ const sets = ref <SetScore []>(match .value .sets ?? []);
36+
37+ const addSet = () => {
38+ sets .value .push ({ team1: 0 , team2: 0 });
39+ onSetsChanged ();
40+ };
41+
42+ const removeSet = (index : number ) => {
43+ sets .value .splice (index , 1 );
44+ onSetsChanged ();
45+ };
46+
47+ const scoreFromSets = computed (() => {
48+ return calculateScoresFromSets (sets .value );
49+ });
50+
51+ const scoreInSyncWithSets = ref (
52+ sets .value .length > 0
53+ ? scores .value .every ((score , index ) => score === scoreFromSets .value [index ])
54+ : true ,
55+ );
56+
57+ const onSetsChanged = () => {
58+ match .value .sets = sets .value ;
59+ if (sets .value .length > 0 && scoreInSyncWithSets .value ) {
60+ // Auto-calculate scores from sets
61+ const [team1Score, team2Score] = scoreFromSets .value ;
62+ scores .value = [team1Score , team2Score ];
63+
64+ match .value .teams [0 ]! .score = team1Score ;
65+ match .value .teams [1 ]! .score = team2Score ;
66+ }
67+ onChanged ();
68+ };
69+
70+ const syncScoresWithSets = () => {
71+ scoreInSyncWithSets .value = true ;
72+ onSetsChanged ();
73+ };
74+
75+ const scoresOutOfSync = computed (() => {
76+ if (! useSets .value || sets .value .length === 0 ) return false ;
77+ const [team1Score, team2Score] = scoreFromSets .value ;
78+ return scores .value [0 ] !== team1Score || scores .value [1 ] !== team2Score ;
79+ });
80+
3081const matcheditor = ref <HTMLDialogElement | null >(null );
3182const openMatchEditor = () => {
3283 if (matcheditor .value ) {
@@ -95,6 +146,64 @@ defineExpose({
95146 </div >
96147 </div >
97148
149+ <div
150+ v-if =" useSets"
151+ class =" sets-section card"
152+ >
153+ <div class =" set-and-sync" >
154+ <h3 >Sets</h3 >
155+ <button
156+ v-if =" scoresOutOfSync"
157+ class =" ghost text-sm"
158+ @click =" syncScoresWithSets"
159+ >
160+ Sync Scores
161+ </button >
162+ </div >
163+ <div class =" sets-list" >
164+ <div
165+ v-for =" (set, index) in sets"
166+ :key =" index"
167+ class =" set-row"
168+ >
169+ <span class =" set-label" >Set {{ index + 1 }}</span >
170+ <div class =" set-scores" >
171+ <input
172+ type =" number"
173+ min =" 0"
174+ v-model.number =" set.team1"
175+ @input =" onSetsChanged"
176+ class =" set-score-input"
177+ />
178+ <span class =" separator" >:</span >
179+ <input
180+ type =" number"
181+ min =" 0"
182+ v-model.number =" set.team2"
183+ @input =" onSetsChanged"
184+ class =" set-score-input"
185+ />
186+ </div >
187+ <button
188+ class =" ghost"
189+ @click =" removeSet(index)"
190+ title =" Remove set"
191+ >
192+ <ion-icon name =" trash-outline" ></ion-icon >
193+ </button >
194+ </div >
195+ </div >
196+ <div class =" add-set" >
197+ <button
198+ class =" ghost"
199+ @click =" addSet"
200+ >
201+ <ion-icon name =" add-outline" ></ion-icon >
202+ Add Set
203+ </button >
204+ </div >
205+ </div >
206+
98207 <div class =" match-status" >
99208 <SegmentPicker
100209 v-model =" match.status"
@@ -160,6 +269,66 @@ defineExpose({
160269 }
161270}
162271
272+ .sets-section {
273+ margin-top : 2em ;
274+ padding-top : 1em ;
275+
276+ h 3 {
277+ margin-bottom : 1em ;
278+ font-size : var (--typography-heading-fontSize-m );
279+ }
280+
281+ .set-and-sync {
282+ display : flex ;
283+ justify-content : space-between ;
284+ align-items : baseline ;
285+ }
286+
287+ .add-set {
288+ display : flex ;
289+ justify-content : center ;
290+ }
291+ }
292+
293+ .sets-list {
294+ display : flex ;
295+ flex-direction : column ;
296+ gap : 0.75em ;
297+ margin-bottom : 1em ;
298+ }
299+
300+ .set-row {
301+ display : grid ;
302+ grid-template-columns : 4rem 1fr auto ;
303+ align-items : center ;
304+ gap : 1em ;
305+
306+ .set-label {
307+ font-size : 0.9em ;
308+ color : var (--color-text-secondary );
309+ }
310+
311+ .set-scores {
312+ display : flex ;
313+ align-items : center ;
314+ gap : 0.5em ;
315+ justify-content : center ;
316+
317+ .set-score-input {
318+ width : 5ch ;
319+ text-align : center ;
320+ padding : 0.25em ;
321+ font-size : 1.1em ;
322+ margin-bottom : 0 ;
323+ }
324+
325+ .separator {
326+ font-size : 1.1em ;
327+ color : var (--color-text-secondary );
328+ }
329+ }
330+ }
331+
163332.match-status {
164333 width : max-content ;
165334 margin-left : auto ;
0 commit comments