Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions soccer-app/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -797,6 +869,10 @@
grid-template-columns: 1fr;
}

.insights-legend-list {
grid-template-columns: 1fr;
}

.team-detail-header {
flex-direction: column;
}
Expand Down
83 changes: 72 additions & 11 deletions soccer-app/src/components/TeamInsights.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { TeamStats } from "../data/teams";
import {
goalDifference,
performanceLabel,
performanceLegend,
winRate,
} from "../utils/teamMetrics";

Expand All @@ -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);
Expand All @@ -29,45 +36,99 @@ export function TeamInsights({ teamName, stats }: TeamInsightsProps) {
const goalDiffWord =
goalDiff > 0 ? "Positive" : goalDiff < 0 ? "Negative" : "Even";

const placeholder = "—";

return (
<section className="insights-section" aria-labelledby="insights-heading">
<h3 id="insights-heading">Team Insights</h3>
<p className="insights-summary">
A quick read on how <strong>{teamName}</strong> are performing this
season.
{hasPlayed ? (
<>
A quick read on how <strong>{teamName}</strong> are performing this
season.
</>
) : (
<>
<strong>{teamName}</strong> haven&rsquo;t played any matches yet, so
there are no performance figures to show.
</>
)}
</p>
<dl className="insights-grid">
<div className={`insight-card insight-${goalDiffTrend}`}>
<div
className={`insight-card ${hasPlayed ? `insight-${goalDiffTrend}` : ""}`}
>
<dt className="insight-label">Goal Difference</dt>
<dd className="insight-value">
<span className="insight-figure">{goalDiffDisplay}</span>
<span className="insight-tag">{goalDiffWord}</span>
{hasPlayed ? (
<>
<span className="insight-figure">{goalDiffDisplay}</span>
<span className="insight-tag">{goalDiffWord}</span>
</>
) : (
<span className="insight-figure">{placeholder}</span>
)}
</dd>
<p className="insight-note">
{stats.goalsFor} scored, {stats.goalsAgainst} conceded
{hasPlayed
? `${stats.goalsFor} scored, ${stats.goalsAgainst} conceded`
: "No matches played yet"}
</p>
</div>

<div className="insight-card">
<dt className="insight-label">Win Rate</dt>
<dd className="insight-value">
<span className="insight-figure">{winRatePercent}%</span>
<span className="insight-figure">
{hasPlayed ? `${winRatePercent}%` : placeholder}
</span>
</dd>
<p className="insight-note">
{stats.won} wins from {stats.played} played
{hasPlayed
? `${stats.won} wins from ${stats.played} played`
: "No matches played yet"}
</p>
</div>

<div className={`insight-card insight-rating-${label.toLowerCase()}`}>
<div
className={`insight-card ${
hasPlayed ? `insight-rating-${label.toLowerCase()}` : ""
}`}
>
<dt className="insight-label">Performance</dt>
<dd className="insight-value">
<span className="insight-figure insight-figure-label">
{label}
{hasPlayed ? label : "Not rated"}
</span>
</dd>
<p className="insight-note">Based on the team&rsquo;s win rate</p>
<p className="insight-note">
{hasPlayed
? "Based on the team’s win rate"
: "Rating available after the first match"}
</p>
</div>
</dl>

<div className="insights-legend">
<h4 className="insights-legend-heading" id="insights-legend-heading">
Performance ratings
</h4>
<dl
className="insights-legend-list"
aria-labelledby="insights-legend-heading"
>
{performanceLegend.map((entry) => (
<div className="insights-legend-item" key={entry.label}>
<dt
className={`insights-legend-term insight-rating-${entry.label.toLowerCase()}`}
>
{entry.label}
</dt>
<dd className="insights-legend-desc">{entry.description}</dd>
</div>
))}
</dl>
</div>
</section>
);
}
24 changes: 24 additions & 0 deletions soccer-app/src/utils/teamMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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%" },
];