Skip to content

Commit 3628fa2

Browse files
feat(awards): Explain award categories (#156)
* feat(awards): Explain award categories Co-Authored-By: Billy Vong <billy@sentry.io> * fix(awards): Use official category descriptions Co-Authored-By: Billy Vong <billy@sentry.io> * fix(awards): Keep category descriptions in checkbox names Remove the checkbox aria-label override so screen readers announce the visible award description from the wrapping label. * fix(awards): Stabilize category description matching Use locale-independent lowercasing and keep unknown-category fallback copy neutral across voting, ballot, and award-roll surfaces. --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Billy Vong <billy@sentry.io>
1 parent 50b7d6f commit 3628fa2

7 files changed

Lines changed: 113 additions & 9 deletions

File tree

src/app/awardCategories.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const CATEGORY_GUIDANCE = [
2+
{
3+
matches: ['best use of ai'],
4+
description: 'Clever and innovative twists with AI.',
5+
},
6+
{
7+
matches: ['for the devs'],
8+
description: 'Tools, workflows, or features that make developer life better.',
9+
},
10+
{
11+
matches: ['just ship it already'],
12+
description: 'The project so useful or obvious you wonder why it isn’t live yet.',
13+
},
14+
{
15+
matches: ['sentry af'],
16+
description: 'The one that feels the most “us”: quirky, sharp, unmistakably Sentry.',
17+
},
18+
{
19+
matches: ['the quiet win'],
20+
description: 'Small but mighty improvements that make a big difference.',
21+
},
22+
] as const;
23+
24+
const DEFAULT_GUIDANCE =
25+
'A focused award category for projects that best fit this theme.';
26+
27+
export function getAwardCategoryDescription(name: string) {
28+
const normalizedName = name.trim().toLowerCase();
29+
return (
30+
CATEGORY_GUIDANCE.find(({matches}) =>
31+
matches.some((match) => normalizedName.includes(match)),
32+
)?.description ?? DEFAULT_GUIDANCE
33+
);
34+
}

src/app/components/ProjectForm.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {useEffect, useId, useState, type FormEvent, type KeyboardEvent} from 'react';
22

33
import type {ProjectDetail, ProjectWriteRequest} from '../../shared/projects';
4+
import {getAwardCategoryDescription} from '../awardCategories';
45
import {useProjectOptions} from '../queries/projects';
56

67
export function ProjectForm({
@@ -437,7 +438,10 @@ export function ProjectForm({
437438
disabled={nominationsLocked || (atLimit && !selected)}
438439
onChange={() => toggleNomination(category.id)}
439440
/>
440-
<span>{category.name}</span>
441+
<span className="awardCategoryChoiceCopy">
442+
<strong>{category.name}</strong>
443+
<small>{getAwardCategoryDescription(category.name)}</small>
444+
</span>
441445
{atLimit && !selected && <small>two selected</small>}
442446
</label>
443447
);

src/app/components/ProjectVoting.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
BallotSelection,
66
BallotStatusResponse,
77
} from '../../shared/administration';
8+
import {getAwardCategoryDescription} from '../awardCategories';
89
import {useVoteMutation} from '../queries/administration';
910

1011
export function ProjectVoting({
@@ -113,6 +114,9 @@ export function ProjectVoting({
113114
</span>
114115
<div className="projectVotingCategoryCopy">
115116
<h3>{category.name}</h3>
117+
<p className="awardCategoryDescription">
118+
{getAwardCategoryDescription(category.name)}
119+
</p>
116120
{!entered ? (
117121
selectedHere ? (
118122
<p>

src/app/routes/ProjectsPage.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {useEffect, useRef, useState} from 'react';
22
import {Link, useParams} from 'wouter';
33

44
import type {BallotStatusResponse} from '../../shared/administration';
5+
import {getAwardCategoryDescription} from '../awardCategories';
56
import {GroupManager} from '../components/GroupManager';
67
import {ProjectCard} from '../components/ProjectCard';
78
import {PageState, QueryState} from '../components/AppLayout';
@@ -254,6 +255,7 @@ export function ProjectsPage({isAdmin = false}: {isAdmin?: boolean}) {
254255
href={`/years/${yearId}/projects/${award.projectId}`}
255256
>
256257
<span>{award.categoryName}</span>
258+
<small>{getAwardCategoryDescription(award.categoryName)}</small>
257259
<strong>{award.projectName}</strong>
258260
</Link>
259261
))}
@@ -446,7 +448,10 @@ function BallotOverview({
446448
<ul>
447449
{selections.map(({category, vote}) => (
448450
<li key={category.id}>
449-
<span>{category.name}</span>
451+
<span className="ballotCategoryCopy">
452+
<strong>{category.name}</strong>
453+
<small>{getAwardCategoryDescription(category.name)}</small>
454+
</span>
450455
{vote.projectActive && vote.nominationEligible ? (
451456
<Link href={`/years/${yearId}/projects/${vote.projectId}`}>
452457
{vote.projectName} <span aria-hidden="true"></span>

src/app/styles.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,17 @@ main {
690690
color: rgba(255, 255, 255, 0.62);
691691
font-size: 0.75rem;
692692
}
693+
.ballotCategoryCopy {
694+
display: grid;
695+
gap: 0.18rem;
696+
}
697+
.ballotCategoryCopy strong {
698+
color: #fff;
699+
font-size: 0.78rem;
700+
}
701+
.ballotCategoryCopy small {
702+
line-height: 1.35;
703+
}
693704
.ballotSelections a {
694705
min-width: 0;
695706
overflow: hidden;
@@ -1127,6 +1138,11 @@ main {
11271138
font-size: 0.72rem;
11281139
font-weight: 600;
11291140
}
1141+
.awardStrip small {
1142+
color: var(--muted);
1143+
font-size: 0.68rem;
1144+
line-height: 1.4;
1145+
}
11301146

11311147
.groupManager {
11321148
padding: 1.5rem;
@@ -1400,6 +1416,10 @@ main {
14001416
font-size: 0.78rem;
14011417
line-height: 1.45;
14021418
}
1419+
.projectVotingCategoryCopy .awardCategoryDescription {
1420+
max-width: 46rem;
1421+
margin-bottom: 0.55rem;
1422+
}
14031423
.projectVotingCategoryCopy p strong {
14041424
color: var(--ink);
14051425
}
@@ -2067,6 +2087,16 @@ main {
20672087
color: var(--ink);
20682088
font-size: 0.78rem;
20692089
}
2090+
.projectForm .awardCategoryChoiceCopy {
2091+
display: grid;
2092+
gap: 0.2rem;
2093+
}
2094+
.projectForm .awardCategoryChoiceCopy strong {
2095+
font-size: 0.82rem;
2096+
}
2097+
.projectForm .awardCategoryChoiceCopy small {
2098+
line-height: 1.4;
2099+
}
20702100
.awardCategoryChoice--limited > span {
20712101
color: var(--muted);
20722102
}

test/app/ProjectForm.test.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ describe('ProjectForm award targeting', () => {
159159
await userEvent.click(
160160
await screen.findByRole('radio', {name: /Focus on specific awards/}),
161161
);
162-
const craft = screen.getByRole('checkbox', {name: 'Craft prize'});
163-
const impact = screen.getByRole('checkbox', {name: 'Biggest impact'});
162+
const craft = screen.getByRole('checkbox', {name: /Craft prize/});
163+
const impact = screen.getByRole('checkbox', {name: /Biggest impact/});
164164
const moonshot = screen.getByRole('checkbox', {name: /Moonshot/});
165165
await userEvent.click(craft);
166166
await userEvent.click(impact);
@@ -201,11 +201,11 @@ describe('ProjectForm award targeting', () => {
201201
expect(
202202
await screen.findByRole('radio', {name: /Focus on specific awards/}),
203203
).toHaveProperty('checked', true);
204-
expect(screen.getByRole('checkbox', {name: 'Biggest impact'})).toHaveProperty(
204+
expect(screen.getByRole('checkbox', {name: /Biggest impact/})).toHaveProperty(
205205
'checked',
206206
true,
207207
);
208-
expect(screen.getByRole('checkbox', {name: 'Craft prize'})).toHaveProperty(
208+
expect(screen.getByRole('checkbox', {name: /Craft prize/})).toHaveProperty(
209209
'checked',
210210
true,
211211
);
@@ -244,7 +244,7 @@ describe('ProjectForm award targeting', () => {
244244
await userEvent.click(
245245
await screen.findByRole('radio', {name: /Focus on specific awards/}),
246246
);
247-
await userEvent.click(screen.getByRole('checkbox', {name: 'Moonshot'}));
247+
await userEvent.click(screen.getByRole('checkbox', {name: /Moonshot/}));
248248
await userEvent.click(screen.getByRole('button', {name: 'Never mind'}));
249249

250250
expect(confirmMock).toHaveBeenCalledTimes(1);
@@ -265,7 +265,7 @@ describe('ProjectForm award targeting', () => {
265265
'disabled',
266266
true,
267267
);
268-
expect(screen.getByRole('checkbox', {name: 'Moonshot'})).toHaveProperty(
268+
expect(screen.getByRole('checkbox', {name: /Moonshot/})).toHaveProperty(
269269
'disabled',
270270
true,
271271
);
@@ -298,7 +298,7 @@ describe('ProjectForm award targeting', () => {
298298

299299
focusedMode.focus();
300300
await userEvent.keyboard(' ');
301-
const moonshot = screen.getByRole('checkbox', {name: 'Moonshot'});
301+
const moonshot = screen.getByRole('checkbox', {name: /Moonshot/});
302302
moonshot.focus();
303303
await userEvent.keyboard(' ');
304304

test/app/awardCategories.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {describe, expect, it} from 'vitest';
2+
3+
import {getAwardCategoryDescription} from '../../src/app/awardCategories';
4+
5+
describe('getAwardCategoryDescription', () => {
6+
it.each([
7+
['Best use of AI', 'Clever and innovative twists with AI.'],
8+
['For the devs!', 'Tools, workflows, or features that make developer life better.'],
9+
[
10+
'Just ship it already',
11+
'The project so useful or obvious you wonder why it isn’t live yet.',
12+
],
13+
[
14+
'Sentry AF',
15+
'The one that feels the most “us”: quirky, sharp, unmistakably Sentry.',
16+
],
17+
['The quiet win', 'Small but mighty improvements that make a big difference.'],
18+
])('describes %s with category-specific guidance', (name, expected) => {
19+
expect(getAwardCategoryDescription(name)).toBe(expected);
20+
});
21+
22+
it('gives new categories useful fallback guidance', () => {
23+
expect(getAwardCategoryDescription('Wildcard')).toBe(
24+
'A focused award category for projects that best fit this theme.',
25+
);
26+
});
27+
});

0 commit comments

Comments
 (0)