Skip to content
Merged
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
6 changes: 6 additions & 0 deletions docs/IMPLEMENTATION_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ Working:
advanced; knockout matches whose participants did not include the
user's stored pick are tracked as `forfeitedKnockoutPoints` and
excluded from their per-user maximum-possible score
- tournament and league leaderboards render the Total column as
`score / per-user maximum possible`
(`tournament.rules.totalMaximumPoints − forfeitedKnockoutPoints`)
with an inline footnote when any player has forfeited points; the
global leaderboard and profile still display earned totals only
because a cross-tournament maximum is not yet exposed

### 2.4 League Participation

Expand Down
9 changes: 9 additions & 0 deletions docs/QA_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,13 @@ Expected:
- tournament leaderboard reflects tournament-scope scores
- round columns match the structure
- prize UI only appears when enabled
- the Total column renders as `score / per-user max`. When no group has
closed with corrected seeding yet, every row's max equals
`tournament.rules.totalMaximumPoints`. After a group is closed and at
least one user has a knockout pick whose actual participants did not
include their stored team, that row's max drops by
`pointsPerCorrect` for each such match and an explanatory footnote
appears below the table

## 15. League Leaderboard

Expand All @@ -296,6 +303,8 @@ Expected:

- league leaderboard is filtered to that league
- tournament leaderboard is not replaced by league-only results
- the Total column on the league board renders the same
`score / per-user max` treatment as the tournament leaderboard

## 16. League Prediction Copy Flow

