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
1 change: 1 addition & 0 deletions src/apps/campus/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { campusRoutes } from './src'
20 changes: 20 additions & 0 deletions src/apps/campus/src/CampusApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { FC, useContext, useMemo } from 'react'
import { Outlet, Routes } from 'react-router-dom'

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

import { toolTitle } from './campus.routes'

const CampusApp: FC = () => {
const { getChildRoutes }: RouterContextData = useContext(routerContext)
const childRoutes = useMemo(() => getChildRoutes(toolTitle), [getChildRoutes])

return (
<>
<Outlet />
<Routes>{childRoutes}</Routes>
</>
)
}

export default CampusApp
56 changes: 56 additions & 0 deletions src/apps/campus/src/campus.routes.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */
import { render, screen } from '@testing-library/react'
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom'

import { campusRoutes, rootRoute } from './campus.routes'

jest.mock('~/config', () => ({
EnvironmentConfig: {
SUBDOMAIN: 'campus',
},
}), {
virtual: true,
})

jest.mock('~/config/constants', () => ({
AppSubdomain: {
campus: 'campus',
},
ToolTitle: {
campus: 'Campus',
},
}), {
virtual: true,
})

jest.mock('~/libs/core', () => ({
lazyLoad: () => (): undefined => undefined,
}), {
virtual: true,
})

const LocationViewer = (): JSX.Element => {
const location = useLocation()

return <div data-testid='location-pathname'>{location.pathname}</div>
}

describe('campus routes', () => {
it('redirects the campus root to /mecw when groupName is missing', async () => {
const campusAppRoute = campusRoutes[0]
const campusChildRoutes = campusAppRoute.children || []
const fallbackRoute = campusChildRoutes.find(route => route.route === '')

render(
<MemoryRouter initialEntries={[rootRoute || '/']}>
<Routes>
<Route element={<LocationViewer />} path={`${rootRoute}/mecw`} />
<Route element={fallbackRoute?.element} path={rootRoute || '/'} />
</Routes>
</MemoryRouter>,
)

expect((await screen.findByTestId('location-pathname')).textContent)
.toBe('/mecw')
})
})
40 changes: 40 additions & 0 deletions src/apps/campus/src/campus.routes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Navigate } from 'react-router-dom'

import { lazyLoad, LazyLoadedComponent, PlatformRoute } from '~/libs/core'
import { AppSubdomain, EnvironmentConfig, ToolTitle } from '~/config'

const CampusApp: LazyLoadedComponent = lazyLoad(() => import('./CampusApp'))
const CampusLeaderboardPage: LazyLoadedComponent = lazyLoad(
() => import('./pages/leaderboard'),
'CampusLeaderboardPage',
)

export const rootRoute: string = (
EnvironmentConfig.SUBDOMAIN === AppSubdomain.campus ? '' : `/${AppSubdomain.campus}`
)

export const toolTitle: string = ToolTitle.campus

export const campusRoutes: ReadonlyArray<PlatformRoute> = [
{
authRequired: true,
children: [
{
element: <Navigate replace to={`${rootRoute}/mecw`} />,
route: '',
},
{
// Campus program leaderboard, eg. https://campus.topcoder-dev.com/mecw
children: [],
element: <CampusLeaderboardPage />,
id: 'Campus Leaderboard',
route: ':groupName',
},
],
domain: AppSubdomain.campus,
element: <CampusApp />,
id: toolTitle,
route: rootRoute,
title: toolTitle,
},
]
1 change: 1 addition & 0 deletions src/apps/campus/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { campusRoutes } from './campus.routes'
1 change: 1 addition & 0 deletions src/apps/campus/src/lib/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './use-campus-leaderboard'
38 changes: 38 additions & 0 deletions src/apps/campus/src/lib/hooks/use-campus-leaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import useSWR, { SWRResponse } from 'swr'

import { CampusChallengeFilter, CampusLeaderboard } from '../models'
import { campusLeaderboardUrl, fetchCampusLeaderboard } from '../services'

export interface CampusLeaderboardResource {
data?: CampusLeaderboard
error?: Error & { response?: { status?: number } }
isLoading: boolean
}

/**
* Loads the campus leaderboard for a group, re-fetching when the filter changes.
*
* @param groupName group name from the route, when available.
* @param challengeFilter selected challenge visibility filter.
* @returns leaderboard resource state.
*/
export function useCampusLeaderboard(
groupName: string | undefined,
challengeFilter: CampusChallengeFilter,
): CampusLeaderboardResource {
const url: string | undefined = groupName
? campusLeaderboardUrl(groupName, challengeFilter)
: undefined

const { data, error }: SWRResponse<CampusLeaderboard, Error> = useSWR(
url,
fetchCampusLeaderboard,
{ revalidateOnFocus: false },
)

return {
data,
error,
isLoading: !!url && !data && !error,
}
}
63 changes: 63 additions & 0 deletions src/apps/campus/src/lib/models/campus-leaderboard.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Shapes returned by the campus leaderboard report endpoint.
*/

export type CampusChallengeFilter = 'all' | 'public' | 'campus'

export interface CampusParticipation {
challengeEndDate: string | null
challengeId: string
challengeName: string | null
challengeStatus: string | null
challengeTrack: string | null
challengeType: string | null
isCampusChallenge: boolean
isPublicChallenge: boolean
passedReview: boolean
placement: number | null
registered: boolean
registeredAt: string | null
score: number | null
submitted: boolean
submittedDate: string | null
won: boolean
}

export interface CampusLeaderboardMember {
challenges: CampusParticipation[]
firstName: string | null
handle: string | null
hasActivity: boolean
lastName: string | null
memberSince: string | null
passingSubmissions: number
photoURL: string | null
rank: number
rating: number | null
ratingColor: string | null
registrations: number
signupDate: string | null
submissions: number
userId: string
wins: number
}

export interface CampusLeaderboardSummary {
membersRegistered: number
membersSubmitted: number
totalMembers: number
}

export interface CampusLeaderboardGroup {
id: string
name: string
oldId: string | null
privateGroup: boolean
}

export interface CampusLeaderboard {
challengeFilter: CampusChallengeFilter
group: CampusLeaderboardGroup
members: CampusLeaderboardMember[]
summary: CampusLeaderboardSummary
}
1 change: 1 addition & 0 deletions src/apps/campus/src/lib/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './campus-leaderboard.model'
32 changes: 32 additions & 0 deletions src/apps/campus/src/lib/services/campus-leaderboard.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Read-only client for the campus leaderboard report.
*/
import { EnvironmentConfig } from '~/config'
import { xhrGetAsync } from '~/libs/core'

import { CampusChallengeFilter, CampusLeaderboard } from '../models'

/**
* Builds the campus leaderboard report url for a group and challenge filter.
*
* @param groupName group name taken from the route.
* @param challengeFilter challenge visibility filter.
* @returns absolute reports api url.
*/
export function campusLeaderboardUrl(
groupName: string,
challengeFilter: CampusChallengeFilter,
): string {
const params: URLSearchParams = new URLSearchParams({ challengeFilter, groupName })
return `${EnvironmentConfig.REPORTS_API}/topcoder/leaderboard/campus?${params.toString()}`
}

/**
* Fetches the campus leaderboard for a group.
*
* @param url campus leaderboard report url.
* @returns leaderboard payload.
*/
export async function fetchCampusLeaderboard(url: string): Promise<CampusLeaderboard> {
return xhrGetAsync<CampusLeaderboard>(url)
}
1 change: 1 addition & 0 deletions src/apps/campus/src/lib/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './campus-leaderboard.service'
Loading
Loading