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
Original file line number Diff line number Diff line change
Expand Up @@ -174,22 +174,19 @@ function CappedRoundsIndicator({
)
}

/** Clickable icon for any score modification (penalty or direct adjust) */
/** Clickable icon shown only for minor/major penalties */
function PenaltyIndicator({
result,
}: {
result: CompetitionLeaderboardEntry["eventResults"][number]
}) {
if (!result.penaltyType && !result.isDirectlyModified) return null

const label = result.penaltyType
? `${result.penaltyType === "major" ? "Major" : "Minor"} Penalty`
: "Score Adjusted"
if (!result.penaltyType) return null

const label = `${result.penaltyType === "major" ? "Major" : "Minor"} Penalty`
const detail =
result.penaltyPercentage != null
? `${result.penaltyPercentage}% deduction applied`
: "This score was modified by an organizer."
: "A penalty was applied to this score."

return (
<Popover>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,20 +217,18 @@ function CappedRoundsIndicator({
)
}

/** Subtle warning icon indicating a penalty or score adjustment */
/** Subtle warning icon shown only for minor/major penalties */
function PenaltyIndicator({
result,
}: {
result: CompetitionLeaderboardEntry["eventResults"][number]
}) {
if (!result.penaltyType && !result.isDirectlyModified) return null
if (!result.penaltyType) return null

const label = result.penaltyType
? `${result.penaltyType === "major" ? "Major" : "Minor"} Penalty${result.penaltyPercentage != null ? ` (${result.penaltyPercentage}%)` : ""}`
: "Score Adjusted"
const label = `${result.penaltyType === "major" ? "Major" : "Minor"} Penalty${result.penaltyPercentage != null ? ` (${result.penaltyPercentage}%)` : ""}`

return (
<span title={label}>
<span title={label} aria-label={label}>
<AlertTriangle className="h-3 w-3 text-muted-foreground" />
</span>
)
Expand Down Expand Up @@ -304,7 +302,7 @@ function hasExpandableContent(
return entry.eventResults.some(
(r) =>
r.trackWorkoutId === selectedEventId &&
(r.videoUrl || r.penaltyType || r.isDirectlyModified),
(r.videoUrl || r.penaltyType),
)
}

Expand Down Expand Up @@ -412,11 +410,9 @@ function ExpandedVideoRow({
? entry.eventResults.filter(
(r) =>
r.trackWorkoutId === selectedEventId &&
(r.videoUrl || r.penaltyType || r.isDirectlyModified),
)
: entry.eventResults.filter(
(r) => r.videoUrl || r.penaltyType || r.isDirectlyModified,
(r.videoUrl || r.penaltyType),
)
: entry.eventResults.filter((r) => r.videoUrl || r.penaltyType)

if (resultsToShow.length === 0) return null

Expand All @@ -436,7 +432,7 @@ function ExpandedVideoRow({
</span>
)}

{/* Penalty / adjustment notices */}
{/* Penalty notice */}
{result.penaltyType && (
<div className="inline-flex items-center gap-1.5 rounded-md bg-amber-500/10 px-2.5 py-1 text-xs font-medium text-amber-600 dark:text-amber-400">
<AlertTriangle className="h-3 w-3" />
Expand All @@ -445,12 +441,6 @@ function ExpandedVideoRow({
` · ${result.penaltyPercentage}% deduction`}
</div>
)}
{!result.penaltyType && result.isDirectlyModified && (
<div className="inline-flex items-center gap-1.5 rounded-md bg-amber-500/10 px-2.5 py-1 text-xs font-medium text-amber-600 dark:text-amber-400">
<AlertTriangle className="h-3 w-3" />
Score adjusted by organizer
</div>
)}

{/* Video content */}
{result.videoUrl && result.videoSubmissionId && (
Expand Down Expand Up @@ -651,9 +641,7 @@ function MobileOnlineLeaderboardRow({
{entry.eventResults.some((r) => r.videoUrl) && (
<Video className="h-4 w-4 text-muted-foreground shrink-0" />
)}
{entry.eventResults.some(
(r) => r.penaltyType || r.isDirectlyModified,
) && (
{entry.eventResults.some((r) => r.penaltyType) && (
<AlertTriangle className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
)}

Expand Down Expand Up @@ -721,12 +709,6 @@ function MobileOnlineLeaderboardRow({
` · ${result.penaltyPercentage}% deduction`}
</span>
)}
{!result.penaltyType && result.isDirectlyModified && (
<span className="text-[10px] text-muted-foreground inline-flex items-center gap-1">
<AlertTriangle className="h-2.5 w-2.5" />
Score adjusted by organizer
</span>
)}
</div>
</SubmissionLinkWrapper>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ describe("LeaderboardPageContent", () => {
expect(indicators.length).toBeGreaterThan(0)
})

it("shows indicator for directly modified score", async () => {
it("does not show indicator for directly modified score without penalty", async () => {
const entries = [
createMockEntry({
eventResults: [
Expand Down Expand Up @@ -600,8 +600,8 @@ describe("LeaderboardPageContent", () => {
expect(screen.getAllByText("4:50").length).toBeGreaterThan(0)
})

const indicators = screen.getAllByLabelText("Score Adjusted")
expect(indicators.length).toBeGreaterThan(0)
expect(screen.queryByLabelText(/penalty/i)).not.toBeInTheDocument()
expect(screen.queryByLabelText(/adjusted/i)).not.toBeInTheDocument()
})

it("shows penalty details in popover on click", async () => {
Expand Down Expand Up @@ -654,7 +654,7 @@ describe("LeaderboardPageContent", () => {
})
})

it("shows organizer message for directly modified scores in popover", async () => {
it("does not show popover for directly modified scores without penalty", async () => {
const entries = [
createMockEntry({
eventResults: [
Expand Down Expand Up @@ -695,13 +695,10 @@ describe("LeaderboardPageContent", () => {
expect(screen.getAllByText("4:50").length).toBeGreaterThan(0)
})

const indicator = screen.getAllByLabelText("Score Adjusted")[0]
fireEvent.click(indicator)

await waitFor(() => {
expect(screen.getByText("Score Adjusted")).toBeInTheDocument()
expect(screen.getByText("This score was modified by an organizer.")).toBeInTheDocument()
})
expect(screen.queryByLabelText("Score Adjusted")).not.toBeInTheDocument()
expect(
screen.queryByText("This score was modified by an organizer."),
).not.toBeInTheDocument()
})
})

Expand Down
2 changes: 2 additions & 0 deletions lat.md/domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ The adjust inputs mirror the athlete submission form: `parseScore` validates ent

The Adjust Score form's penalty-tiebreak state is seeded from `submission.score.tiebreakValue` (the decoded display string) rather than the empty string, so editing other fields without touching the tiebreak input preserves the existing value. Without that seed, the server would receive `tieBreakScore: undefined`, fail the `if (data.tieBreakScore && score.tiebreakScheme)` truthiness check, and persist `tiebreakValue: null`, silently erasing a previously-entered tiebreak.

The public leaderboard only surfaces a warning icon — and the accompanying expanded notice — for scores carrying a Minor or Major penalty. Plain score adjustments (`isDirectlyModified` with `penaltyType: null`) are treated as data-entry corrections and intentionally render without any warning badge so athletes aren't alarmed by routine fixes. Both [[apps/wodsmith-start/src/components/competition-leaderboard-table.tsx#PenaltyIndicator]] and [[apps/wodsmith-start/src/components/online-competition-leaderboard-table.tsx#PenaltyIndicator]] gate on `penaltyType` alone, and the online table's expanded row / mobile collapsible suppress the "Score adjusted by organizer" line for the same reason.

### Manual Score Entry

When an athlete uploads a video without filling in the score field, the sidebar renders [[apps/wodsmith-start/src/components/compete/enter-score-form.tsx#EnterScoreForm]] instead of the placeholder, letting the reviewer create the missing score in one step.
Expand Down
Loading