11import { useState , useEffect } from "react" ;
2- import { fetchContestants , fetchAnalysis , fetchSeasons } from "../hooks/useData" ;
2+ import { fetchContestants , fetchAnalysis } from "../hooks/useData" ;
3+
4+ interface TaskStat { attempted : number ; won : number ; winPct : number ; ppt : number }
5+ interface Contestant {
6+ id : number ; name : string ; seasonIds : number [ ] ; seasonWins : number ;
7+ episodes : number ; episodeWins : number ; episodeWinPct : number ;
8+ totalPoints : number ; pointsPerTask : number ; pointsPerEpisode : number ;
9+ tasksAttempted : number ; tasksWon : number ; taskWinPct : number ;
10+ basePoints : number ; bonusPoints : number ; pointsDeducted : number ; dqs : number ;
11+ taskBreakdown : {
12+ format : Record < string , TaskStat > ;
13+ setting : Record < string , TaskStat > ;
14+ activity : Record < string , TaskStat > ;
15+ judgement : Record < string , TaskStat > ;
16+ } ;
17+ }
318
419interface Analysis {
520 totalContestants : number ;
@@ -9,28 +24,136 @@ interface Analysis {
924 seasonWinners : { season : number ; name : string ; id : number } [ ] ;
1025}
1126
27+ function computeShowStats ( contestants : Contestant [ ] ) {
28+ // Task makeup by setting (prize/filmed/homework/live)
29+ const settingTotals : Record < string , number > = { } ;
30+ const formatTotals : Record < string , number > = { } ;
31+ const activityTotals : Record < string , number > = { } ;
32+ const judgementTotals : Record < string , number > = { } ;
33+ let totalTasks = 0 ;
34+ let totalBonus = 0 , totalDeductions = 0 , totalDQs = 0 ;
35+ let totalEpisodes = 0 , totalPoints = 0 ;
36+
37+ // Per-season aggregates
38+ const seasonData : Record < number , { bonus : number ; deductions : number ; dqs : number ; points : number ; episodes : number ; contestants : number } > = { } ;
39+
40+ // Records
41+ let highestPpT = { name : "" , val : 0 } ;
42+ let lowestPpT = { name : "" , val : Infinity } ;
43+ let mostDQs = { name : "" , val : 0 } ;
44+ let mostEpWins = { name : "" , val : 0 } ;
45+ let mostTasksWon = { name : "" , val : 0 } ;
46+ let highestTotal = { name : "" , val : 0 } ;
47+ let lowestTotal = { name : "" , val : Infinity } ;
48+
49+ for ( const c of contestants ) {
50+ // Aggregates
51+ totalBonus += c . bonusPoints ;
52+ totalDeductions += c . pointsDeducted ;
53+ totalDQs += c . dqs ;
54+ totalPoints += c . totalPoints ;
55+
56+ // Records
57+ if ( c . pointsPerTask > highestPpT . val ) highestPpT = { name : c . name , val : c . pointsPerTask } ;
58+ if ( c . pointsPerTask < lowestPpT . val ) lowestPpT = { name : c . name , val : c . pointsPerTask } ;
59+ if ( c . dqs > mostDQs . val ) mostDQs = { name : c . name , val : c . dqs } ;
60+ if ( c . episodeWins > mostEpWins . val ) mostEpWins = { name : c . name , val : c . episodeWins } ;
61+ if ( c . tasksWon > mostTasksWon . val ) mostTasksWon = { name : c . name , val : c . tasksWon } ;
62+ if ( c . totalPoints > highestTotal . val ) highestTotal = { name : c . name , val : c . totalPoints } ;
63+ if ( c . totalPoints < lowestTotal . val ) lowestTotal = { name : c . name , val : c . totalPoints } ;
64+
65+ for ( const sId of c . seasonIds ) {
66+ if ( ! seasonData [ sId ] ) seasonData [ sId ] = { bonus : 0 , deductions : 0 , dqs : 0 , points : 0 , episodes : 0 , contestants : 0 } ;
67+ seasonData [ sId ] . bonus += c . bonusPoints ;
68+ seasonData [ sId ] . deductions += c . pointsDeducted ;
69+ seasonData [ sId ] . dqs += c . dqs ;
70+ seasonData [ sId ] . points += c . totalPoints ;
71+ seasonData [ sId ] . contestants += 1 ;
72+ seasonData [ sId ] . episodes = Math . max ( seasonData [ sId ] . episodes , c . episodes ) ;
73+ }
74+
75+ // Task breakdowns — divide by 5 (contestants per season) to get unique task counts
76+ for ( const [ k , v ] of Object . entries ( c . taskBreakdown . setting ) ) {
77+ settingTotals [ k ] = ( settingTotals [ k ] || 0 ) + v . attempted ;
78+ }
79+ for ( const [ k , v ] of Object . entries ( c . taskBreakdown . format ) ) {
80+ formatTotals [ k ] = ( formatTotals [ k ] || 0 ) + v . attempted ;
81+ }
82+ for ( const [ k , v ] of Object . entries ( c . taskBreakdown . activity ) ) {
83+ activityTotals [ k ] = ( activityTotals [ k ] || 0 ) + v . attempted ;
84+ }
85+ for ( const [ k , v ] of Object . entries ( c . taskBreakdown . judgement ) ) {
86+ judgementTotals [ k ] = ( judgementTotals [ k ] || 0 ) + v . attempted ;
87+ }
88+ totalTasks += c . tasksAttempted ;
89+ }
90+
91+ // Each task is attempted by 5 contestants, so unique tasks = total / 5
92+ const uniqueTasks = Math . round ( totalTasks / 5 ) ;
93+ for ( const seasons of Object . values ( seasonData ) ) {
94+ totalEpisodes += seasons . episodes ;
95+ }
96+
97+ // Per-season bonus/dqs for the table
98+ const seasonStats = Object . entries ( seasonData )
99+ . map ( ( [ s , d ] ) => ( { season : parseInt ( s ) , ...d , avgPpE : + ( d . points / ( d . episodes * 5 ) ) . toFixed ( 1 ) } ) )
100+ . sort ( ( a , b ) => a . season - b . season ) ;
101+
102+ return {
103+ settingTotals, formatTotals, activityTotals, judgementTotals,
104+ uniqueTasks, totalTasks, totalBonus, totalDeductions, totalDQs,
105+ totalEpisodes, totalPoints,
106+ seasonStats,
107+ records : { highestPpT, lowestPpT, mostDQs, mostEpWins, mostTasksWon, highestTotal, lowestTotal } ,
108+ } ;
109+ }
110+
12111export default function Dashboard ( ) {
13112 const [ analysis , setAnalysis ] = useState < Analysis | null > ( null ) ;
113+ const [ contestants , setContestants ] = useState < Contestant [ ] > ( [ ] ) ;
14114 const [ loading , setLoading ] = useState ( true ) ;
15115
16116 useEffect ( ( ) => {
17- fetchAnalysis ( ) . then ( ( data ) => { setAnalysis ( data ) ; setLoading ( false ) ; } ) ;
117+ Promise . all ( [ fetchAnalysis ( ) , fetchContestants ( ) ] ) . then ( ( [ a , c ] ) => {
118+ setAnalysis ( a ) ;
119+ setContestants ( c ) ;
120+ setLoading ( false ) ;
121+ } ) ;
18122 } , [ ] ) ;
19123
20124 if ( loading || ! analysis ) return < div className = "loading" > < div className = "spinner" /> Loading insights...</ div > ;
21125
22126 const w = analysis . winners ;
23127 const nw = analysis . nonWinners ;
128+ const stats = computeShowStats ( contestants ) ;
129+
130+ const divBy5 = ( totals : Record < string , number > ) => {
131+ const out : Record < string , number > = { } ;
132+ for ( const [ k , v ] of Object . entries ( totals ) ) out [ k ] = Math . round ( v / 5 ) ;
133+ return out ;
134+ } ;
135+ const settingTasks = divBy5 ( stats . settingTotals ) ;
136+ const formatTasks = divBy5 ( stats . formatTotals ) ;
137+ const activityTasks = divBy5 ( stats . activityTotals ) ;
138+ const settingTotal = Object . values ( settingTasks ) . reduce ( ( a , b ) => a + b , 0 ) ;
139+ const formatTotal = Object . values ( formatTasks ) . reduce ( ( a , b ) => a + b , 0 ) ;
140+ const activityTotal = Object . values ( activityTasks ) . reduce ( ( a , b ) => a + b , 0 ) ;
141+
142+ const settingLabels : Record < string , string > = { prize : "🎁 Prize" , filmed : "🎬 Filmed" , homework : "📝 Homework" , live : "🎤 Live" } ;
143+ const formatLabels : Record < string , string > = { solo : "👤 Solo" , team : "👥 Team" , split : "✂️ Split" , tiebreak : "⚡ Tiebreak" } ;
144+ const activityLabels : Record < string , string > = { creative : "🎨 Creative" , mental : "🧠 Mental" , physical : "💪 Physical" , social : "🗣️ Social" } ;
24145
25146 return (
26147 < div >
27148 < div className = "card" >
28149 < h2 > 🏆 Taskmaster UK — By The Numbers</ h2 >
29150 < div className = "stat-grid" >
30151 < div className = "stat-item" > < div className = "stat-value" > { analysis . totalContestants } </ div > < div className = "stat-label" > Contestants</ div > </ div >
31- < div className = "stat-item" > < div className = "stat-value" > { analysis . totalSeasons } </ div > < div className = "stat-label" > Seasons</ div > </ div >
32- < div className = "stat-item" > < div className = "stat-value" > { w . count } </ div > < div className = "stat-label" > Champions</ div > </ div >
33- < div className = "stat-item" > < div className = "stat-value" > { w . avgPointsPerTask } </ div > < div className = "stat-label" > Avg Winner PpT</ div > </ div >
152+ < div className = "stat-item" > < div className = "stat-value" > { analysis . totalSeasons } </ div > < div className = "stat-label" > Series</ div > </ div >
153+ < div className = "stat-item" > < div className = "stat-value" > { stats . totalEpisodes } </ div > < div className = "stat-label" > Episodes</ div > </ div >
154+ < div className = "stat-item" > < div className = "stat-value" > { stats . uniqueTasks } </ div > < div className = "stat-label" > Tasks</ div > </ div >
155+ < div className = "stat-item" > < div className = "stat-value" > { stats . totalDQs } </ div > < div className = "stat-label" > Disqualifications</ div > </ div >
156+ < div className = "stat-item" > < div className = "stat-value" > { stats . totalBonus } </ div > < div className = "stat-label" > Bonus Points Awarded</ div > </ div >
34157 </ div >
35158 </ div >
36159
@@ -74,32 +197,139 @@ export default function Dashboard() {
74197 </ div >
75198
76199 < div className = "card" >
77- < h2 > 🏆 Season Champions</ h2 >
200+ < h2 > 📋 Task Makeup — By Setting</ h2 >
201+ < table className = "comparison-table" >
202+ < thead > < tr > < th > Setting</ th > < th > Tasks</ th > < th > % of Total</ th > </ tr > </ thead >
203+ < tbody >
204+ { Object . entries ( settingTasks )
205+ . sort ( ( [ , a ] , [ , b ] ) => b - a )
206+ . map ( ( [ k , v ] ) => (
207+ < tr key = { k } >
208+ < td > { settingLabels [ k ] || k } </ td >
209+ < td > < strong > { v } </ strong > </ td >
210+ < td > { ( ( v / settingTotal ) * 100 ) . toFixed ( 1 ) } %</ td >
211+ </ tr >
212+ ) ) }
213+ </ tbody >
214+ </ table >
215+ < BarChart data = { settingTasks } labels = { settingLabels } total = { settingTotal } />
216+ </ div >
217+
218+ < div className = "card" >
219+ < h2 > 👥 Task Makeup — By Format</ h2 >
220+ < table className = "comparison-table" >
221+ < thead > < tr > < th > Format</ th > < th > Tasks</ th > < th > % of Total</ th > </ tr > </ thead >
222+ < tbody >
223+ { Object . entries ( formatTasks )
224+ . sort ( ( [ , a ] , [ , b ] ) => b - a )
225+ . map ( ( [ k , v ] ) => (
226+ < tr key = { k } >
227+ < td > { formatLabels [ k ] || k } </ td >
228+ < td > < strong > { v } </ strong > </ td >
229+ < td > { ( ( v / formatTotal ) * 100 ) . toFixed ( 1 ) } %</ td >
230+ </ tr >
231+ ) ) }
232+ </ tbody >
233+ </ table >
234+ < BarChart data = { formatTasks } labels = { formatLabels } total = { formatTotal } />
235+ </ div >
236+
237+ < div className = "card" >
238+ < h2 > 🎯 Task Makeup — By Activity</ h2 >
78239 < table className = "comparison-table" >
79- < thead > < tr > < th > Season </ th > < th > Champion </ th > </ tr > </ thead >
240+ < thead > < tr > < th > Activity </ th > < th > Tasks </ th > < th > % of Total </ th > </ tr > </ thead >
80241 < tbody >
81- { analysis . seasonWinners . map ( ( sw ) => (
82- < tr key = { sw . season } >
83- < td > Series { sw . season } </ td >
84- < td > < strong > { sw . name } </ strong > </ td >
242+ { Object . entries ( activityTasks )
243+ . sort ( ( [ , a ] , [ , b ] ) => b - a )
244+ . map ( ( [ k , v ] ) => (
245+ < tr key = { k } >
246+ < td > { activityLabels [ k ] || k } </ td >
247+ < td > < strong > { v } </ strong > </ td >
248+ < td > { ( ( v / activityTotal ) * 100 ) . toFixed ( 1 ) } %</ td >
249+ </ tr >
250+ ) ) }
251+ </ tbody >
252+ </ table >
253+ < BarChart data = { activityTasks } labels = { activityLabels } total = { activityTotal } />
254+ </ div >
255+
256+ < div className = "card card-full" >
257+ < h2 > 📊 Season-by-Season Stats</ h2 >
258+ < table className = "comparison-table" style = { { tableLayout : "auto" } } >
259+ < thead > < tr > < th > Series</ th > < th > Eps</ th > < th > Bonus</ th > < th > Deductions</ th > < th > DQs</ th > < th > Avg PpE</ th > </ tr > </ thead >
260+ < tbody >
261+ { stats . seasonStats . map ( ( s ) => (
262+ < tr key = { s . season } >
263+ < td > S{ s . season } </ td >
264+ < td > { s . episodes } </ td >
265+ < td > { s . bonus } </ td >
266+ < td > { s . deductions } </ td >
267+ < td > { s . dqs } </ td >
268+ < td > < strong > { s . avgPpE } </ strong > </ td >
85269 </ tr >
86270 ) ) }
87271 </ tbody >
88272 </ table >
89273 </ div >
274+
275+ < div className = "card" >
276+ < h2 > 🎖️ All-Time Records</ h2 >
277+ < table className = "comparison-table" >
278+ < thead > < tr > < th > Record</ th > < th > Contestant</ th > < th > Value</ th > </ tr > </ thead >
279+ < tbody >
280+ < tr > < td > 🔥 Highest PpT</ td > < td > < strong > { stats . records . highestPpT . name } </ strong > </ td > < td > { stats . records . highestPpT . val } </ td > </ tr >
281+ < tr > < td > 📉 Lowest PpT</ td > < td > < strong > { stats . records . lowestPpT . name } </ strong > </ td > < td > { stats . records . lowestPpT . val } </ td > </ tr >
282+ < tr > < td > ⭐ Most Points</ td > < td > < strong > { stats . records . highestTotal . name } </ strong > </ td > < td > { stats . records . highestTotal . val } </ td > </ tr >
283+ < tr > < td > 😬 Fewest Points</ td > < td > < strong > { stats . records . lowestTotal . name } </ strong > </ td > < td > { stats . records . lowestTotal . val } </ td > </ tr >
284+ < tr > < td > 🏅 Most Tasks Won</ td > < td > < strong > { stats . records . mostTasksWon . name } </ strong > </ td > < td > { stats . records . mostTasksWon . val } </ td > </ tr >
285+ < tr > < td > 📺 Most Episode Wins</ td > < td > < strong > { stats . records . mostEpWins . name } </ strong > </ td > < td > { stats . records . mostEpWins . val } </ td > </ tr >
286+ < tr > < td > 🚫 Most DQs</ td > < td > < strong > { stats . records . mostDQs . name } </ strong > </ td > < td > { stats . records . mostDQs . val } </ td > </ tr >
287+ </ tbody >
288+ </ table >
289+ </ div >
90290 </ div >
91291 </ div >
92292 ) ;
93293}
94294
295+ function BarChart ( { data, labels, total } : { data : Record < string , number > ; labels : Record < string , string > ; total : number } ) {
296+ const sorted = Object . entries ( data ) . sort ( ( [ , a ] , [ , b ] ) => b - a ) ;
297+ const max = sorted [ 0 ] ?. [ 1 ] || 1 ;
298+ const colors = [ "#790000" , "#a1663c" , "#CC0000" , "#44250a" , "#8a7a60" ] ;
299+ return (
300+ < div style = { { marginTop : "0.75rem" , display : "flex" , flexDirection : "column" , gap : "0.35rem" } } >
301+ { sorted . map ( ( [ k , v ] , i ) => (
302+ < div key = { k } style = { { display : "flex" , alignItems : "center" , gap : "0.5rem" } } >
303+ < span style = { { width : "110px" , fontSize : "0.75rem" , textAlign : "right" , flexShrink : 0 } } >
304+ { ( labels [ k ] || k ) . replace ( / ^ [ ^ \s ] + \s / , "" ) }
305+ </ span >
306+ < div style = { { flex : 1 , background : "var(--tm-cream-dark)" , borderRadius : "3px" , height : "18px" , overflow : "hidden" } } >
307+ < div style = { {
308+ width : `${ ( v / max ) * 100 } %` ,
309+ height : "100%" ,
310+ background : colors [ i % colors . length ] ,
311+ borderRadius : "3px" ,
312+ transition : "width 0.5s" ,
313+ } } />
314+ </ div >
315+ < span style = { { fontSize : "0.75rem" , width : "40px" , flexShrink : 0 } } >
316+ { ( ( v / total ) * 100 ) . toFixed ( 0 ) } %
317+ </ span >
318+ </ div >
319+ ) ) }
320+ </ div >
321+ ) ;
322+ }
323+
95324function CompRow ( { label, w, nw, suffix = "" } : { label : string ; w : number ; nw : number ; suffix ?: string } ) {
96325 const edge = w - nw ;
97- const edgeStr = edge > 0 ? `+${ edge . toFixed ( 2 ) } ${ suffix } ` : `${ edge . toFixed ( 2 ) } ${ suffix } ` ;
326+ const fmt = ( n : number ) => Number . isInteger ( n ) ? n . toString ( ) : n . toFixed ( 2 ) ;
327+ const edgeStr = edge > 0 ? `+${ fmt ( edge ) } ${ suffix } ` : `${ fmt ( edge ) } ${ suffix } ` ;
98328 return (
99329 < tr >
100330 < td > { label } </ td >
101- < td > < strong > { w } { suffix } </ strong > </ td >
102- < td > { nw } { suffix } </ td >
331+ < td > < strong > { fmt ( w ) } { suffix } </ strong > </ td >
332+ < td > { fmt ( nw ) } { suffix } </ td >
103333 < td className = { edge > 0 ? "highlight-better" : "highlight-worse" } > { edgeStr } </ td >
104334 </ tr >
105335 ) ;
0 commit comments