Skip to content

Commit

Permalink
Cohort stats leaderboard (#21)
Browse files Browse the repository at this point in the history
* Add types for fetching cohorts

* Add services for fetching cohort data

* Add frontend for Cohort Leaderboard

* Fix bug in loading CohortStats data

* Add cohort name to leaderboard entry
  • Loading branch information
Advayp authored Feb 9, 2025
1 parent b90dc59 commit 50cc899
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 1 deletion.
175 changes: 175 additions & 0 deletions src/components/CohortStatsLeaderboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { Flex, Spinner, Text, Box, HStack, Button } from '@chakra-ui/react';
import React, { useState } from 'react';
import {
CohortStatsOrderBy,
LeaderboardHeader,
LeaderboardType,
Row,
SortDirection,
} from '../types';
import Leaderboard from './Leaderboard';
import { OrderBySelect } from './OrderBySelect';
import { lastUpdated } from '../utils';
import { useLeaderboard } from '../hooks/useLeaderboard';
import { ATTENDANCE_PAGE_SIZE } from '../services/leaderboard';

interface Props {
order: CohortStatsOrderBy;
onOrderChange: (order: CohortStatsOrderBy) => void;
}

const formatLeaderboardEntry = (key: keyof Row, row: Row): React.ReactNode => {
return <Text fontWeight="medium">{row[key]}</Text>;
};

export const CohortStatsLeaderboard: React.FC<Props> = ({
order,
onOrderChange,
}) => {
const [sortDirection, setSortDirection] = useState<SortDirection>(
SortDirection.Desc
);

const [page, setPage] = useState(1);

const {
isLoading,
error,
leaderboardData: cohortStatsData,
count,
} = useLeaderboard(LeaderboardType.CohortStats, order, page);

const totalPages = Math.ceil(count! / ATTENDANCE_PAGE_SIZE);

if (isLoading) {
return (
<Flex justify="center" align="center" h="400px">
<Spinner size="lg" color="blue.500" thickness="3px" />
</Flex>
);
}

if (error) {
return (
<Flex justify="center" align="center" h="400px">
<Text color="red.500">Failed to load leaderboard data</Text>
</Flex>
);
}

const attendanceOptions = [
{ value: CohortStatsOrderBy.Applications, label: 'Applications' },
{ value: CohortStatsOrderBy.DailyCheck, label: 'Daily Check-ins' },
{ value: CohortStatsOrderBy.Interviews, label: 'Interviews' },
{ value: CohortStatsOrderBy.Offers, label: 'Offers' },
{
value: CohortStatsOrderBy.OnlineAssessments,
label: 'Online Assessments',
},
];

const headers: LeaderboardHeader[] = [
{ key: 'rank', label: 'Rank', static: true },
{ key: 'username', label: 'Username', static: true },
{ key: 'applications', label: 'Applications', static: false },
{ key: 'dailyCheck', label: 'Daily Check-ins', static: false },
{ key: 'interviews', label: 'Interviews', static: false },
{ key: 'offers', label: 'Offers', static: false },
{ key: 'onlineAssessments', label: 'Online Assessments', static: false },
{ key: 'cohortName', label: 'Cohort Name', static: false },
];

const getOrderByFromKey = (key: keyof Row): CohortStatsOrderBy | null => {
switch (key) {
case 'applications':
return CohortStatsOrderBy.Applications;
case 'dailyCheck':
return CohortStatsOrderBy.DailyCheck;
case 'interviews':
return CohortStatsOrderBy.Interviews;
case 'offers':
return CohortStatsOrderBy.Offers;
case 'onlineAssessments':
return CohortStatsOrderBy.OnlineAssessments;
default:
return null;
}
};

const handleSort = (key: keyof Row) => {
const newOrder = getOrderByFromKey(key);
if (newOrder) {
// if active column clicked
if (newOrder === order) {
setSortDirection((prev) =>
prev === SortDirection.Asc ? SortDirection.Desc : SortDirection.Asc
);
} else {
// desc by default
setSortDirection(SortDirection.Desc);
onOrderChange(newOrder);
}
}
};

const orderColKey = (order: CohortStatsOrderBy) => {
switch (order) {
case CohortStatsOrderBy.Applications:
return 'applications';
case CohortStatsOrderBy.DailyCheck:
return 'dailyCheck';
case CohortStatsOrderBy.Interviews:
return 'interviews';
case CohortStatsOrderBy.Offers:
return 'offers';
case CohortStatsOrderBy.OnlineAssessments:
return 'onlineAssessments';
default:
return '';
}
};

return (
<Box>
<Flex justify="space-between" align="center" mb={4}>
<Text fontSize="sm" color="gray.500">
Last updated: {lastUpdated(cohortStatsData)}
</Text>
<OrderBySelect
value={order}
onChange={onOrderChange}
options={attendanceOptions}
/>
</Flex>
<Box>
<Leaderboard
data={cohortStatsData}
orderBy={order}
sortDirection={sortDirection}
orderColKey={orderColKey(order)}
headers={headers}
cellFormatter={formatLeaderboardEntry}
onSort={handleSort}
/>
<HStack w="100%" justify="center" mt={2}>
<Button
isDisabled={page == 1}
onClick={() => {
setPage(page - 1);
}}
>
Previous
</Button>
<Button
isDisabled={page == totalPages}
onClick={() => {
setPage(page + 1);
}}
>
Next
</Button>
</HStack>
</Box>
</Box>
);
};
16 changes: 16 additions & 0 deletions src/pages/LeaderboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import {
GitHubOrderBy,
ApplicationOrderBy,
EngagementOrderBy,
CohortStatsOrderBy,
} from '../types';
import { LeetcodeLeaderboard } from '../components/LeetcodeLeaderboard';
import { GithubLeaderboard } from '../components/GithubLeaderboard';
import { ApplicationLeaderboard } from '../components/ApplicationLeaderboard';
import { AttendanceLeaderboard } from '../components/AttendanceLeaderboard';
import { CohortStatsLeaderboard } from '../components/CohortStatsLeaderboard';

const LeaderboardPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<LeaderboardType>(
Expand All @@ -38,6 +40,9 @@ const LeaderboardPage: React.FC = () => {
EngagementOrderBy.Attendance
);

const [cohortStatsOrderBy, setCohortStatsOrderBy] =
useState<CohortStatsOrderBy>(CohortStatsOrderBy.DailyCheck);

const borderColor = useColorModeValue('gray.200', 'gray.700');

const includedLeaderboards = [
Expand All @@ -55,6 +60,10 @@ const LeaderboardPage: React.FC = () => {
type: LeaderboardType.Attendance,
tabLabel: 'Attendance',
},
{
type: LeaderboardType.CohortStats,
tabLabel: 'Cohort Stats',
},
];

const handleTabChange = (index: number) => {
Expand Down Expand Up @@ -104,6 +113,13 @@ const LeaderboardPage: React.FC = () => {
order={engagementOrderBy}
/>
);
case LeaderboardType.CohortStats:
return (
<CohortStatsLeaderboard
onOrderChange={setCohortStatsOrderBy}
order={cohortStatsOrderBy}
/>
);
}
};

Expand Down
12 changes: 12 additions & 0 deletions src/services/cohort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { RawCohortData, Cohort } from '../types';
import { deserializeMember } from './member';

export const deserializeCohortData = ({
members,
...rest
}: RawCohortData): Cohort => {
return {
members: members.map(deserializeMember),
...rest,
};
};
60 changes: 60 additions & 0 deletions src/services/leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ import {
RawPaginatedAttendanceResponse,
PaginatedAttendanceResponse,
PaginatedLeaderboardResponse,
CohortStatsOrderBy,
RawPaginatedCohortStatsResponse,
PaginatedCohortStatsResponse,
RawCohortStats,
CohortStats,
} from '../types';
import { devPrint } from '../components/utils/RandomUtils';
import { deserializeCohortData } from './cohort';

function deserializeLeetCodeStats({
user: { username },
Expand Down Expand Up @@ -87,6 +93,33 @@ function deserializePaginatedAttendanceResponse({
};
}

function deserializeCohortStats({
member: { username },
daily_checks,
online_assessments,
cohort,
...rest
}: RawCohortStats): CohortStats {
return {
username,
dailyCheck: daily_checks,
onlineAssessments: online_assessments,
lastUpdated: new Date(),
cohortName: deserializeCohortData(cohort).name,
...rest,
};
}

function deserializePaginatedCohortStatsResponse({
results,
...rest
}: RawPaginatedCohortStatsResponse): PaginatedCohortStatsResponse {
return {
data: results.map(deserializeCohortStats),
...rest,
};
}

export function getLeetcodeLeaderboard(
orderBy: LeetCodeOrderBy = LeetCodeOrderBy.Total
): Promise<void | PaginatedLeaderboardResponse> {
Expand Down Expand Up @@ -184,6 +217,31 @@ export function getAttendanceLeaderboard(
.catch(devPrint);
}

export const COHORT_PAGE_SIZE = 50;

export function getCohortStatsLeaderboard(
orderBy: CohortStatsOrderBy = CohortStatsOrderBy.DailyCheck,
page: number = 1,
pageSize: number = COHORT_PAGE_SIZE
) {
return api
.get(
`/leaderboard/cohorts/?order_by=${orderBy}&page=${page}&page_size=${pageSize}`
)
.then((res) => {
if (res.status !== 200) {
throw new Error('Failed to get cohort stats leaderboard');
}

const deserializedResponse = deserializePaginatedCohortStatsResponse(
res.data
);

return deserializedResponse;
})
.catch(devPrint);
}

export const getLeaderboardDataHandlerFromType = (
type: LeaderboardType
): LeaderboardDataHandler => {
Expand All @@ -198,6 +256,8 @@ export const getLeaderboardDataHandlerFromType = (
return getNewGradLeaderboard as LeaderboardDataHandler;
case LeaderboardType.Attendance:
return getAttendanceLeaderboard as LeaderboardDataHandler;
case LeaderboardType.CohortStats:
return getCohortStatsLeaderboard as LeaderboardDataHandler;
default:
throw new Error('Invalid leaderboard type was provided');
}
Expand Down
Loading

0 comments on commit 50cc899

Please sign in to comment.