@@ -29,12 +29,12 @@ interface Props {
2929}
3030
3131export default function WatchModeScorer ( { tasks, players, seasonNumber, onComplete, onCancel } : Props ) {
32- const scorableTasks = tasks . filter ( ( t ) => t . judgement !== "objective" ) ;
3332 const [ currentTaskIdx , setCurrentTaskIdx ] = useState ( 0 ) ;
3433 const [ currentPlayerIdx , setCurrentPlayerIdx ] = useState ( 0 ) ;
3534 const [ allScores , setAllScores ] = useState < Record < string , Record < number , Record < number , number > > > > ( { } ) ;
3635 // allScores[playerId][taskId][contestantId] = score
3736 const [ showResults , setShowResults ] = useState ( false ) ;
37+ const [ revealedObjective , setRevealedObjective ] = useState ( false ) ;
3838 const [ funFacts , setFunFacts ] = useState < FunFact [ ] > ( [ ] ) ;
3939 const [ shownFacts , setShownFacts ] = useState < Record < string , Set < string > > > ( { } ) ; // playerId -> set of shown fact texts
4040
@@ -49,30 +49,29 @@ export default function WatchModeScorer({ tasks, players, seasonNumber, onComple
4949 // Pick a relevant, non-repeated fun fact for the current player
5050 const getFactForPlayer = ( playerId : string ) : string | null => {
5151 const seen = shownFacts [ playerId ] || new Set < string > ( ) ;
52- // Priority 1: facts about contestants in this season
5352 const contestantFacts = funFacts . filter (
5453 ( f ) => f . contestants . some ( ( c ) => seasonContestants . includes ( c ) ) && ! seen . has ( f . text )
5554 ) ;
5655 if ( contestantFacts . length > 0 ) return contestantFacts [ Math . floor ( Math . random ( ) * contestantFacts . length ) ] . text ;
57- // Priority 2: facts about this season number
5856 const seasonFacts = funFacts . filter (
5957 ( f ) => f . seasons . includes ( seasonNumber ) && ! seen . has ( f . text )
6058 ) ;
6159 if ( seasonFacts . length > 0 ) return seasonFacts [ Math . floor ( Math . random ( ) * seasonFacts . length ) ] . text ;
62- // Priority 3: general facts (no season/contestant tags)
6360 const generalFacts = funFacts . filter (
6461 ( f ) => f . seasons . length === 0 && f . contestants . length === 0 && ! seen . has ( f . text )
6562 ) ;
6663 if ( generalFacts . length > 0 ) return generalFacts [ Math . floor ( Math . random ( ) * generalFacts . length ) ] . text ;
6764 return null ;
6865 } ;
6966
70- // Memoize the current fact so it doesn't change on re-render
7167 const [ currentFact , setCurrentFact ] = useState < string | null > ( null ) ;
7268
7369 useEffect ( ( ) => {
7470 if ( funFacts . length === 0 ) return ;
75- const playerId = players [ currentPlayerIdx ] ?. id ;
71+ // For objective tasks, use first player's id for fact tracking
72+ const playerId = currentTask ?. judgement === "objective"
73+ ? players [ 0 ] ?. id
74+ : players [ currentPlayerIdx ] ?. id ;
7675 if ( ! playerId ) return ;
7776 const fact = getFactForPlayer ( playerId ) ;
7877 setCurrentFact ( fact ) ;
@@ -86,10 +85,15 @@ export default function WatchModeScorer({ tasks, players, seasonNumber, onComple
8685 // eslint-disable-next-line react-hooks/exhaustive-deps
8786 } , [ currentTaskIdx , currentPlayerIdx , funFacts ] ) ;
8887
89- const currentTask = scorableTasks [ currentTaskIdx ] ;
88+ const currentTask = tasks [ currentTaskIdx ] ;
89+ const isObjective = currentTask ?. judgement === "objective" ;
90+ const scorableTasks = tasks . filter ( ( t ) => t . judgement !== "objective" ) ;
9091 const currentPlayer = players [ currentPlayerIdx ] ;
91- const totalSteps = scorableTasks . length * players . length ;
92- const currentStep = currentTaskIdx * players . length + currentPlayerIdx + 1 ;
92+
93+ // For subjective tasks: step counting only counts subjective tasks × players
94+ const subjectiveIdx = scorableTasks . indexOf ( currentTask ) ;
95+ const totalSteps = isObjective ? null : scorableTasks . length * players . length ;
96+ const currentStep = isObjective ? null : subjectiveIdx * players . length + currentPlayerIdx + 1 ;
9397
9498 const playerScoresForTask = allScores [ currentPlayer ?. id ] ?. [ currentTask ?. id ] || { } ;
9599
@@ -111,16 +115,30 @@ export default function WatchModeScorer({ tasks, players, seasonNumber, onComple
111115 ) ;
112116
113117 const handleNext = ( ) => {
114- if ( currentPlayerIdx < players . length - 1 ) {
118+ if ( isObjective ) {
119+ // Objective tasks: advance to next task (no per-player rotation)
120+ setRevealedObjective ( false ) ;
121+ if ( currentTaskIdx < tasks . length - 1 ) {
122+ setCurrentTaskIdx ( currentTaskIdx + 1 ) ;
123+ setCurrentPlayerIdx ( 0 ) ;
124+ } else {
125+ setShowResults ( true ) ;
126+ }
127+ } else if ( currentPlayerIdx < players . length - 1 ) {
115128 setCurrentPlayerIdx ( currentPlayerIdx + 1 ) ;
116- } else if ( currentTaskIdx < scorableTasks . length - 1 ) {
129+ } else if ( currentTaskIdx < tasks . length - 1 ) {
117130 setCurrentTaskIdx ( currentTaskIdx + 1 ) ;
118131 setCurrentPlayerIdx ( 0 ) ;
119132 } else {
120133 setShowResults ( true ) ;
121134 }
122135 } ;
123136
137+ // Also reset reveal state when task changes
138+ useEffect ( ( ) => {
139+ setRevealedObjective ( false ) ;
140+ } , [ currentTaskIdx ] ) ;
141+
124142 if ( showResults ) {
125143 return (
126144 < ResultsView
@@ -154,72 +172,171 @@ export default function WatchModeScorer({ tasks, players, seasonNumber, onComple
154172 ) ;
155173 }
156174
157- return (
158- < div className = "card" >
159- < div style = { { display : "flex" , justifyContent : "space-between" , alignItems : "center" , marginBottom : "1rem" } } >
160- < h2 >
161- Task { currentTaskIdx + 1 } /{ scorableTasks . length }
162- </ h2 >
163- < button onClick = { onCancel } style = { { background : "none" , border : "1px solid var(--tm-cream-dark)" , color : "var(--tm-text-muted)" , padding : "0.4rem 1rem" , borderRadius : "6px" , cursor : "pointer" } } >
164- Cancel
165- </ button >
166- </ div >
175+ // Objective task view
176+ if ( isObjective ) {
177+ return (
178+ < div >
179+ < div className = "card" >
180+ < div style = { { display : "flex" , justifyContent : "space-between" , alignItems : "center" , marginBottom : "1rem" } } >
181+ < h2 > Task { currentTaskIdx + 1 } /{ tasks . length } </ h2 >
182+ < button onClick = { onCancel } style = { { background : "none" , border : "1px solid var(--tm-cream-dark)" , color : "var(--tm-text-muted)" , padding : "0.4rem 1rem" , borderRadius : "6px" , cursor : "pointer" } } >
183+ Cancel
184+ </ button >
185+ </ div >
167186
168- < div style = { { background : "var(--tm-cream-dark)" , padding : "1rem" , borderRadius : "8px" , marginBottom : "1rem" } } >
169- < p style = { { color : "var(--tm-red)" , fontWeight : 700 , marginBottom : "0.5rem" } } > { currentTask . name } </ p >
170- < p style = { { color : "var(--tm-text-muted)" , fontSize : "0.85rem" } } >
171- Type: { currentTask . judgement } · Step { currentStep } / { totalSteps }
172- </ p >
173- </ div >
187+ < div style = { { background : "var(--tm-cream-dark)" , padding : "1rem" , borderRadius : "8px" , marginBottom : "1rem" } } >
188+ < p style = { { color : "var(--tm-red)" , fontWeight : 700 , marginBottom : "0.5rem" } } > { currentTask . name } </ p >
189+ < p style = { { color : "var(--tm-text-muted)" , fontSize : "0.85rem" } } >
190+ Type: objective — scores are determined by measurable results
191+ </ p >
192+ </ div >
174193
175- < div style = { {
176- background : `${ currentPlayer . color } 20` ,
177- border : `2px solid ${ currentPlayer . color } ` ,
178- borderRadius : "8px" ,
179- padding : "1rem" ,
180- marginBottom : "1rem" ,
181- textAlign : "center" ,
182- } } >
183- < p style = { { fontSize : "1.2rem" , fontWeight : 700 } } >
184- 🎯 { currentPlayer . name } , score this task!
185- </ p >
186- < p style = { { color : "var(--tm-text-muted)" , fontSize : "0.85rem" } } >
187- Award 1-5 points to each contestant
188- </ p >
189- </ div >
194+ < div style = { { display : "flex" , flexDirection : "column" , gap : "0.75rem" } } >
195+ { currentTask . contestants . map ( ( c ) => (
196+ < div key = { c . id } style = { {
197+ display : "flex" ,
198+ alignItems : "center" ,
199+ justifyContent : "space-between" ,
200+ background : "var(--tm-cream-dark)" ,
201+ padding : "0.5rem 1rem" ,
202+ borderRadius : "6px" ,
203+ } } >
204+ < span style = { { fontWeight : 600 } } > { c . name } </ span >
205+ < span style = { { fontWeight : 700 , fontSize : "1.1rem" } } >
206+ { revealedObjective ? `${ c . actualScore } pts` : "? ? ?" }
207+ </ span >
208+ </ div >
209+ ) ) }
210+ </ div >
190211
191- < div style = { { display : "flex" , flexDirection : "column" , gap : "0.75rem" } } >
192- { currentTask . contestants . map ( ( c ) => (
193- < div key = { c . id } style = { {
194- display : "flex" ,
195- alignItems : "center" ,
196- justifyContent : "space-between" ,
197- background : "var(--tm-cream-dark)" ,
212+ { ! revealedObjective && (
213+ < button
214+ onClick = { ( ) => setRevealedObjective ( true ) }
215+ style = { {
216+ width : "100%" ,
217+ marginTop : "1rem" ,
218+ padding : "0.75rem" ,
219+ background : "none" ,
220+ border : "1px solid var(--tm-cream-dark)" ,
221+ color : "var(--tm-text-muted)" ,
222+ borderRadius : "8px" ,
223+ cursor : "pointer" ,
224+ fontSize : "0.95rem" ,
225+ } }
226+ >
227+ 👀 Reveal Scores
228+ </ button >
229+ ) }
230+
231+ < button
232+ className = "btn-primary"
233+ style = { { width : "100%" , marginTop : "0.5rem" , padding : "0.75rem" } }
234+ onClick = { handleNext }
235+ >
236+ { currentTaskIdx === tasks . length - 1 ? "🏆 See Results!" : "Next Task →" }
237+ </ button >
238+ </ div >
239+
240+ { currentFact && (
241+ < div style = { {
242+ background : "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)" ,
243+ border : "1px solid #334155" ,
244+ borderRadius : "8px" ,
245+ padding : "0.75rem 1rem" ,
246+ marginTop : "1rem" ,
198247 } } >
199- < span style = { { fontWeight : 600 } } > { c . name } </ span >
200- < div style = { { display : "flex" , gap : "0.25rem" } } >
201- { [ 1 , 2 , 3 , 4 , 5 ] . map ( ( score ) => (
202- < button
203- key = { score }
204- onClick = { ( ) => setScore ( c . id , score ) }
205- style = { {
206- width : "40px" ,
207- height : "40px" ,
208- borderRadius : "8px" ,
209- border : playerScoresForTask [ c . id ] === score ? "2px solid var(--tm-red)" : "1px solid var(--tm-cream-dark)" ,
210- background : playerScoresForTask [ c . id ] === score ? "var(--tm-red-bright)" : "white" ,
211- color : playerScoresForTask [ c . id ] === score ? "white" : "var(--tm-text-dark)" ,
212- fontWeight : 700 ,
213- cursor : "pointer" ,
214- fontSize : "1rem" ,
215- } }
216- >
217- { score }
218- </ button >
219- ) ) }
220- </ div >
248+ < p style = { { margin : 0 , fontSize : "0.85rem" , color : "#94a3b8" } } >
249+ < span style = { { marginRight : "0.4rem" } } > 💡</ span >
250+ { currentFact }
251+ </ p >
221252 </ div >
222- ) ) }
253+ ) }
254+ </ div >
255+ ) ;
256+ }
257+
258+ // Subjective/combo task view
259+ return (
260+ < div >
261+ < div className = "card" >
262+ < div style = { { display : "flex" , justifyContent : "space-between" , alignItems : "center" , marginBottom : "1rem" } } >
263+ < h2 >
264+ Task { currentTaskIdx + 1 } /{ tasks . length }
265+ </ h2 >
266+ < button onClick = { onCancel } style = { { background : "none" , border : "1px solid var(--tm-cream-dark)" , color : "var(--tm-text-muted)" , padding : "0.4rem 1rem" , borderRadius : "6px" , cursor : "pointer" } } >
267+ Cancel
268+ </ button >
269+ </ div >
270+
271+ < div style = { { background : "var(--tm-cream-dark)" , padding : "1rem" , borderRadius : "8px" , marginBottom : "1rem" } } >
272+ < p style = { { color : "var(--tm-red)" , fontWeight : 700 , marginBottom : "0.5rem" } } > { currentTask . name } </ p >
273+ < p style = { { color : "var(--tm-text-muted)" , fontSize : "0.85rem" } } >
274+ Type: { currentTask . judgement } · Step { currentStep } /{ totalSteps }
275+ </ p >
276+ </ div >
277+
278+ < div style = { {
279+ background : `${ currentPlayer . color } 20` ,
280+ border : `2px solid ${ currentPlayer . color } ` ,
281+ borderRadius : "8px" ,
282+ padding : "1rem" ,
283+ marginBottom : "1rem" ,
284+ textAlign : "center" ,
285+ } } >
286+ < p style = { { fontSize : "1.2rem" , fontWeight : 700 } } >
287+ 🎯 { currentPlayer . name } , score this task!
288+ </ p >
289+ < p style = { { color : "var(--tm-text-muted)" , fontSize : "0.85rem" } } >
290+ Award 1-5 points to each contestant
291+ </ p >
292+ </ div >
293+
294+ < div style = { { display : "flex" , flexDirection : "column" , gap : "0.75rem" } } >
295+ { currentTask . contestants . map ( ( c ) => (
296+ < div key = { c . id } style = { {
297+ display : "flex" ,
298+ alignItems : "center" ,
299+ justifyContent : "space-between" ,
300+ background : "var(--tm-cream-dark)" ,
301+ } } >
302+ < span style = { { fontWeight : 600 } } > { c . name } </ span >
303+ < div style = { { display : "flex" , gap : "0.25rem" } } >
304+ { [ 1 , 2 , 3 , 4 , 5 ] . map ( ( score ) => (
305+ < button
306+ key = { score }
307+ onClick = { ( ) => setScore ( c . id , score ) }
308+ style = { {
309+ width : "40px" ,
310+ height : "40px" ,
311+ borderRadius : "8px" ,
312+ border : playerScoresForTask [ c . id ] === score ? "2px solid var(--tm-red)" : "1px solid var(--tm-cream-dark)" ,
313+ background : playerScoresForTask [ c . id ] === score ? "var(--tm-red-bright)" : "white" ,
314+ color : playerScoresForTask [ c . id ] === score ? "white" : "var(--tm-text-dark)" ,
315+ fontWeight : 700 ,
316+ cursor : "pointer" ,
317+ fontSize : "1rem" ,
318+ } }
319+ >
320+ { score }
321+ </ button >
322+ ) ) }
323+ </ div >
324+ </ div >
325+ ) ) }
326+ </ div >
327+
328+ < button
329+ className = "btn-primary"
330+ style = { { width : "100%" , marginTop : "1rem" , padding : "0.75rem" } }
331+ onClick = { handleNext }
332+ disabled = { ! allContestantsScored }
333+ >
334+ { currentTaskIdx === tasks . length - 1 && currentPlayerIdx === players . length - 1
335+ ? "🏆 See Results!"
336+ : currentPlayerIdx === players . length - 1
337+ ? "Next Task →"
338+ : `Next: ${ players [ currentPlayerIdx + 1 ] . name } 's turn →` }
339+ </ button >
223340 </ div >
224341
225342 { currentFact && (
@@ -236,19 +353,6 @@ export default function WatchModeScorer({ tasks, players, seasonNumber, onComple
236353 </ p >
237354 </ div >
238355 ) }
239-
240- < button
241- className = "btn-primary"
242- style = { { width : "100%" , marginTop : "1rem" , padding : "0.75rem" } }
243- onClick = { handleNext }
244- disabled = { ! allContestantsScored }
245- >
246- { currentTaskIdx === scorableTasks . length - 1 && currentPlayerIdx === players . length - 1
247- ? "🏆 See Results!"
248- : currentPlayerIdx === players . length - 1
249- ? "Next Task →"
250- : `Next: ${ players [ currentPlayerIdx + 1 ] . name } 's turn →` }
251- </ button >
252356 </ div >
253357 ) ;
254358}
0 commit comments