From 2cc6317c62468eea74382e0805a34e15be9043ca Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 17 Aug 2026 07:42:57 +0300 Subject: [PATCH 1/7] PM-5370 - campus app setup --- src/apps/campus/index.ts | 1 + src/apps/campus/src/CampusApp.tsx | 20 ++++++++++++++ src/apps/campus/src/campus.routes.tsx | 33 +++++++++++++++++++++++ src/apps/campus/src/home.tsx | 10 +++++++ src/apps/campus/src/index.ts | 1 + src/apps/platform/src/platform.routes.tsx | 2 ++ src/config/constants.ts | 2 ++ 7 files changed, 69 insertions(+) create mode 100644 src/apps/campus/index.ts create mode 100644 src/apps/campus/src/CampusApp.tsx create mode 100644 src/apps/campus/src/campus.routes.tsx create mode 100644 src/apps/campus/src/home.tsx create mode 100644 src/apps/campus/src/index.ts diff --git a/src/apps/campus/index.ts b/src/apps/campus/index.ts new file mode 100644 index 000000000..6f39cd49b --- /dev/null +++ b/src/apps/campus/index.ts @@ -0,0 +1 @@ +export * from './src' diff --git a/src/apps/campus/src/CampusApp.tsx b/src/apps/campus/src/CampusApp.tsx new file mode 100644 index 000000000..931de6a3f --- /dev/null +++ b/src/apps/campus/src/CampusApp.tsx @@ -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 ( + <> + + {childRoutes} + + ) +} + +export default CampusApp diff --git a/src/apps/campus/src/campus.routes.tsx b/src/apps/campus/src/campus.routes.tsx new file mode 100644 index 000000000..0f891d648 --- /dev/null +++ b/src/apps/campus/src/campus.routes.tsx @@ -0,0 +1,33 @@ +import { lazyLoad, LazyLoadedComponent, PlatformRoute } from '~/libs/core' +import { AppSubdomain, EnvironmentConfig, ToolTitle } from '~/config' + +const CampusApp: LazyLoadedComponent = lazyLoad(() => import('./CampusApp')) +const CampusHomePage: LazyLoadedComponent = lazyLoad( + () => import('./home'), + 'CampusHomePage', +) + +export const rootRoute: string = ( + EnvironmentConfig.SUBDOMAIN === AppSubdomain.campus ? '' : `/${AppSubdomain.campus}` +) + +export const toolTitle: string = ToolTitle.campus + +export const campusRoutes: ReadonlyArray = [ + { + authRequired: true, + children: [ + { + children: [], + element: , + id: 'Campus Home', + route: '', + }, + ], + domain: AppSubdomain.campus, + element: , + id: toolTitle, + route: rootRoute, + title: toolTitle, + }, +] diff --git a/src/apps/campus/src/home.tsx b/src/apps/campus/src/home.tsx new file mode 100644 index 000000000..93568a303 --- /dev/null +++ b/src/apps/campus/src/home.tsx @@ -0,0 +1,10 @@ +import { FC } from 'react' + +const CampusHomePage: FC = () => ( +
+

Campus

+

Welcome to the Campus application.

+
+) + +export default CampusHomePage diff --git a/src/apps/campus/src/index.ts b/src/apps/campus/src/index.ts new file mode 100644 index 000000000..903dee652 --- /dev/null +++ b/src/apps/campus/src/index.ts @@ -0,0 +1 @@ +export { campusRoutes } from './campus.routes' diff --git a/src/apps/platform/src/platform.routes.tsx b/src/apps/platform/src/platform.routes.tsx index 87fc44c02..a39f1f82e 100644 --- a/src/apps/platform/src/platform.routes.tsx +++ b/src/apps/platform/src/platform.routes.tsx @@ -2,6 +2,7 @@ import { lazyLoad, LazyLoadedComponent, PlatformRoute } from '~/libs/core' import { learnRoutes } from '~/apps/learn' import { devCenterRoutes } from '~/apps/dev-center' +import { campusRoutes } from '~/apps/campus' import { profilesRoutes } from '~/apps/profiles' import { accountsRoutes } from '~/apps/accounts' import { onboardingRoutes } from '~/apps/onboarding' @@ -38,6 +39,7 @@ export const platformRoutes: Array = [ // that matches the current path ...onboardingRoutes, ...devCenterRoutes, + ...campusRoutes, ...copilotsRoutes, ...learnRoutes, ...profilesRoutes, diff --git a/src/config/constants.ts b/src/config/constants.ts index 62a460111..2c4f94c4e 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -9,6 +9,7 @@ export enum AppSubdomain { wallet = 'wallet', walletAdmin = 'wallet-admin', copilots = 'copilots', + campus = 'campus', admin = 'system-admin', review = 'review', calendar = 'calendar', @@ -32,6 +33,7 @@ export enum ToolTitle { wallet = 'Wallet', walletAdmin = 'Wallet Admin', copilots = 'Copilots', + campus = 'Campus', admin = 'Admin', review = 'Review', calendar = 'Calendar', From e39b1e640cb0d2dd993c19dc87a3739e5d1b92d6 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Aug 2026 09:29:12 +0300 Subject: [PATCH 2/7] PM-5370 - campus leaderboard page --- src/apps/campus/index.ts | 2 +- src/apps/campus/src/campus.routes.tsx | 11 + src/apps/campus/src/lib/hooks/index.ts | 1 + .../src/lib/hooks/use-campus-leaderboard.ts | 38 +++ .../lib/models/campus-leaderboard.model.ts | 63 ++++ src/apps/campus/src/lib/models/index.ts | 1 + .../services/campus-leaderboard.service.ts | 32 ++ src/apps/campus/src/lib/services/index.ts | 1 + .../CampusLeaderboardPage.module.scss | 172 ++++++++++ .../CampusLeaderboardPage.spec.tsx | 222 +++++++++++++ .../leaderboard/CampusLeaderboardPage.tsx | 301 ++++++++++++++++++ .../ParticipationHistoryModal.module.scss | 23 ++ .../leaderboard/ParticipationHistoryModal.tsx | 124 ++++++++ .../leaderboard/RankingRulesModal.module.scss | 14 + .../pages/leaderboard/RankingRulesModal.tsx | 43 +++ .../campus/src/pages/leaderboard/index.ts | 3 + tsconfig.paths.json | 3 + 17 files changed, 1053 insertions(+), 1 deletion(-) create mode 100644 src/apps/campus/src/lib/hooks/index.ts create mode 100644 src/apps/campus/src/lib/hooks/use-campus-leaderboard.ts create mode 100644 src/apps/campus/src/lib/models/campus-leaderboard.model.ts create mode 100644 src/apps/campus/src/lib/models/index.ts create mode 100644 src/apps/campus/src/lib/services/campus-leaderboard.service.ts create mode 100644 src/apps/campus/src/lib/services/index.ts create mode 100644 src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.module.scss create mode 100644 src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.spec.tsx create mode 100644 src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx create mode 100644 src/apps/campus/src/pages/leaderboard/ParticipationHistoryModal.module.scss create mode 100644 src/apps/campus/src/pages/leaderboard/ParticipationHistoryModal.tsx create mode 100644 src/apps/campus/src/pages/leaderboard/RankingRulesModal.module.scss create mode 100644 src/apps/campus/src/pages/leaderboard/RankingRulesModal.tsx create mode 100644 src/apps/campus/src/pages/leaderboard/index.ts diff --git a/src/apps/campus/index.ts b/src/apps/campus/index.ts index 6f39cd49b..3c2923024 100644 --- a/src/apps/campus/index.ts +++ b/src/apps/campus/index.ts @@ -1 +1 @@ -export * from './src' +export { campusRoutes } from './src' diff --git a/src/apps/campus/src/campus.routes.tsx b/src/apps/campus/src/campus.routes.tsx index 0f891d648..61efd6028 100644 --- a/src/apps/campus/src/campus.routes.tsx +++ b/src/apps/campus/src/campus.routes.tsx @@ -6,6 +6,10 @@ const CampusHomePage: LazyLoadedComponent = lazyLoad( () => import('./home'), 'CampusHomePage', ) +const CampusLeaderboardPage: LazyLoadedComponent = lazyLoad( + () => import('./pages/leaderboard'), + 'CampusLeaderboardPage', +) export const rootRoute: string = ( EnvironmentConfig.SUBDOMAIN === AppSubdomain.campus ? '' : `/${AppSubdomain.campus}` @@ -23,6 +27,13 @@ export const campusRoutes: ReadonlyArray = [ id: 'Campus Home', route: '', }, + { + // Campus program leaderboard, eg. https://campus.topcoder-dev.com/mecw + children: [], + element: , + id: 'Campus Leaderboard', + route: ':groupName', + }, ], domain: AppSubdomain.campus, element: , diff --git a/src/apps/campus/src/lib/hooks/index.ts b/src/apps/campus/src/lib/hooks/index.ts new file mode 100644 index 000000000..55f29ec92 --- /dev/null +++ b/src/apps/campus/src/lib/hooks/index.ts @@ -0,0 +1 @@ +export * from './use-campus-leaderboard' diff --git a/src/apps/campus/src/lib/hooks/use-campus-leaderboard.ts b/src/apps/campus/src/lib/hooks/use-campus-leaderboard.ts new file mode 100644 index 000000000..7a4b1a954 --- /dev/null +++ b/src/apps/campus/src/lib/hooks/use-campus-leaderboard.ts @@ -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 = useSWR( + url, + fetchCampusLeaderboard, + { revalidateOnFocus: false }, + ) + + return { + data, + error, + isLoading: !!url && !data && !error, + } +} diff --git a/src/apps/campus/src/lib/models/campus-leaderboard.model.ts b/src/apps/campus/src/lib/models/campus-leaderboard.model.ts new file mode 100644 index 000000000..b450c0379 --- /dev/null +++ b/src/apps/campus/src/lib/models/campus-leaderboard.model.ts @@ -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 +} diff --git a/src/apps/campus/src/lib/models/index.ts b/src/apps/campus/src/lib/models/index.ts new file mode 100644 index 000000000..d4bcf47dd --- /dev/null +++ b/src/apps/campus/src/lib/models/index.ts @@ -0,0 +1 @@ +export * from './campus-leaderboard.model' diff --git a/src/apps/campus/src/lib/services/campus-leaderboard.service.ts b/src/apps/campus/src/lib/services/campus-leaderboard.service.ts new file mode 100644 index 000000000..50e3e59b5 --- /dev/null +++ b/src/apps/campus/src/lib/services/campus-leaderboard.service.ts @@ -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 { + return xhrGetAsync(url) +} diff --git a/src/apps/campus/src/lib/services/index.ts b/src/apps/campus/src/lib/services/index.ts new file mode 100644 index 000000000..cb7a5a248 --- /dev/null +++ b/src/apps/campus/src/lib/services/index.ts @@ -0,0 +1 @@ +export * from './campus-leaderboard.service' diff --git a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.module.scss b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.module.scss new file mode 100644 index 000000000..3451b2d1a --- /dev/null +++ b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.module.scss @@ -0,0 +1,172 @@ +@import '@libs/ui/styles/includes'; + +.header { + margin-top: $sp-8; + margin-bottom: $sp-6; + + h1 { + margin-bottom: $sp-2; + } +} + +.subtitle { + color: $black-60; +} + +.stats { + display: grid; + gap: $sp-4; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + margin-bottom: $sp-6; +} + +.statCard { + align-items: center; + background: $tc-white; + border: 1px solid $black-10; + border-radius: 8px; + display: flex; + gap: $sp-4; + padding: $sp-4; +} + +.statIcon { + align-items: center; + border-radius: 50%; + display: flex; + flex: 0 0 auto; + height: 48px; + justify-content: center; + width: 48px; + + svg { + height: 24px; + width: 24px; + } +} + +.statIconMembers { + background: #e6f7f0; + color: #0ab88a; +} + +.statIconRegistered { + background: #e9f2fe; + color: #2a8ded; +} + +.statIconSubmitted { + background: #f0eafc; + color: #7b61ff; +} + +.statLabel { + color: $black-80; + margin-bottom: $sp-1; +} + +.statValue { + @include font-barlow-condensed; + + font-size: 28px; + font-weight: 500; +} + +.toolbar { + align-items: center; + display: flex; + justify-content: space-between; + gap: $sp-4; + margin-bottom: $sp-4; +} + +.filter { + max-width: 320px; + min-width: 220px; + width: 100%; +} + +.lbTable { + tbody td { + vertical-align: middle; + } +} + +.rulesLink { + align-items: center; + background: none; + border: none; + color: $turq-160; + cursor: pointer; + display: flex; + gap: $sp-2; + padding: 0; + + svg { + height: 20px; + width: 20px; + } +} + +.rank { + align-items: center; + border-radius: 50%; + display: inline-flex; + font-weight: 700; + height: 28px; + justify-content: center; + width: 28px; +} + +.gold { + background: #f5c344; + color: $tc-white; +} + +.silver { + background: $black-20; + color: $black-100; +} + +.bronze { + background: #d9a48f; + color: $tc-white; +} + +.handleCell { + align-items: center; + display: flex; + gap: $sp-3; +} + +.avatar { + flex: 0 0 auto; + height: 40px; + width: 40px; +} + +.handle { + font-weight: 500; +} + +.wins { + color: #0ab88a; + font-weight: 500; +} + +.chevron { + color: $black-60; + height: 20px; + width: 20px; +} + +.clickableRow { + cursor: pointer; +} + +.empty, +.error { + color: $black-60; + padding: $sp-6 0; + text-align: center; +} diff --git a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.spec.tsx b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.spec.tsx new file mode 100644 index 000000000..0335d3135 --- /dev/null +++ b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.spec.tsx @@ -0,0 +1,222 @@ +/* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports, react/jsx-no-bind, + react/no-unused-prop-types, react/no-array-index-key, unicorn/no-null */ +import '@testing-library/jest-dom' +import type { ChangeEvent, PropsWithChildren, ReactNode } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { MemoryRouter, Route, Routes } from 'react-router-dom' + +import { CampusLeaderboard, CampusLeaderboardMember, CampusParticipation } from '../../lib/models' + +import { CampusLeaderboardPage } from './CampusLeaderboardPage' + +interface StubColumn { + columnId?: string + label?: string + propertyName?: string + renderer?: (data: T) => ReactNode +} + +interface StubTableProps { + columns: ReadonlyArray> + data: ReadonlyArray + moreToLoad?: boolean + onLoadMoreClick?: () => void + onRowClick?: (data: T) => void +} + +interface StubSelectProps { + onChange: (event: ChangeEvent) => void + options: ReadonlyArray<{ label?: ReactNode, value: string }> + value?: string +} + +jest.mock('~/config', () => ({ + AppSubdomain: { campus: 'campus' }, + EnvironmentConfig: { REPORTS_API: 'https://api.example.com/v6/reports', SUBDOMAIN: 'campus' }, +}), { virtual: true }) + +jest.mock('~/libs/shared', () => ({ + ProfilePicture: (): JSX.Element => , + textFormatDateLocaleShortString: (date?: Date): string | undefined => date?.toISOString(), +}), { virtual: true }) + +jest.mock('~/libs/ui', () => { + const Icon = (): JSX.Element => + + return { + BaseModal: (props: PropsWithChildren<{ open?: boolean, title?: ReactNode }>): JSX.Element => ( + props.open ? ( +
+

{props.title}

+ {props.children} +
+ ) : <> + ), + ContentLayout: (props: PropsWithChildren<{}>): JSX.Element =>
{props.children}
, + IconOutline: new Proxy({}, { get: () => Icon }), + InputSelect: (props: StubSelectProps): JSX.Element => ( + + ), + LoadingSpinner: (props: { hide?: boolean }): JSX.Element => ( + props.hide ? <> :
Loading
+ ), + PageTitle: (): JSX.Element => <>, + Table: (props: StubTableProps): JSX.Element => ( + + + {props.data.map((row, rowIndex) => ( + props.onRowClick?.(row)}> + {props.columns.map(column => ( + + ))} + + ))} + +
+ {column.renderer + ? column.renderer(row) + : String((row as Record)[column.propertyName ?? ''])} +
+ ), + } +}, { virtual: true }) + +const mockUseCampusLeaderboard = jest.fn() + +jest.mock('../../lib/hooks', () => ({ + useCampusLeaderboard: (...args: unknown[]) => mockUseCampusLeaderboard(...args), +})) + +const participation = (overrides: Partial = {}): CampusParticipation => ({ + challengeEndDate: '2026-02-01T00:00:00.000Z', + challengeId: 'c1', + challengeName: 'Campus Sprint', + challengeStatus: 'COMPLETED', + challengeTrack: 'Development', + challengeType: 'Challenge', + isCampusChallenge: true, + isPublicChallenge: false, + passedReview: true, + placement: 1, + registered: true, + registeredAt: '2026-01-05T00:00:00.000Z', + score: 95, + submitted: true, + submittedDate: '2026-01-20T00:00:00.000Z', + won: true, + ...overrides, +}) + +const member = (overrides: Partial = {}): CampusLeaderboardMember => ({ + challenges: [participation()], + firstName: 'Ada', + handle: 'testaws1', + hasActivity: true, + lastName: 'Lovelace', + memberSince: '2025-01-01T00:00:00.000Z', + passingSubmissions: 1, + photoURL: null, + rank: 1, + rating: 1500, + ratingColor: '#3f3', + registrations: 1, + signupDate: '2026-01-01T00:00:00.000Z', + submissions: 1, + userId: '1', + wins: 1, + ...overrides, +}) + +const leaderboard = (): CampusLeaderboard => ({ + challengeFilter: 'all', + group: { id: 'group-1', name: 'MECW', oldId: null, privateGroup: false }, + members: [ + member(), + member({ + challenges: [], + handle: 'quiet_member', + hasActivity: false, + passingSubmissions: 0, + rank: 2, + registrations: 0, + submissions: 0, + userId: '2', + wins: 0, + }), + ], + summary: { membersRegistered: 842, membersSubmitted: 623, totalMembers: 1248 }, +}) + +function renderPage(): void { + render( + + + } path='/:groupName' /> + + , + ) +} + +describe('CampusLeaderboardPage', () => { + beforeEach(() => { + mockUseCampusLeaderboard.mockReturnValue({ data: leaderboard(), isLoading: false }) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('requests the leaderboard for the group in the route', () => { + renderPage() + + expect(mockUseCampusLeaderboard) + .toHaveBeenCalledWith('mecw', 'all') + }) + + it('renders the participation summary and every group member', () => { + renderPage() + + expect(screen.getByText('1,248')) + .toBeInTheDocument() + expect(screen.getByText('842')) + .toBeInTheDocument() + expect(screen.getByText('623')) + .toBeInTheDocument() + expect(screen.getByText('testaws1')) + .toBeInTheDocument() + expect(screen.getByText('quiet_member')) + .toBeInTheDocument() + }) + + it('opens the participation history only for members with activity', () => { + renderPage() + + fireEvent.click(screen.getByText('quiet_member')) + expect(screen.queryByText(/Participation History/)).not.toBeInTheDocument() + + fireEvent.click(screen.getByText('testaws1')) + expect(screen.getByText(/testaws1 — Participation History/)) + .toBeInTheDocument() + expect(screen.getByText('Campus Sprint')) + .toBeInTheDocument() + expect(screen.getByText('Won (place 1)')) + .toBeInTheDocument() + }) + + it('re-requests the leaderboard when the challenge filter changes', () => { + renderPage() + + fireEvent.change(screen.getByTestId('challenge-filter'), { target: { value: 'campus' } }) + + expect(mockUseCampusLeaderboard) + .toHaveBeenLastCalledWith('mecw', 'campus') + }) +}) diff --git a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx new file mode 100644 index 000000000..64ac94a98 --- /dev/null +++ b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx @@ -0,0 +1,301 @@ +/** + * Campus program leaderboard for a single group (`/:groupName`). + */ +import { ChangeEvent, FC, useCallback, useMemo, useState } from 'react' +import { useParams } from 'react-router-dom' +import classNames from 'classnames' + +import { + ContentLayout, + IconOutline, + InputSelect, + InputSelectOption, + LoadingSpinner, + PageTitle, + Table, + TableColumn, +} from '~/libs/ui' +import { ProfilePicture } from '~/libs/shared' + +import { + CampusChallengeFilter, + CampusLeaderboardMember, +} from '../../lib/models' +import { CampusLeaderboardResource, useCampusLeaderboard } from '../../lib/hooks' + +import { ParticipationHistoryModal } from './ParticipationHistoryModal' +import { RankingRulesModal } from './RankingRulesModal' +import styles from './CampusLeaderboardPage.module.scss' + +const PAGE_SIZE: number = 50 + +const CHALLENGE_FILTER_OPTIONS: ReadonlyArray = [ + { label: 'All Challenges', value: 'all' }, + { label: 'Public Challenges', value: 'public' }, + { label: 'Campus Challenges', value: 'campus' }, +] + +const RANK_MEDAL_CLASSES: { [rank: number]: string } = { + 1: styles.gold, + 2: styles.silver, + 3: styles.bronze, +} + +/** + * Marks rows that open the participation history modal. + * + * @param member leaderboard row. + * @returns row class name, when the row is clickable. + */ +function rowClassName(member: CampusLeaderboardMember): string | undefined { + return member.hasActivity ? styles.clickableRow : undefined +} + +/** + * Renders a rank badge, medal-styled for the top three ranks. + * + * @param member leaderboard row. + * @returns rank cell. + */ +function renderRank(member: CampusLeaderboardMember): JSX.Element { + return ( + + {member.rank} + + ) +} + +/** + * Renders the member avatar and rating-colored handle. + * + * @param member leaderboard row. + * @returns handle cell. + */ +function renderHandle(member: CampusLeaderboardMember): JSX.Element { + return ( +
+ + + {member.handle ?? member.userId} + +
+ ) +} + +export const CampusLeaderboardPage: FC = () => { + const groupName: string | undefined = useParams<{ groupName: string }>().groupName + const [challengeFilter, setChallengeFilter] = useState('all') + const [visibleCount, setVisibleCount] = useState(PAGE_SIZE) + const [selectedMember, setSelectedMember] = useState() + const [rulesVisible, setRulesVisible] = useState(false) + + const { data, error, isLoading }: CampusLeaderboardResource + = useCampusLeaderboard(groupName, challengeFilter) + + const displayGroupName: string = data?.group.name ?? groupName ?? '' + + const onFilterChange = useCallback((event: ChangeEvent): void => { + setChallengeFilter(event.target.value as CampusChallengeFilter) + setVisibleCount(PAGE_SIZE) + }, []) + + const onRowClick = useCallback((member: CampusLeaderboardMember): void => { + if (!member.hasActivity) { + return + } + + setSelectedMember(member) + }, []) + + const columns = useMemo>>(() => [ + { + columnId: 'rank', + label: 'Rank', + renderer: renderRank, + type: 'element', + }, + { + columnId: 'handle', + label: 'Handle', + renderer: renderHandle, + type: 'element', + }, + { + columnId: 'registrations', + label: 'Number of Registrations', + propertyName: 'registrations', + tooltip: 'Challenges the member registered for.', + type: 'number', + }, + { + columnId: 'submissions', + label: 'Number of Submissions', + propertyName: 'submissions', + tooltip: 'Challenges the member submitted to. At most one submission is counted per challenge.', + type: 'number', + }, + { + columnId: 'passingSubmissions', + label: 'Number of Passing Submissions', + propertyName: 'passingSubmissions', + tooltip: 'Challenges where a submission passed review. ' + + 'At most one passing submission is counted per challenge.', + type: 'number', + }, + { + columnId: 'wins', + label: 'Number of Wins', + renderer: (member: CampusLeaderboardMember) => ( + {member.wins} + ), + type: 'numberElement', + }, + { + columnId: 'open', + label: '', + renderer: (member: CampusLeaderboardMember) => (member.hasActivity ? ( + + ) : ), + type: 'element', + }, + ], []) + + const members: ReadonlyArray = data?.members ?? [] + const visibleMembers = useMemo( + () => members.slice(0, visibleCount), + [members, visibleCount], + ) + + const onLoadMoreClick = useCallback((): void => { + setVisibleCount(count => count + PAGE_SIZE) + }, []) + + return ( + + Campus Program Leaderboard + +
+

Campus Program Leaderboard

+

+ {`Track participation and performance of members in the ${displayGroupName} `} + group across challenges. +

+
+ + + + {!!error && ( +
+ {error.response?.status === 403 + ? 'You do not have access to this leaderboard.' + : `The leaderboard for "${displayGroupName}" could not be loaded.`} +
+ )} + + {!!data && ( + <> +
+
+ + + +
+
Total Members in Group
+
+ {data.summary.totalMembers.toLocaleString()} +
+
+
+
+ + + +
+
+ Members + {' Registered to Any Challenge'} +
+
+ {data.summary.membersRegistered.toLocaleString()} +
+
+
+
+ + + +
+
+ Members + {' Submitted to Any Challenge'} +
+
+ {data.summary.membersSubmitted.toLocaleString()} +
+
+
+
+ +
+
+ +
+ +
+ + + + {!members.length && ( +
+ {`No members were found in the ${displayGroupName} group.`} +
+ )} + + )} + + + + + + ) +} + +export default CampusLeaderboardPage diff --git a/src/apps/campus/src/pages/leaderboard/ParticipationHistoryModal.module.scss b/src/apps/campus/src/pages/leaderboard/ParticipationHistoryModal.module.scss new file mode 100644 index 000000000..d21d0279d --- /dev/null +++ b/src/apps/campus/src/pages/leaderboard/ParticipationHistoryModal.module.scss @@ -0,0 +1,23 @@ +@import '@libs/ui/styles/includes'; + +.summary { + color: $black-80; + display: flex; + flex-wrap: wrap; + gap: $sp-5; + margin-bottom: $sp-4; +} + +.challengeCell { + display: flex; + flex-direction: column; +} + +.challengeName { + font-weight: 500; +} + +.challengeMeta { + color: $black-60; + font-size: 12px; +} diff --git a/src/apps/campus/src/pages/leaderboard/ParticipationHistoryModal.tsx b/src/apps/campus/src/pages/leaderboard/ParticipationHistoryModal.tsx new file mode 100644 index 000000000..f372c13f2 --- /dev/null +++ b/src/apps/campus/src/pages/leaderboard/ParticipationHistoryModal.tsx @@ -0,0 +1,124 @@ +/** + * Participation history for one leaderboard member. + */ +import { FC, useMemo } from 'react' + +import { BaseModal, Table, TableColumn } from '~/libs/ui' +import { textFormatDateLocaleShortString } from '~/libs/shared' + +import { CampusLeaderboardMember, CampusParticipation } from '../../lib/models' + +import styles from './ParticipationHistoryModal.module.scss' + +interface ParticipationHistoryModalProps { + member?: CampusLeaderboardMember + onClose: () => void +} + +/** + * Formats an api date as a short local date. + * + * @param value iso date string. + * @returns formatted date or an em dash. + */ +function formatDate(value: string | null): string { + return (value ? textFormatDateLocaleShortString(new Date(value)) : undefined) ?? '—' +} + +/** + * Describes the outcome of a member's participation in a challenge. + * + * @param entry participation entry. + * @returns human readable result. + */ +function formatResult(entry: CampusParticipation): string { + if (entry.won) { + return entry.placement ? `Won (place ${entry.placement})` : 'Won' + } + + if (entry.passedReview) { + return 'Passed review' + } + + if (entry.submitted) { + return 'Did not pass review' + } + + return 'No submission' +} + +export const ParticipationHistoryModal: FC = props => { + const member: CampusLeaderboardMember | undefined = props.member + + const columns = useMemo>>(() => [ + { + columnId: 'challenge', + label: 'Challenge', + renderer: (entry: CampusParticipation) => ( +
+ {entry.challengeName ?? entry.challengeId} + + {[entry.challengeTrack, entry.challengeType].filter(Boolean) + .join(' • ')} + +
+ ), + type: 'element', + }, + { + columnId: 'registeredAt', + label: 'Registered', + renderer: (entry: CampusParticipation) => {formatDate(entry.registeredAt)}, + type: 'element', + }, + { + columnId: 'submittedDate', + label: 'Submitted', + renderer: (entry: CampusParticipation) => {formatDate(entry.submittedDate)}, + type: 'element', + }, + { + columnId: 'result', + label: 'Result', + renderer: (entry: CampusParticipation) => {formatResult(entry)}, + type: 'element', + }, + ], []) + + if (!member) { + return <> + } + + return ( + +
+ + {`${member.registrations} registrations`} + + + {`${member.submissions} submissions`} + + + {`${member.passingSubmissions} passing`} + + + {`${member.wins} wins`} + +
+ +
+ + ) +} + +export default ParticipationHistoryModal diff --git a/src/apps/campus/src/pages/leaderboard/RankingRulesModal.module.scss b/src/apps/campus/src/pages/leaderboard/RankingRulesModal.module.scss new file mode 100644 index 000000000..3163ddb50 --- /dev/null +++ b/src/apps/campus/src/pages/leaderboard/RankingRulesModal.module.scss @@ -0,0 +1,14 @@ +@import '@libs/ui/styles/includes'; + +.rules { + list-style: decimal outside; + margin: $sp-3 0 $sp-4 $sp-5; + + li { + margin-bottom: $sp-1; + } +} + +.note { + color: $black-60; +} diff --git a/src/apps/campus/src/pages/leaderboard/RankingRulesModal.tsx b/src/apps/campus/src/pages/leaderboard/RankingRulesModal.tsx new file mode 100644 index 000000000..d7bebd976 --- /dev/null +++ b/src/apps/campus/src/pages/leaderboard/RankingRulesModal.tsx @@ -0,0 +1,43 @@ +/** + * Explains the leaderboard ranking criteria. + */ +import { FC } from 'react' + +import { BaseModal } from '~/libs/ui' + +import styles from './RankingRulesModal.module.scss' + +interface RankingRulesModalProps { + onClose: () => void + open: boolean +} + +export const RankingRulesModal: FC = props => { + if (!props.open) { + return <> + } + + return ( + +

Members are ranked by the following criteria, in order:

+
    +
  1. Number of wins, highest first
  2. +
  3. Number of passing submissions, highest first
  4. +
  5. Number of registrations, highest first
  6. +
  7. Signup time, earliest first
  8. +
+

+ At most one submission and one passing submission are counted per member per + challenge. Every member of the group is listed, including members with no + challenge activity. +

+
+ ) +} + +export default RankingRulesModal diff --git a/src/apps/campus/src/pages/leaderboard/index.ts b/src/apps/campus/src/pages/leaderboard/index.ts new file mode 100644 index 000000000..e6be4c579 --- /dev/null +++ b/src/apps/campus/src/pages/leaderboard/index.ts @@ -0,0 +1,3 @@ +export { default as CampusLeaderboardPage } from './CampusLeaderboardPage' +export { default as ParticipationHistoryModal } from './ParticipationHistoryModal' +export { default as RankingRulesModal } from './RankingRulesModal' diff --git a/tsconfig.paths.json b/tsconfig.paths.json index 54e6a260e..809bc6b7c 100644 --- a/tsconfig.paths.json +++ b/tsconfig.paths.json @@ -27,6 +27,9 @@ "@wallet/*": [ "./src/apps/wallet/src/*" ], + "@campus/*": [ + "./src/apps/campus/src/*" + ], "@walletAdmin/*": [ "./src/apps/wallet-admin/src/*" ], From 115599f162e47bddd41373607ff925b2c5245804 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Aug 2026 14:35:50 +0300 Subject: [PATCH 3/7] PM-5370 - campus data loading --- .../CampusLeaderboardPage.module.scss | 4 ++ .../leaderboard/CampusLeaderboardPage.tsx | 56 +++++++++++-------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.module.scss b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.module.scss index 3451b2d1a..947620fba 100644 --- a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.module.scss +++ b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.module.scss @@ -86,6 +86,10 @@ width: 100%; } +.tableWrapper { + position: relative; +} + .lbTable { tbody td { vertical-align: middle; diff --git a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx index 64ac94a98..f00d69160 100644 --- a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx +++ b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx @@ -2,7 +2,7 @@ * Campus program leaderboard for a single group (`/:groupName`). */ import { ChangeEvent, FC, useCallback, useMemo, useState } from 'react' -import { useParams } from 'react-router-dom' +import { useParams, useSearchParams } from 'react-router-dom' import classNames from 'classnames' import { @@ -94,7 +94,12 @@ function renderHandle(member: CampusLeaderboardMember): JSX.Element { export const CampusLeaderboardPage: FC = () => { const groupName: string | undefined = useParams<{ groupName: string }>().groupName - const [challengeFilter, setChallengeFilter] = useState('all') + const [searchParams, setSearchParams] = useSearchParams() + const searchChallengeFilter: string | null = searchParams.get('type') + const challengeFilter: CampusChallengeFilter = ( + searchChallengeFilter === 'public' || searchChallengeFilter === 'campus' + ) ? searchChallengeFilter : 'all' + const [visibleCount, setVisibleCount] = useState(PAGE_SIZE) const [selectedMember, setSelectedMember] = useState() const [rulesVisible, setRulesVisible] = useState(false) @@ -105,9 +110,15 @@ export const CampusLeaderboardPage: FC = () => { const displayGroupName: string = data?.group.name ?? groupName ?? '' const onFilterChange = useCallback((event: ChangeEvent): void => { - setChallengeFilter(event.target.value as CampusChallengeFilter) + const value = event.target.value as CampusChallengeFilter + + setSearchParams({ + ...Object.fromEntries(searchParams.entries()), + type: value, + }, { replace: true }) + setVisibleCount(PAGE_SIZE) - }, []) + }, [searchParams, setSearchParams]) const onRowClick = useCallback((member: CampusLeaderboardMember): void => { if (!member.hasActivity) { @@ -192,8 +203,6 @@ export const CampusLeaderboardPage: FC = () => {

- - {!!error && (
{error.response?.status === 403 @@ -202,7 +211,7 @@ export const CampusLeaderboardPage: FC = () => {
)} - {!!data && ( + {(!!data || isLoading) && ( <>
@@ -212,7 +221,7 @@ export const CampusLeaderboardPage: FC = () => {
Total Members in Group
- {data.summary.totalMembers.toLocaleString()} + {data?.summary.totalMembers.toLocaleString() ?? '-'}
@@ -226,7 +235,7 @@ export const CampusLeaderboardPage: FC = () => { {' Registered to Any Challenge'}
- {data.summary.membersRegistered.toLocaleString()} + {data?.summary.membersRegistered.toLocaleString() ?? '-'}
@@ -240,7 +249,7 @@ export const CampusLeaderboardPage: FC = () => { {' Submitted to Any Challenge'}
- {data.summary.membersSubmitted.toLocaleString()} + {data?.summary.membersSubmitted.toLocaleString() ?? '-'}
@@ -265,19 +274,22 @@ export const CampusLeaderboardPage: FC = () => { -
+
+ +
+ - {!members.length && ( + {!isLoading && !members.length && (
{`No members were found in the ${displayGroupName} group.`}
From 824dd183999aac97344848bc0b5f2683bb639725 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Aug 2026 14:57:00 +0300 Subject: [PATCH 4/7] PM-5370 - clear out old code --- src/apps/campus/src/campus.routes.spec.tsx | 56 ++++++++++++++++++++++ src/apps/campus/src/campus.routes.tsx | 9 +--- src/apps/campus/src/home.tsx | 10 ---- 3 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 src/apps/campus/src/campus.routes.spec.tsx delete mode 100644 src/apps/campus/src/home.tsx diff --git a/src/apps/campus/src/campus.routes.spec.tsx b/src/apps/campus/src/campus.routes.spec.tsx new file mode 100644 index 000000000..c5db10f66 --- /dev/null +++ b/src/apps/campus/src/campus.routes.spec.tsx @@ -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
{location.pathname}
+} + +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( + + + } path={`${rootRoute}/mecw`} /> + + + , + ) + + expect((await screen.findByTestId('location-pathname')).textContent) + .toBe('/mecw') + }) +}) diff --git a/src/apps/campus/src/campus.routes.tsx b/src/apps/campus/src/campus.routes.tsx index 61efd6028..3bfb76248 100644 --- a/src/apps/campus/src/campus.routes.tsx +++ b/src/apps/campus/src/campus.routes.tsx @@ -1,11 +1,8 @@ import { lazyLoad, LazyLoadedComponent, PlatformRoute } from '~/libs/core' +import { Navigate } from 'react-router-dom' import { AppSubdomain, EnvironmentConfig, ToolTitle } from '~/config' const CampusApp: LazyLoadedComponent = lazyLoad(() => import('./CampusApp')) -const CampusHomePage: LazyLoadedComponent = lazyLoad( - () => import('./home'), - 'CampusHomePage', -) const CampusLeaderboardPage: LazyLoadedComponent = lazyLoad( () => import('./pages/leaderboard'), 'CampusLeaderboardPage', @@ -22,9 +19,7 @@ export const campusRoutes: ReadonlyArray = [ authRequired: true, children: [ { - children: [], - element: , - id: 'Campus Home', + element: , route: '', }, { diff --git a/src/apps/campus/src/home.tsx b/src/apps/campus/src/home.tsx deleted file mode 100644 index 93568a303..000000000 --- a/src/apps/campus/src/home.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { FC } from 'react' - -const CampusHomePage: FC = () => ( -
-

Campus

-

Welcome to the Campus application.

-
-) - -export default CampusHomePage From e2f296d1739a250a30e370bb6c7f3cf47432743b Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Aug 2026 15:19:14 +0300 Subject: [PATCH 5/7] PM-5370 - clickable row --- src/apps/campus/src/campus.routes.spec.tsx | 2 +- .../CampusLeaderboardPage.module.scss | 15 +++++++++++---- .../CampusLeaderboardPage.spec.tsx | 11 +++++++---- .../leaderboard/CampusLeaderboardPage.tsx | 19 ++++++++++--------- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/apps/campus/src/campus.routes.spec.tsx b/src/apps/campus/src/campus.routes.spec.tsx index c5db10f66..b42d5d4f3 100644 --- a/src/apps/campus/src/campus.routes.spec.tsx +++ b/src/apps/campus/src/campus.routes.spec.tsx @@ -42,7 +42,7 @@ describe('campus routes', () => { const fallbackRoute = campusChildRoutes.find(route => route.route === '') render( - + } path={`${rootRoute}/mecw`} /> diff --git a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.module.scss b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.module.scss index 947620fba..3505c8520 100644 --- a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.module.scss +++ b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.module.scss @@ -158,16 +158,23 @@ font-weight: 500; } +.chevronButton { + align-items: center; + background: transparent; + border: none; + color: inherit; + cursor: pointer; + display: inline-flex; + justify-content: center; + padding: 0; +} + .chevron { color: $black-60; height: 20px; width: 20px; } -.clickableRow { - cursor: pointer; -} - .empty, .error { color: $black-60; diff --git a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.spec.tsx b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.spec.tsx index 0335d3135..a520a79c9 100644 --- a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.spec.tsx +++ b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.spec.tsx @@ -196,13 +196,16 @@ describe('CampusLeaderboardPage', () => { .toBeInTheDocument() }) - it('opens the participation history only for members with activity', () => { + it('opens the participation history only when the chevron is clicked for active members', () => { renderPage() - fireEvent.click(screen.getByText('quiet_member')) - expect(screen.queryByText(/Participation History/)).not.toBeInTheDocument() + expect(screen.queryByRole('button', { + name: /View participation history for quiet_member/i, + })).not.toBeInTheDocument() - fireEvent.click(screen.getByText('testaws1')) + fireEvent.click(screen.getByRole('button', { + name: /View participation history for testaws1/i, + })) expect(screen.getByText(/testaws1 — Participation History/)) .toBeInTheDocument() expect(screen.getByText('Campus Sprint')) diff --git a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx index f00d69160..ef71516f8 100644 --- a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx +++ b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx @@ -47,10 +47,6 @@ const RANK_MEDAL_CLASSES: { [rank: number]: string } = { * @param member leaderboard row. * @returns row class name, when the row is clickable. */ -function rowClassName(member: CampusLeaderboardMember): string | undefined { - return member.hasActivity ? styles.clickableRow : undefined -} - /** * Renders a rank badge, medal-styled for the top three ranks. * @@ -120,7 +116,7 @@ export const CampusLeaderboardPage: FC = () => { setVisibleCount(PAGE_SIZE) }, [searchParams, setSearchParams]) - const onRowClick = useCallback((member: CampusLeaderboardMember): void => { + const openParticipationHistory = useCallback((member: CampusLeaderboardMember): void => { if (!member.hasActivity) { return } @@ -175,11 +171,18 @@ export const CampusLeaderboardPage: FC = () => { columnId: 'open', label: '', renderer: (member: CampusLeaderboardMember) => (member.hasActivity ? ( - + ) : ), type: 'element', }, - ], []) + ], [openParticipationHistory]) const members: ReadonlyArray = data?.members ?? [] const visibleMembers = useMemo( @@ -283,9 +286,7 @@ export const CampusLeaderboardPage: FC = () => { disableSorting moreToLoad={visibleCount < members.length} onLoadMoreClick={onLoadMoreClick} - onRowClick={onRowClick} removeDefaultSort - rowClassName={rowClassName} /> From 6340c2c2749acb687aec66ed669fa8f285fce53c Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Aug 2026 15:34:17 +0300 Subject: [PATCH 6/7] lint --- src/apps/campus/src/campus.routes.tsx | 3 ++- .../campus/src/pages/leaderboard/CampusLeaderboardPage.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/apps/campus/src/campus.routes.tsx b/src/apps/campus/src/campus.routes.tsx index 3bfb76248..6426a9b61 100644 --- a/src/apps/campus/src/campus.routes.tsx +++ b/src/apps/campus/src/campus.routes.tsx @@ -1,5 +1,6 @@ -import { lazyLoad, LazyLoadedComponent, PlatformRoute } from '~/libs/core' 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')) diff --git a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx index ef71516f8..8e9023c41 100644 --- a/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx +++ b/src/apps/campus/src/pages/leaderboard/CampusLeaderboardPage.tsx @@ -173,7 +173,7 @@ export const CampusLeaderboardPage: FC = () => { renderer: (member: CampusLeaderboardMember) => (member.hasActivity ? (