diff --git a/src/apps/gigs/README.md b/src/apps/gigs/README.md
index fb79893cb..1b3933b39 100644
--- a/src/apps/gigs/README.md
+++ b/src/apps/gigs/README.md
@@ -22,10 +22,10 @@ cards follow the platform design system. The reference was the August 2026 Figma
file `C2cA6508RhpjWJDp7MLKbO`, Color page `1:54`, with design context retrieved
from `674:8828`. Layout retains the legacy listing/detail/form hierarchy while
adapting to the platform components. Styles apply only inside `.gigs-app`.
-The listing search uses the same teal focused border and ring as the other 2026
-opportunity filters instead of inheriting the legacy blue outline. Keyboard
-focus retains a real teal outline, with a system Highlight fallback in forced
-color modes.
+The listing search, location and sort controls use the same teal focused border
+and ring as the other 2026 opportunity filters instead of inheriting the browser's
+blue outline. Keyboard focus retains a real teal outline, with a system Highlight
+fallback in forced color modes.
The Gig Work resources callout opens its external community guide in a new tab
with the opener relationship removed.
@@ -37,9 +37,10 @@ lookup accepts both Recruit's current direct array and its legacy `{ data }`
envelope. Applications use the refreshed platform token and preserve the existing
multipart `form`/`resume` contract and Recruit custom field IDs 1, 2, 13 and 14. A saved
resume may be reused; otherwise PDF/DOCX up to **8,000,000 bytes** is required to
-match the server's multer limit. No success state appears without an explicit
-`success: true` response. HTTP errors and Recruit error envelopes returned with
-HTTP 200 both reject. Candidate searches return an existing profile from either
+match the server's multer limit. Recruit's populated assignment response and its
+idempotent `{ success: true }` response both confirm submission; empty, explicitly
+unsuccessful, HTTP-error and HTTP-200 error-envelope responses reject. Candidate
+searches return an existing profile from either
response shape. A bare `[]` or `{ data: [] }` means no existing candidate and
opens the application form with the member's Topcoder profile. Candidate lookup
failures still block prefill/submission and expose a retry.
@@ -48,6 +49,8 @@ Candidate Terms and the Equal Employment Opportunity Policy load on demand
from the existing Payload compatibility endpoint using the original modal IDs.
Descriptions and policy bodies are sanitized before display. Styling, scripts,
unsafe URLs and embedded form controls cannot affect the surrounding application.
+Policy dialogs size to their content, center their titles and provide both the
+standard dismiss icon and a visible Close action.
Search, country, sort and page are URL parameters. Updating filters preserves
unrelated parameters such as `ref`, and resets the result page. The selected
@@ -82,8 +85,9 @@ yarn test:no-watch --runInBand --watch=false --runTestsByPath \
src/apps/gigs/src/pages/GigApplyPage.spec.tsx
```
-The tests cover discovery rules, detail-route scroll restoration, salary fallbacks, required fields, consent and
-availability, upload limits, legacy payload mapping, HTTP-200 error envelopes,
+The tests cover discovery rules, detail-route scroll restoration, salary fallbacks, legacy validation copy,
+required fields, consent and availability, upload limits, legacy payload mapping, Recruit assignment responses,
+policy close actions, HTTP-200 error envelopes,
expired authentication, empty candidate search responses, candidate lookup retry,
prefill, submission retry and already-placed candidates.
Also verify the listing, detail and anonymous apply route against real Recruit
diff --git a/src/apps/gigs/src/components/GigApplicationForm.spec.tsx b/src/apps/gigs/src/components/GigApplicationForm.spec.tsx
index 18644f779..96ea43571 100644
--- a/src/apps/gigs/src/components/GigApplicationForm.spec.tsx
+++ b/src/apps/gigs/src/components/GigApplicationForm.spec.tsx
@@ -42,7 +42,13 @@ jest.mock(
{props.children}
),
- BaseModal: (props: any) => (props.open ?
-
+
+ Close
+
+ )}
+ classNames={{ modal: 'gigs-policy-modal' }}
+ open={!!props.id}
+ onClose={props.close}
+ title={props.title}
+ size='lg'
+ >
+
{error ? (
{
)
it('uses a refreshed token for multipart submission without a manual content-type boundary', async () => {
const body = new FormData()
- fetchMock.mockResolvedValue(response({ success: true }))
+ fetchMock.mockResolvedValue(response({
+ candidate_slug: 'candidate-slug',
+ id: 123,
+ job_slug: 'gig-slug',
+ }))
await applyToGig('gig-slug', body)
expect(fetchMock)
.toHaveBeenCalledWith(
@@ -101,12 +105,30 @@ describe('Recruit API integration', () => {
}),
)
})
+ it('also accepts Recruit\'s idempotent already-assigned success response', async () => {
+ fetchMock.mockResolvedValue(response({ success: true }))
+ await expect(applyToGig('gig-slug', new FormData()))
+ .resolves.toBeUndefined()
+ })
+ it.each([
+ { message: 'Assignment failed' },
+ { candidate_slug: 'candidate-slug' },
+ { candidate_slug: 'candidate-slug', job_slug: 'another-gig' },
+ ])('rejects a nonempty response that does not confirm the requested assignment: %j', async data => {
+ fetchMock.mockResolvedValue(response(data))
+ await expect(applyToGig('gig-slug', new FormData())).rejects.toThrow('not confirmed')
+ })
it('never treats an error, an empty result, or an expired session as a successful application', async () => {
fetchMock.mockResolvedValue(response({ error: true, errorObj: { notAllowed: true } }))
await expect(applyToGig('gig-slug', new FormData())).rejects.toThrow('already placed')
fetchMock.mockResolvedValue(response({}))
- await expect(applyToGig('gig-slug', new FormData())).rejects.toThrow('not confirmed');
- (tokenGetAsync as jest.Mock).mockResolvedValue({})
+ await expect(applyToGig('gig-slug', new FormData())).rejects.toThrow('not confirmed')
+ fetchMock.mockResolvedValue(response({ success: false }))
+ await expect(applyToGig('gig-slug', new FormData())).rejects.toThrow('not confirmed')
+ fetchMock.mockResolvedValue(response(['unexpected']))
+ await expect(applyToGig('gig-slug', new FormData())).rejects.toThrow('not confirmed')
+ const tokenGetMock = tokenGetAsync as jest.Mock
+ tokenGetMock.mockResolvedValue({})
fetchMock.mockClear()
await expect(applyToGig('gig-slug', new FormData())).rejects.toMatchObject({ status: 401 })
expect(fetchMock).not.toHaveBeenCalled()
diff --git a/src/apps/gigs/src/gigs.service.ts b/src/apps/gigs/src/gigs.service.ts
index f6d8b5424..60b69ec5c 100644
--- a/src/apps/gigs/src/gigs.service.ts
+++ b/src/apps/gigs/src/gigs.service.ts
@@ -5,6 +5,24 @@ import { Candidate, Gig } from './models'
const RECRUIT_URL = `${EnvironmentConfig.COMMUNITY_APP_URL}/api/recruit`
+/**
+ * Returns whether Recruit supplied an explicit success or an assignment for the requested Gig.
+ *
+ * @param result Recruit's parsed application response.
+ * @param slug The requested Gig slug, which must match a returned assignment resource.
+ * @returns Whether the response confirms the application.
+ */
+function isConfirmedApplication(result: unknown, slug: string): boolean {
+ if (!result || typeof result !== 'object' || Array.isArray(result)) return false
+ const response = result as Record
+ return response.success === true
+ || (
+ typeof response.candidate_slug === 'string'
+ && response.candidate_slug.trim().length > 0
+ && response.job_slug === slug
+ )
+}
+
/** An API failure with an HTTP-equivalent status, including Recruit errors returned with HTTP 200. */
export class RecruitError extends Error {
status: number
@@ -74,14 +92,19 @@ export async function getCandidate(email: string): Promise {
- const result = await recruitRequest<{ success?: boolean }>(
+ const result = await recruitRequest(
`${RECRUIT_URL}/jobs/${encodeURIComponent(slug)}/apply`,
true,
body,
)
- if (!result.success) throw new RecruitError('Your application was not confirmed. Please try again.', 502)
+ if (!isConfirmedApplication(result, slug)) {
+ throw new RecruitError('Your application was not confirmed. Please try again.', 502)
+ }
}
/** Loads an authored candidate policy from the Payload compatibility endpoint; returns its Markdown body. */
diff --git a/src/apps/gigs/src/gigs.utils.spec.ts b/src/apps/gigs/src/gigs.utils.spec.ts
index bf3495076..137e458c9 100644
--- a/src/apps/gigs/src/gigs.utils.spec.ts
+++ b/src/apps/gigs/src/gigs.utils.spec.ts
@@ -130,6 +130,20 @@ describe('Gigs discovery and application contracts', () => {
'The maximum file size is 8 MB.',
)
})
+ it('matches the legacy required, minimum and maximum copy for phone and city', () => {
+ expect(validateApplication({ ...valid, city: '', phone: '' }))
+ .toEqual(expect.objectContaining({ city: 'Required field', phone: 'Required field' }))
+ expect(validateApplication({ ...valid, city: 'H', phone: '1' }))
+ .toEqual(expect.objectContaining({
+ city: 'Must be at least 2 characters',
+ phone: 'Must be at least 2 characters',
+ }))
+ expect(validateApplication({ ...valid, city: 'c'.repeat(51), phone: '1'.repeat(51) }))
+ .toEqual(expect.objectContaining({
+ city: 'Must be max 50 characters',
+ phone: 'Must be max 50 characters',
+ }))
+ })
it('reuses a saved resume but requires one if the existing candidate has no resume', () => {
expect(
validateApplication(
diff --git a/src/apps/gigs/src/gigs.utils.ts b/src/apps/gigs/src/gigs.utils.ts
index fbc2b07d9..9f501c7b0 100644
--- a/src/apps/gigs/src/gigs.utils.ts
+++ b/src/apps/gigs/src/gigs.utils.ts
@@ -74,17 +74,24 @@ export function filterGigs(jobs: Gig[], search: string, location: string, sort:
})
}
-/** Validates application values against the legacy contract and the server's 8,000,000-byte upload limit. */
+/**
+ * Validates application values against the legacy contract, including its field-specific validation copy,
+ * and the server's 8,000,000-byte upload limit.
+ */
export function validateApplication(values: ApplicationValues, candidate?: Candidate): ApplicationErrors {
const errors: ApplicationErrors = {}
const fields = ['firstName', 'lastName', 'email', 'city', 'phone'] as const
fields.forEach(field => {
const value = values[field].trim()
const max = ['city', 'phone'].includes(field) ? 50 : 40
- if (value.length < 2) errors[field] = 'Enter at least 2 characters.'
- else if (value.length > max) errors[field] = `Enter no more than ${max} characters.`
+ if (!value) errors[field] = 'Required field'
+ else if (value.length < 2) errors[field] = 'Must be at least 2 characters'
+ else if (value.length > max) errors[field] = `Must be max ${max} characters`
})
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email.trim())) errors.email = 'Enter a valid email address.'
+ if (values.email.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email.trim())) {
+ errors.email = 'Enter a valid email address.'
+ }
+
if (!values.country) errors.country = 'Select your country.'
if (!/^\d+$/.test(values.pay.trim())) {
errors.pay = 'Enter your weekly pay expectation as a whole dollar amount.'
diff --git a/src/apps/gigs/src/pages/GigsPage.spec.tsx b/src/apps/gigs/src/pages/GigsPage.spec.tsx
index 101c438ed..3b31842b1 100644
--- a/src/apps/gigs/src/pages/GigsPage.spec.tsx
+++ b/src/apps/gigs/src/pages/GigsPage.spec.tsx
@@ -81,7 +81,7 @@ describe('GigsPage listing presentation', () => {
mockUseSWR.mockReturnValue({ data: [], error: undefined, mutate: jest.fn() })
})
- it('uses the scoped 2026 focus treatment on the gig search field', () => {
+ it('uses the scoped 2026 focus treatment on every Gig listing filter control', () => {
render(
@@ -89,7 +89,11 @@ describe('GigsPage listing presentation', () => {
)
expect(screen.getByRole('searchbox', { name: 'Search' }))
- .toHaveClass('gigs-filter-input')
+ .toHaveClass('gigs-filter-control')
+ expect(screen.getByRole('combobox', { name: 'Location' }))
+ .toHaveClass('gigs-filter-control')
+ expect(screen.getByRole('combobox', { name: 'Sort by' }))
+ .toHaveClass('gigs-filter-control')
})
it('opens the Gig Work resources in a separate tab without an opener', () => {
diff --git a/src/apps/gigs/src/pages/GigsPage.tsx b/src/apps/gigs/src/pages/GigsPage.tsx
index 3c08cd506..19583069b 100644
--- a/src/apps/gigs/src/pages/GigsPage.tsx
+++ b/src/apps/gigs/src/pages/GigsPage.tsx
@@ -75,7 +75,7 @@ const GigsPage: FC = () => {
Find a gig
Search
{
/>
Location
updateFilter('location', event.target.value)}
@@ -119,6 +120,7 @@ const GigsPage: FC = () => {
Sort by
updateFilter('sort', event.target.value)}
diff --git a/src/apps/gigs/src/styles/index.scss b/src/apps/gigs/src/styles/index.scss
index 565002e5c..0ebc146c0 100644
--- a/src/apps/gigs/src/styles/index.scss
+++ b/src/apps/gigs/src/styles/index.scss
@@ -24,8 +24,8 @@
.gigs-filters h2 { margin: 0; }
.gigs-filters label { font-weight: 700; margin-top: 8px; }
input:not([type='checkbox'], [type='radio']), select { width: 100%; min-height: 44px; padding: 10px 12px; background: $tc-2026-surface; color: $tc-2026-body; border: 1px solid $tc-2026-border-strong; font-size: 14px; }
- .gigs-filter-input:focus { border-color: $tc-2026-teal; box-shadow: 0 0 0 1px $tc-2026-teal; }
- .gigs-filter-input:focus-visible { outline: 2px solid $tc-2026-teal; outline-offset: 2px; }
+ .gigs-filter-control:focus { border-color: $tc-2026-teal; box-shadow: 0 0 0 1px $tc-2026-teal; }
+ .gigs-filter-control:focus-visible { outline: 2px solid $tc-2026-teal; outline-offset: 2px; }
input[readonly] { background: $tc-2026-canvas; }
.gigs-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 16px; }
.gigs-toolbar h2 { margin: 0; font-size: 18px; }
@@ -102,6 +102,8 @@
.gigs-select__control--is-focused { border-color: $tc-2026-teal; box-shadow: 0 0 0 1px $tc-2026-teal; }
.gigs-select__multi-value { background: $tc-2026-canvas; }
.gigs-select__option--is-focused { background: $tc-2026-canvas; }
+ &.gigs-policy { min-height: 0; padding-bottom: 0; }
+ &.gigs-policy .gigs-content { margin-bottom: 0; }
@media (max-width: 960px) {
.gigs-container { width: calc(100% - 40px); }
@@ -125,7 +127,26 @@
}
@media (forced-colors: active) {
- .gigs-filter-input:focus { border-color: Highlight; box-shadow: none; }
- .gigs-filter-input:focus-visible { outline-color: Highlight; }
+ .gigs-filter-control:focus { border-color: Highlight; box-shadow: none; }
+ .gigs-filter-control:focus-visible { outline-color: Highlight; }
}
}
+
+.gigs-policy-modal h3 {
+ padding: 0 48px;
+ text-align: center;
+}
+
+.gigs-policy-modal-body {
+ flex: 0 1 auto;
+}
+
+.gigs-policy-actions {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+}
+
+@media (max-width: 464px) {
+ .react-responsive-modal-modal.gigs-policy-modal { height: auto; }
+}
diff --git a/src/apps/opportunities/README.md b/src/apps/opportunities/README.md
index 475126aff..6ff136252 100644
--- a/src/apps/opportunities/README.md
+++ b/src/apps/opportunities/README.md
@@ -53,6 +53,16 @@ against challenge names, authored tags, and standardized skills before its
server-side pagination, so selecting a chip returns every matching review
opportunity rather than filtering only the currently loaded page.
+Copilot card skills retain that same shareable `search` route and visible
+sidebar value. While the deployed Projects API rejects its JSON-backed
+`search` and `skills` queries with an HTTP 500, the client uses the existing
+bounded compatibility loader and filters the complete supported result window
+locally. A safe owner-side `projectName` query is unioned with those public rows
+because public list payloads omit project names; status and canonical
+opportunity-type facets remain intact across both result sets. This avoids
+issuing the known-broken filtered request and keeps skill and project selection
+functional until that owner query is repaired.
+
## List and grid views
Every domain toolbar exposes the same accessible List/Grid selector from the
@@ -104,13 +114,12 @@ than presented as free work.
Approved applications, rather than pending applications, consume reviewer
capacity. When `remainingPositions` reaches zero, eligible reviewers can still
-use the detail CTA to join the waitlist; the page explains that outcome before
-submission and confirms it afterward. Review API persists these applications as
-`PENDING`. Browse and My Work cards render that caller state as `Waitlisted`
-with the Figma clock icon and a green outline in both list and grid views.
-That state remains while capacity is full, then naturally returns to `Applied`
-if a position reopens or to `Approved` when the reviewer is selected. An explicit
-`WAITLISTED` application status uses the same badge.
+use the “Apply to be a reviewer (waitlist)” detail CTA; the page explains that
+outcome before submission and confirms that Support will contact the applicant
+if another reviewer cannot complete the review. Review API persists these
+applications as `PENDING`. Browse and My Work cards render that caller state as `Waitlisted`
+while capacity remains full, then naturally return to `Applied` if a position
+reopens or to `Approved` when the reviewer is selected.
Long card titles expose their complete value in the authored dark tooltip.
When a card has more skills than fit in its visible skill row, its `+n` control
@@ -121,11 +130,12 @@ Scientist, and Data Engineer enum values.
## Competition card contract
-Competition list cards consume the Challenge API v6 list response directly;
-they do not make per-card follow-up requests. Track catalog values drive the
-Figma Design, Development, Data Science, AI, and QA pill palettes. Challenge,
-First2Finish, Marathon Match, and Task catalog values map to their authored
-subtype icons and member-facing labels.
+Competition list cards consume the Challenge API v6 list response directly.
+Completed pages make one batched Members API projection request for the winner
+IDs in that page; they do not make per-card follow-up requests. Track catalog
+values drive the Figma Design, Development, Data Science, AI, and QA pill
+palettes. Challenge, First2Finish, Marathon Match, and Task catalog values map
+to their authored subtype icons and member-facing labels.
- “Open for registration” requires an `ACTIVE` challenge and an open
`Registration` phase (or legacy combined `Open` phase). `ACTIVE` by itself
@@ -142,6 +152,11 @@ subtype icons and member-facing labels.
the authored yellow, light-blue, and peach placement assets at their native
14×18px size; the dark second- and third-place podium variants are reserved
for the Winners presentation.
+- Completed cards replace registration and stale phase-progress states with the
+ explicit Completed state. Up to three actual winner photos appear beside the
+ placement prizes with the existing podium medals; missing or failed photos
+ retain a handle-initial fallback. The complete avatar-and-medal affordance
+ opens that challenge's Winners tab.
- `currentPhase` is preferred for the phase chip. Older responses fall back to
the latest-started open phase. Progress uses actual then scheduled dates,
clamps to 0–100%, and may derive the end from the phase duration in seconds.
@@ -170,8 +185,9 @@ At phone widths, the timezone moves above a vertical timeline: phase nodes and
progress connectors occupy the left rail while each phase name and its dates
remain in an aligned, content-sized row to the right. Each mobile row owns its
marker and connector, so wrapped dates and enlarged text grow the rail instead
-of overlapping the following milestone. Wider layouts retain the horizontal
-timeline and its overflow fallback for tablet-sized screens.
+of overlapping the following milestone. The mobile prize/action card follows
+the expanded timeline instead of interrupting it. Wider layouts retain the
+horizontal timeline and its overflow fallback for tablet-sized screens.
On phone viewports, Registrants preserves its semantic table while presenting
each API row as the Figma key/value card. Registration Date remains a
@@ -409,15 +425,21 @@ owns the Review App handoff.
The Marathon Match My Submissions table reserves enough width for the complete
submission timestamp and keeps its date heading and sort icon on one line,
aligned with the dates beneath it. Score columns remain right aligned.
-Submission history replaces the unreliable status field with Final Score and
-uses a responsive table that scrolls only on narrow viewports. Design
-submissions can be deleted only while Submission or Checkpoint Submission is
-open. Successful deletion updates both the challenge and member submission
-counts as well as the current list. Replacing a Design submission without
-reloading therefore preserves accurate totals, and deleting the member's last
-submission clears the submission-based Unregister restriction. Failed or
-cancelled deletions leave the counts unchanged; Review API remains authoritative
-for submission limits.
+Submission history replaces the unreliable status field with Final Score. At
+phone widths, each attempt becomes a compact stacked label/value card in the
+legacy Submission, Final Score, Provisional Score, and Time order, avoiding
+horizontal clipping. The dialog also exposes the latest-submission summary and
+compact close action only at that breakpoint. History requests include the
+selected member ID;
+Review API returns every attempt to that member and authorized challenge staff,
+while ordinary viewers receive only the selected entrant's latest attempt.
+Design submissions can be deleted only while Submission or Checkpoint
+Submission is open. Successful deletion updates both the challenge and member
+submission counts as well as the current list. Replacing a Design submission
+without reloading therefore preserves accurate totals, and deleting the
+member's last submission clears the submission-based Unregister restriction.
+Failed or cancelled deletions leave the counts unchanged; Review API remains
+authoritative for submission limits.
Challenge Discussion reads and writes use the authenticated
`/v6/forums` API. Topic creation, comments and nested replies, owner edits,
diff --git a/src/apps/opportunities/src/components/ChallengeDetailHeader.module.scss b/src/apps/opportunities/src/components/ChallengeDetailHeader.module.scss
index 199891307..4fcf7d66d 100644
--- a/src/apps/opportunities/src/components/ChallengeDetailHeader.module.scss
+++ b/src/apps/opportunities/src/components/ChallengeDetailHeader.module.scss
@@ -292,14 +292,16 @@
display: flex;
flex-direction: column;
gap: 4px;
+}
- small {
- align-self: flex-start;
- color: rgba(255, 255, 255, .6);
- font-size: 12px;
- font-weight: 700;
- line-height: 16px;
- }
+.prizeTitle {
+ align-self: center;
+ color: rgba(255, 255, 255, .6);
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 16px;
+ text-align: center;
+ width: 100%;
}
.prizes {
@@ -417,10 +419,12 @@
display: flex;
flex-direction: column;
gap: 16px;
- margin: 24px auto 0;
- max-width: 1200px;
+ grid-column: 1 / -1;
+ margin: 0;
+ max-width: none;
padding-top: 24px;
position: relative;
+ width: 100%;
z-index: 1;
}
@@ -612,6 +616,14 @@
line-height: 40px;
}
+ .expandedTimeline {
+ order: 2;
+ }
+
+ .actionCard {
+ order: 3;
+ }
+
.timeline {
align-items: flex-start;
flex-direction: column;
diff --git a/src/apps/opportunities/src/components/ChallengeDetailHeader.spec.tsx b/src/apps/opportunities/src/components/ChallengeDetailHeader.spec.tsx
index 8d2131497..f8b20fcaf 100644
--- a/src/apps/opportunities/src/components/ChallengeDetailHeader.spec.tsx
+++ b/src/apps/opportunities/src/components/ChallengeDetailHeader.spec.tsx
@@ -93,6 +93,27 @@ describe('ChallengeDetailHeader actions and presentation', () => {
.not.toBeInTheDocument()
})
+ it.each(['Design', 'Development', 'Data Science', 'Quality Assurance', 'AI'])(
+ 'uses the centered Prizes title treatment for the %s track',
+ track => {
+ render(
+
+
+ ,
+ )
+
+ expect(screen.getByText('Prizes'))
+ .toHaveClass('prizeTitle')
+ },
+ )
+
it('shows only Register for an unregistered open challenge', () => {
render(
@@ -453,6 +474,12 @@ describe('ChallengeDetailHeader actions and presentation', () => {
fireEvent.click(screen.getByRole('button', { name: 'Show full timeline' }))
const timeline = screen.getByRole('region', { name: 'Challenge timeline' })
+ const prizeCard = screen.getByText('Prizes')
+ .closest('aside')
+ expect(prizeCard)
+ .toHaveClass('actionCard')
+ expect(timeline.parentElement)
+ .toBe(prizeCard?.parentElement)
const items = within(timeline)
.getAllByRole('listitem')
expect(items)
diff --git a/src/apps/opportunities/src/components/ChallengeDetailHeader.tsx b/src/apps/opportunities/src/components/ChallengeDetailHeader.tsx
index a5862cba6..5b405b48b 100644
--- a/src/apps/opportunities/src/components/ChallengeDetailHeader.tsx
+++ b/src/apps/opportunities/src/components/ChallengeDetailHeader.tsx
@@ -570,7 +570,7 @@ export const ChallengeDetailHeader: FC = props => {