diff --git a/soccer-app/src/App.css b/soccer-app/src/App.css index 201dbc5..2de3053 100644 --- a/soccer-app/src/App.css +++ b/soccer-app/src/App.css @@ -645,6 +645,78 @@ color: #f87171; } +/* ===== Insights Legend ===== */ +.insights-legend { + margin-top: 16px; + padding: 14px 16px; + background: #1e293b; + border: 1px solid #334155; + border-radius: 10px; +} + +.insights-legend-heading { + margin: 0 0 10px; + font-size: 0.7rem; + color: #64748b; + text-transform: uppercase; + font-weight: 700; + letter-spacing: 0.05em; +} + +.insights-legend-list { + margin: 0; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 8px 16px; +} + +.insights-legend-item { + display: flex; + align-items: baseline; + gap: 8px; +} + +.insights-legend-term { + flex-shrink: 0; + padding: 2px 8px; + border-radius: 4px; + border-left: 3px solid #64748b; + background: #0f172a; + font-size: 0.75rem; + font-weight: 700; + color: #f1f5f9; +} + +/* Reuse the rating accent colours from the insight cards. The legend term uses + the same `insight-rating-*` classes, so the border colour is inherited; the + text colour is set here for the legend context. */ +.insights-legend-term.insight-rating-elite { + color: #22d3ee; + border-left-color: #22d3ee; +} + +.insights-legend-term.insight-rating-strong { + color: #4ade80; + border-left-color: #4ade80; +} + +.insights-legend-term.insight-rating-average { + color: #fb923c; + border-left-color: #fb923c; +} + +.insights-legend-term.insight-rating-struggling { + color: #f87171; + border-left-color: #f87171; +} + +.insights-legend-desc { + margin: 0; + font-size: 0.75rem; + color: #94a3b8; + line-height: 1.4; +} + /* ===== Highlights ===== */ .highlights-grid { display: grid; @@ -797,6 +869,10 @@ grid-template-columns: 1fr; } + .insights-legend-list { + grid-template-columns: 1fr; + } + .team-detail-header { flex-direction: column; } diff --git a/soccer-app/src/components/TeamInsights.tsx b/soccer-app/src/components/TeamInsights.tsx index fc46ded..021cac9 100644 --- a/soccer-app/src/components/TeamInsights.tsx +++ b/soccer-app/src/components/TeamInsights.tsx @@ -2,6 +2,7 @@ import type { TeamStats } from "../data/teams"; import { goalDifference, performanceLabel, + performanceLegend, winRate, } from "../utils/teamMetrics"; @@ -17,8 +18,14 @@ interface TeamInsightsProps { * qualitative performance label) rather than recomputing anything locally. * Meaning is conveyed through text as well as colour so the panel does not * rely on colour alone. + * + * The panel is resilient to missing or zero-match statistics: when a team has + * played no matches it shows honest placeholder copy instead of misleading + * figures (e.g. "0%" win rate or "NaN" from dividing by zero). */ export function TeamInsights({ teamName, stats }: TeamInsightsProps) { + const hasPlayed = stats.played > 0; + const goalDiff = goalDifference(stats); const winRatePercent = Math.round(winRate(stats) * 100); const label = performanceLabel(stats); @@ -29,45 +36,99 @@ export function TeamInsights({ teamName, stats }: TeamInsightsProps) { const goalDiffWord = goalDiff > 0 ? "Positive" : goalDiff < 0 ? "Negative" : "Even"; + const placeholder = "—"; + return (

Team Insights

- A quick read on how {teamName} are performing this - season. + {hasPlayed ? ( + <> + A quick read on how {teamName} are performing this + season. + + ) : ( + <> + {teamName} haven’t played any matches yet, so + there are no performance figures to show. + + )}

-
+
Goal Difference
- {goalDiffDisplay} - {goalDiffWord} + {hasPlayed ? ( + <> + {goalDiffDisplay} + {goalDiffWord} + + ) : ( + {placeholder} + )}

- {stats.goalsFor} scored, {stats.goalsAgainst} conceded + {hasPlayed + ? `${stats.goalsFor} scored, ${stats.goalsAgainst} conceded` + : "No matches played yet"}

Win Rate
- {winRatePercent}% + + {hasPlayed ? `${winRatePercent}%` : placeholder} +

- {stats.won} wins from {stats.played} played + {hasPlayed + ? `${stats.won} wins from ${stats.played} played` + : "No matches played yet"}

-
+
Performance
- {label} + {hasPlayed ? label : "Not rated"}
-

Based on the team’s win rate

+

+ {hasPlayed + ? "Based on the team’s win rate" + : "Rating available after the first match"} +

+ +
+

+ Performance ratings +

+
+ {performanceLegend.map((entry) => ( +
+
+ {entry.label} +
+
{entry.description}
+
+ ))} +
+
); } diff --git a/soccer-app/src/utils/teamMetrics.ts b/soccer-app/src/utils/teamMetrics.ts index ebe9c93..f8634f7 100644 --- a/soccer-app/src/utils/teamMetrics.ts +++ b/soccer-app/src/utils/teamMetrics.ts @@ -65,3 +65,27 @@ export function performanceLabel(stats: TeamStats): PerformanceLabel { } return "Struggling"; } + +/** + * A single entry in the performance-label legend. + */ +export interface PerformanceLegendEntry { + /** The qualitative label. */ + label: PerformanceLabel; + /** Human-readable description of the win-rate band the label covers. */ + description: string; +} + +/** + * Legend describing the win-rate bands behind each performance label. + * + * Kept alongside {@link performanceLabel} so the copy shown to users stays in + * sync with the thresholds used to compute the label. Ordered from strongest + * to weakest. + */ +export const performanceLegend: readonly PerformanceLegendEntry[] = [ + { label: "Elite", description: "Win rate 70% or higher" }, + { label: "Strong", description: "Win rate 50–69%" }, + { label: "Average", description: "Win rate 30–49%" }, + { label: "Struggling", description: "Win rate below 30%" }, +];