Expand Down
4 changes: 4 additions & 0 deletions src/i18n/messages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,10 @@ export const EN = {
sfPts: 'SF',
finalPts: 'Final',
total: 'Total',
effectiveMaxTooltip:
'Score / your maximum possible. Knockout matches whose actual teams did not include your stored pick are excluded from your maximum.',
effectiveMaxNote:
'Total shows score / per-user maximum. Knockout matches whose actual teams did not include your stored pick are excluded from your maximum.',
you: 'You',
prizePool: 'Prize Pool',
participants: 'Participants',
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/messages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,10 @@ export const ES = {
sfPts: 'SF',
finalPts: 'Final',
total: 'Total',
effectiveMaxTooltip:
'Puntaje / tu máximo posible. Los partidos de eliminación cuyos equipos reales no incluyen tu pick guardado quedan fuera de tu máximo.',
effectiveMaxNote:
'El total muestra puntaje / máximo posible por usuario. Los partidos de eliminación cuyos equipos reales no incluyen tu pick guardado quedan fuera de tu máximo.',
you: 'Tú',
prizePool: 'Fondo de Premios',
participants: 'Participantes',
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/messages/it.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ export const IT = {
sfPts: 'Semi.',
finalPts: 'Finale',
total: 'Totale',
effectiveMaxTooltip:
'Punteggio / massimo possibile. Le partite a eliminazione le cui squadre reali non includono la tua scelta salvata sono escluse dal tuo massimo.',
effectiveMaxNote:
'Il totale mostra punteggio / massimo possibile per utente. Le partite a eliminazione le cui squadre reali non includono la tua scelta salvata sono escluse dal tuo massimo.',
you: 'Tu',
prizePool: 'Montepremi',
participants: 'Partecipanti',
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/messages/nl.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ export const NL = {
sfPts: 'Half',
finalPts: 'Finale',
total: 'Totaal',
effectiveMaxTooltip:
'Score / jouw maximaal mogelijke. Knock-outwedstrijden waarvan de echte teams jouw opgeslagen keuze niet bevatten, tellen niet mee voor jouw maximum.',
effectiveMaxNote:
'Totaal toont score / persoonlijk maximum. Knock-outwedstrijden waarvan de echte teams jouw opgeslagen keuze niet bevatten, tellen niet mee voor jouw maximum.',
you: 'Jij',
prizePool: 'Prijzenpot',
participants: 'Deelnemers',
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/messages/pt.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ export const PT = {
sfPts: 'Semi.',
finalPts: 'Final',
total: 'Total',
effectiveMaxTooltip:
'Pontuação / seu máximo possível. Os jogos de mata-mata cujas equipes reais não incluem o seu palpite guardado ficam fora do seu máximo.',
effectiveMaxNote:
'O total mostra pontuação / máximo possível por usuário. Os jogos de mata-mata cujas equipes reais não incluem o seu palpite guardado ficam fora do seu máximo.',
you: 'Você',
prizePool: 'Premiação',
participants: 'Participantes',
Expand Down
8 changes: 8 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -3004,6 +3004,14 @@ html.theme-light .nav-dropdown-settings,
overflow: hidden;
}

.leaderboard-table-footnote {
color: var(--text-muted);
font-size: 0.85rem;
line-height: 1.45;
margin: 0;
padding: 0.75rem 1.5rem 1.25rem;
}

.app-table {
width: 100%;
border-collapse: collapse;
Expand Down
26 changes: 26 additions & 0 deletions src/pages/Leaderboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@ export default function Leaderboard() {
? currentLeague.description || t('leaderboard.leagueDescription')
: tournamentName || t('home.noTournaments');

// The tournament-wide maximum a perfect bracket can score. We use it
// together with each player's `forfeitedKnockoutPoints` (knockout matches
// whose actual participants did not include the player's stored pick) to
// render a per-user effective max alongside the total.
const tournamentMaxPoints = tournament?.rules?.totalMaximumPoints || 0;
const someoneForfeited = players.some(
(player) => (player.forfeitedKnockoutPoints || 0) > 0
);

if (loading) {
return (
<PageShell className="leaderboard-page min-h-[50vh] place-items-center">
Expand Down Expand Up @@ -291,6 +300,10 @@ export default function Leaderboard() {
<tbody>
{players.map((player, index) => {
const isCurrentUser = user?.id === player.userId;
const forfeited = player.forfeitedKnockoutPoints || 0;
const effectiveMax = tournamentMaxPoints
? Math.max(0, tournamentMaxPoints - forfeited)
: 0;

return (
<tr
Expand Down Expand Up @@ -332,13 +345,26 @@ export default function Leaderboard() {
))}
<td className="text-center font-bold text-emerald-400 text-lg">
{formatNumber(player.totalScore || 0)}
{tournamentMaxPoints ? (
<span
className="text-slate-500 text-sm font-normal"
title={t('leaderboard.effectiveMaxTooltip')}
>
{` / ${formatNumber(effectiveMax)}`}
</span>
) : null}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{someoneForfeited ? (
<p className="leaderboard-table-footnote">
{t('leaderboard.effectiveMaxNote')}
</p>
) : null}
</Panel>
)}
</PageShell>
Expand Down
18 changes: 18 additions & 0 deletions src/pages/League.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,11 @@ export default function League() {
<tbody>
{players.map((player, index) => {
const isCurrentUser = user?.id === player.userId;
const forfeited = player.forfeitedKnockoutPoints || 0;
const tournamentMaxPoints = tournament?.rules?.totalMaximumPoints || 0;
const effectiveMax = tournamentMaxPoints
? Math.max(0, tournamentMaxPoints - forfeited)
: 0;

return (
<tr
Expand Down Expand Up @@ -565,13 +570,26 @@ export default function League() {
))}
<td className="text-center font-bold text-emerald-400 text-lg">
{formatNumber(player.totalScore || 0)}
{tournamentMaxPoints ? (
<span
className="text-slate-500 text-sm font-normal"
title={t('leaderboard.effectiveMaxTooltip')}
>
{` / ${formatNumber(effectiveMax)}`}
</span>
) : null}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{players.some((player) => (player.forfeitedKnockoutPoints || 0) > 0) ? (
<p className="leaderboard-table-footnote">
{t('leaderboard.effectiveMaxNote')}
</p>
) : null}
</Panel>
)}
</PageShell>
Expand Down
Loading