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
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@ workflows:
only:
- dev
- copilot_reviewer
- support-app
- PM-5460
- opportunities-v6
tags:
only: /^dev-.*/

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
"browser-cookies": "^1.2.0",
"city-timezones": "^1.3.2",
"classnames": "^2.5.1",
"contentful": "^9.3.7",
"country-calling-code": "0.0.3",
"crypto-js": "^4.2.0",
"customize-cra": "^1.0.0",
Expand All @@ -64,10 +63,12 @@
"express-interceptor": "^1.2.0",
"fflate": "^0.8.2",
"filestack-js": "^3.44.2",
"flag-icons": "^6.7.0",
"highcharts": "^10.3.3",
"highcharts-react-official": "^3.2.3",
"highlight.js": "^11.11.1",
"html2canvas": "^1.4.1",
"i18n-iso-countries": "^3.7.1",
"lodash": "^4.18.1",
"markdown-it": "^14.3.0",
"marked": "4.3.0",
Expand Down Expand Up @@ -111,6 +112,7 @@
"redux-promise-middleware": "^6.2.0",
"redux-thunk": "^2.4.2",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^5.0.1",
"rehype-stringify": "^10.0.1",
"remark-breaks": "^3.0.3",
"remark-frontmatter": "^4.0.1",
Expand All @@ -126,9 +128,7 @@
"typescript": "^4.9.5",
"universal-navigation": "https://github.com/topcoder-platform/universal-navigation#master",
"uuid": "^11.1.0",
"yup": "^1.7.1",
"flag-icons": "^6.7.0",
"i18n-iso-countries": "^3.7.1"
"yup": "^1.7.1"
},
"devDependencies": {
"@babel/core": "^7.29.6",
Expand Down
1 change: 1 addition & 0 deletions src/apps/admin/src/lib/styles/index.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import './includes';
@import '@libs/ui/styles/2026/index';

body.admin-app {
color: $body-color;
Expand Down
1 change: 1 addition & 0 deletions src/apps/calendar/src/lib/styles/index.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import '@libs/ui/styles/includes';
@import '@libs/ui/styles/2026/index';

:root {
--LeaveColor: #4caf50;
Expand Down
8 changes: 7 additions & 1 deletion src/apps/copilots/src/CopilotsApp.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { FC, useContext } from 'react'
import { FC, useContext, useEffect } from 'react'
import { Outlet, Routes } from 'react-router-dom'

import { routerContext, RouterContextData } from '~/libs/core'
import { SharedSwrConfig } from '~/libs/shared'

import { toolTitle } from './copilots.routes'
import './styles/index.scss'

const CopilotsApp: FC<{}> = () => {
const { getChildRoutes }: RouterContextData = useContext(routerContext)

useEffect(() => {
document.body.classList.add('copilots-app')
return () => document.body.classList.remove('copilots-app')
}, [])

return (
<SharedSwrConfig>
<Outlet />
Expand Down
1 change: 1 addition & 0 deletions src/apps/copilots/src/styles/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import '@libs/ui/styles/2026/index';
8 changes: 7 additions & 1 deletion src/apps/engagements/src/EngagementsApp.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import type { FC } from 'react'
import { useContext } from 'react'
import { useContext, useEffect } from 'react'
import { Outlet, Routes } from 'react-router-dom'

import type { RouterContextData } from '~/libs/core'
import { routerContext } from '~/libs/core'

import { toolTitle } from './engagements.routes'
import { EngagementsSwr } from './lib'
import './styles/index.scss'

const EngagementsApp: FC<{}> = () => {
const { getChildRoutes }: RouterContextData = useContext(routerContext)

useEffect(() => {
document.body.classList.add('engagements-app')
return () => document.body.classList.remove('engagements-app')
}, [])

return (
<EngagementsSwr>
<Outlet />
Expand Down
1 change: 1 addition & 0 deletions src/apps/engagements/src/styles/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import '@libs/ui/styles/2026/index';
134 changes: 134 additions & 0 deletions src/apps/opportunities/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Opportunities

The Opportunities app replaces the legacy community-app challenge discovery,
challenge detail, and reviewer-opportunity detail experiences. The main route
is `/opportunities`; domain tabs use `/opportunities/:kind`, challenge details
use `/opportunities/challenge/:challengeId`, and review details use
`/opportunities/review/:reviewOpportunityId`.

The four headline metrics come from one `GET /v6/opportunities/summary`
request. List content is requested lazily from its owning API as members switch
tabs, filter, sort, or paginate. Do not prefetch bucket-sized list payloads.

The Competitions sidebar follows the authored Figma filter with one Search
control and the helper text “Search skills, technologies, projects.” Its value
is sent through the Challenge API `search` parameter; Competitions does not
render a second skills/technologies field. Other opportunity domains retain
their owner-specific skill facet where supported.

## List and grid views

Every domain toolbar exposes the same accessible List/Grid selector from the
Figma. List remains the default. The selected presentation is held above the
keyed domain listing, so it remains stable while a member moves among
Competitions, Engagements, Copilot Opportunities, and Review Opportunities.
Changing the presentation is client-only: it reuses the current owner API page
and does not issue another list request or create per-card requests.

At the authored 1200px desktop content width, Grid uses two 439px cards with a
16px gutter inside the 894px results column. Cards retain each owner's content:
competition prizes and phase progress stack above a horizontal metric footer;
engagement, copilot, and review metrics move to a vertical footer below the
card content. The responsive grid uses the same cards and automatically drops
to one column when two authored-width cards no longer fit. Both selector
buttons remain keyboard accessible and expose their active state with
`aria-pressed`.

## 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.

- “Open for registration” requires an `ACTIVE` challenge and an open
`Registration` phase (or legacy combined `Open` phase). `ACTIVE` by itself
is not treated as an open registration window. The server-filtered “My
competitions” result marks those cards Registered without per-card calls.
- The prize footer uses only the `PLACEMENT` prize set and preserves its API
order as first, second, and third place. Checkpoint, copilot, and reviewer
payments are not mixed into competitor prizes.
- `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.
Competition pages revalidate once a minute and when focus returns; cards
with no open phase omit the phase display instead of inventing one.
- The right rail shows submissions and registrants from Challenge API. It also
reserves the Figma Posts row; until Challenge API publishes `numOfPosts`, the
value is an em dash rather than a fabricated discussion or forum count.

## Challenge Markdown table of contents

Challenge descriptions are safe Markdown. Authors create the generated table
of contents with level-two and level-three ATX headings:

```markdown
## Challenge Summary

Summary content.

### Required Deliverables

Deliverable content.
```

- `##` creates a top-level table-of-contents entry.
- `###` creates a nested entry.
- `#` is reserved for the page title and is not included.
- Duplicate headings are supported; stable source-line suffixes keep their
fragment links unique.
- GFM tables and hard line breaks are supported. Raw HTML is not rendered.

Challenges declaring `descriptionFormat: "html"` use the legacy HTML path;
the HTML is DOMPurify-sanitized before rendering and does not produce a
Markdown table of contents. When Challenge API returns `privateDescription`,
the page renders it under “Registered User Additional Information” using the
same declared format. Challenge API remains authoritative for whether that
field is present for the caller.

## Owning API contracts

- Competitions: Challenge API, including `currentPhase`,
`currentPhaseNames`, phase schedules, `PLACEMENT` prize sets, plural
`tracks` and `types`, and canonical Challenge catalog values.
- Engagements: Engagements API, including top-level `durationWeeks` or
`durationMonths` and `IMMEDIATE`, `FEW_DAYS`, or `FEW_WEEKS` anticipated
start values. Cards display hydrated `skills[].name` values and retain
`requiredSkills` IDs only as a fallback for older API deployments.
- Copilot opportunities: Projects API, where the Figma track facet maps to the
opportunity `type` enum (`dev`, `qa`, `design`, `ai`, `datascience`).
- Review opportunities: Review API metadata search. The application action
uses `defaultApplicationRole` or a role selected from `applicationRoles`.

Challenge registration and registrant displays resolve the canonical
Submitter resource role and exclude copilot, reviewer, observer, and manager
resources. The Registrants tab requests bounded Resource API pages and uses its
pagination headers instead of truncating a fixed bucket. “My competitions”
resolves that same role and sends `memberId` plus
`resourceRoleId` to Challenge API so role narrowing happens before the other
filters, global sorting, counts, and pagination. The terms modal similarly
filters Challenge API references to the
Submitter role and loads complete v5 Terms API records before an electronic
agreement. Passive “Review challenge terms” mode never registers or agrees on
a member's behalf.
DocuSign-template terms expose the Terms API recipient flow and return to the
challenge route after signing; registration remains blocked until the service
reports that every external agreement is complete.

Design preview galleries use the public-safe
`GET /v6/submissions/previews?challengeId=...` endpoint rather than the
protected general submissions list. Review API remains authoritative for group,
whitelist, and review-phase release checks and returns only released Payload
asset URLs; an absent preview is rendered as a pending placeholder.

Forum links are composed from the environment-specific
`VANILLA_FORUM.V2_URL` origin, so development routes stay on the development
Vanilla instance and production routes stay on production.

The challenge rail parses case-insensitive `fileTypes`, `submissionLimit`,
`environment`, and `codeRepo` metadata, shows safe Challenge API discussions
and attachments, and fails closed for unsafe or retired-host URLs. Positive
legacy screening and review scorecard IDs link through the environment-specific
`ADMIN.ONLINE_REVIEW_URL`; Review App remains the primary authenticated review
handoff.
1 change: 1 addition & 0 deletions src/apps/opportunities/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src'
47 changes: 47 additions & 0 deletions src/apps/opportunities/src/OpportunitiesApp.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */
import '@testing-library/jest-dom'
import { render } from '@testing-library/react'
import { MemoryRouter, Route } from 'react-router-dom'

import { routerContext } from '~/libs/core'

import OpportunitiesApp from './OpportunitiesApp'

jest.mock('~/libs/core', () => {
const React = jest.requireActual('react')
return { routerContext: React.createContext({ getChildRoutes: () => [] }) }
}, { virtual: true })
jest.mock('./opportunities.routes', () => ({ toolTitle: 'Opportunities' }))

describe('OpportunitiesApp', () => {
afterEach(() => {
document.body.className = ''
})

it('scopes the 2026 design system to app content instead of universal navigation', () => {
const getChildRoutes = jest.fn(() => [
<Route element={<div>Opportunities</div>} key='root' path='/' />,
])
const result = render(
<MemoryRouter>
<routerContext.Provider value={{
getChildRoutes,
} as any}
>
<OpportunitiesApp />
</routerContext.Provider>
</MemoryRouter>,
)

expect(result.container.querySelector('.opportunities-app.tc-2026'))
.toBeInTheDocument()
expect(document.body)
.toHaveClass('opportunities-page')
expect(document.body)
.not.toHaveClass('opportunities-app', 'tc-2026')

result.unmount()
expect(document.body)
.not.toHaveClass('opportunities-page')
})
})
50 changes: 50 additions & 0 deletions src/apps/opportunities/src/OpportunitiesApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
FC,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import { Outlet, Routes } from 'react-router-dom'

import { routerContext, RouterContextData } from '~/libs/core'

import { OpportunityView } from './models'
import {
opportunityViewContext,
OpportunityViewContextData,
} from './opportunities.context'
import { toolTitle } from './opportunities.routes'
import './styles/index.scss'

/**
* Hosts nested Opportunities routes and scopes the 2026 design system to this app.
*
* @returns the active child page and its nested Router elements.
* @throws Does not throw.
*/
const OpportunitiesApp: FC = () => {
const { getChildRoutes }: RouterContextData = useContext(routerContext)
const childRoutes = useMemo(() => getChildRoutes(toolTitle), [getChildRoutes])
const [view, setView] = useState<OpportunityView>('list')
const viewContext = useMemo<OpportunityViewContextData>(() => ({
onViewChange: setView,
view,
}), [view])

useEffect(() => {
document.body.classList.add('opportunities-page')
return () => document.body.classList.remove('opportunities-page')
}, [])

return (
<opportunityViewContext.Provider value={viewContext}>
<div className='opportunities-app tc-2026'>
<Outlet />
<Routes>{childRoutes}</Routes>
</div>
</opportunityViewContext.Provider>
)
}

export default OpportunitiesApp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/apps/opportunities/src/assets/challenge-type.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/apps/opportunities/src/assets/chevron-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/apps/opportunities/src/assets/competition.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading