Skip to content

Commit

Permalink
chore: fix contest submissions table
Browse files Browse the repository at this point in the history
  • Loading branch information
bacali95 committed Oct 27, 2024
1 parent e4580b1 commit 5a8c72b
Show file tree
Hide file tree
Showing 18 changed files with 501 additions and 330 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ Thumbs.db

.nx/cache
.nx/workspace-data

**/vite.config.{js,ts,mjs,mts,cjs,cts}.timestamp*
6 changes: 3 additions & 3 deletions apps/client/src/core/components/PageTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const PageTemplate = forwardRef<HTMLDivElement, Props>(
justify="between"
fullWidth
>
<Flex className="gap-2 text-xl" align="center">
<Flex className="gap-2 text-xl" align="center" fullWidth>
{!isSubSection && (
<>
<Sidebar.Trigger />
Expand All @@ -58,11 +58,11 @@ export const PageTemplate = forwardRef<HTMLDivElement, Props>(
{title}
{filtersProps && <FiltersTrigger {...filtersProps} />}
</Flex>
{
{actions && (
<Flex className="gap-2" align="center">
{actions}
</Flex>
}
)}
</Flex>
{filtersProps && <FiltersContent {...filtersProps} />}
<Flex
Expand Down
30 changes: 30 additions & 0 deletions apps/client/src/core/components/SubmissionResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FC } from 'react';
import { cn } from 'tw-react-components';

import { Judging, Prisma } from '@prisma/client';

import { dateComparator } from '@core/utils';

import { JUDGING_RESULT_LABELS } from '../constants';

type Props = {
submission: Prisma.SubmissionGetPayload<{ include: { judgings: true } }>;
};

export const SubmissionResult: FC<Props> = ({ submission }) => {
const judging = submission.judgings
.slice()
.sort(dateComparator<Judging>('startTime', true))
.shift();
return (
<b
className={cn({
'text-green-700 dark:text-green-400': judging?.result === 'ACCEPTED',
'text-red-700 dark:text-red-400': judging?.result && judging.result !== 'ACCEPTED',
'text-gray-500 dark:text-gray-400': !judging?.result,
})}
>
{JUDGING_RESULT_LABELS[judging?.result ?? 'PENDING']}
</b>
);
};
1 change: 1 addition & 0 deletions apps/client/src/core/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './Login';
export * from './Logout';
export * from './NavBar';
export * from './PageTemplate';
export * from './SubmissionResult';
12 changes: 12 additions & 0 deletions apps/client/src/core/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { JudgingResult } from '@prisma/client';

export const JUDGING_RESULT_LABELS: Record<JudgingResult | 'PENDING', string> = {
ACCEPTED: 'Accepted',
WRONG_ANSWER: 'Wrong Answer',
TIME_LIMIT_EXCEEDED: 'Time Limit Exceeded',
MEMORY_LIMIT_EXCEEDED: 'Memory Limit Exceeded',
RUNTIME_ERROR: 'Runtime Error',
COMPILATION_ERROR: 'Compile Error',
SYSTEM_ERROR: 'System Error',
PENDING: 'Pending',
};
2 changes: 1 addition & 1 deletion apps/client/src/core/contexts/auth/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Prisma } from '@prisma/client';

import { request } from '../../utils';

export type User = Prisma.UserGetPayload<{ include: { role: true } }>;
export type User = Prisma.UserGetPayload<{ include: { role: true; team: true } }>;

export const AuthContext = createContext<AuthContext<User>>({
connected: false,
Expand Down
5 changes: 5 additions & 0 deletions apps/client/src/core/utils/dateComparator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function dateComparator<T>(field: keyof T, inv = false): (a: T, b: T) => number {
return (a, b) =>
new Date((inv ? b : a)[field] as Date).getTime() -
new Date((inv ? a : b)[field] as Date).getTime();
}
15 changes: 15 additions & 0 deletions apps/client/src/core/utils/formatRestTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function formatRestTime(time: number, withSeconds = true): string {
if (time <= 0) return 'contest over';
const days = Math.floor(time / 86400);
const hours = Math.floor((time % 86400) / 3600);
const minutes = Math.floor((time % 3600) / 60);
const seconds = Math.floor(time % 60);

let result = '';
days && (result += `${days}d `);
(days || hours) && (result += `${hours.toString().padStart(2, '0')}:`);
result += minutes.toString().padStart(2, '0');
withSeconds && (result += `:${seconds.toString().padStart(2, '0')}`);

return result;
}
3 changes: 3 additions & 0 deletions apps/client/src/core/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
export * from './files';

export * from './dateComparator';
export * from './formatBytes';
export * from './formatRestTime';
export * from './getDisplayDate';
export * from './getRandomHexColor';
export * from './getRGBColorContrast';
export * from './isContestRunning';
export * from './queryParser';
export * from './request';
11 changes: 11 additions & 0 deletions apps/client/src/core/utils/isContestRunning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Contest } from '@prisma/client';

export function isContestRunning(contest?: Contest): boolean {
const now = Date.now();
return (
!!contest &&
!!contest.endTime &&
new Date(contest.startTime).getTime() < now &&
now < new Date(contest.endTime).getTime()
);
}
66 changes: 26 additions & 40 deletions apps/client/src/pages/admin/AdminLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ import {
UsersRoundIcon,
} from 'lucide-react';
import { FC, useMemo } from 'react';
import { Route, Routes } from 'react-router-dom';
import { Navigate, Route, Routes } from 'react-router-dom';
import { Layout, Sidebar, SidebarProps } from 'tw-react-components';

import { ContestsSection } from './ContestsSection';
import { NavUser } from './NavUser';
import { ContestView } from './views/contests/ContestView';
import { ContestsList } from './views/contests/ContestsList';
import { ExecutablesList } from './views/executables/ExecutablesList';
import { JudgeHostsList } from './views/judge-hosts/JudgeHostsList';
import { LanguagesList } from './views/languages/LanguagesList';
import { ProblemView } from './views/problems/ProblemView';
import { ProblemsList } from './views/problems/ProblemsList';
import { SubmissionsList } from './views/submissions/SubmissionsList';
import { TeamCategoriesList } from './views/team-category/TeamCategoriesList';
import { TeamsList } from './views/teams/TeamsList';
import { UsersList } from './views/users/UsersList';
Expand Down Expand Up @@ -86,57 +89,40 @@ export const AdminLayout: FC = () => {
title: 'Judge Hosts',
Icon: ServerIcon,
},
// {
// pathname: 'submissions',
// title: 'Submissions',
// Icon: PaperAirplaneIcon,
// label:
// totalPendingSubmissions > 0 ? (
// <div className="rounded-md bg-yellow-500 px-2 py-0.5 text-white">
// {totalPendingSubmissions}
// </div>
// ) : undefined,
// },
// {
// pathname: 'clarifications',
// title: 'Clarifications',
// Icon: ChatIcon,
// },
// {
// pathname: 'scoreboard',
// title: 'Scoreboard',
// Icon: ChartBarIcon,
// },
],
},
],
extraContent: <ContestsSection />,
footer: <NavUser />,
}),
[],
);

return (
<Layout
className="p-0"
sidebarProps={sidebarProps}
// <ActiveContestSelector className="rounded-md p-2 hover:bg-gray-200 dark:hover:bg-gray-700" />
>
<Layout className="p-0" sidebarProps={sidebarProps}>
<Routes>
{/* <Route exact path="/" component={Dashboard} />*/}
<Route path="/users" element={<UsersList />} />
<Route path="/teams" element={<TeamsList />} />
<Route path="/teams/categories" element={<TeamCategoriesList />} />
<Route path="/contests" element={<ContestsList />} />
<Route path="/problems" element={<ProblemsList />} />
<Route path="/problems/:id" element={<ProblemView />} />
<Route path="/languages" element={<LanguagesList />} />
<Route path="/executables" element={<ExecutablesList />} />
<Route path="/judge-hosts" element={<JudgeHostsList />} />
<Route path="users" element={<UsersList />} />
<Route path="teams" element={<TeamsList />} />
<Route path="teams/categories" element={<TeamCategoriesList />} />
<Route path="contests" element={<ContestsList />} />
<Route path="contests/:id" element={<ContestView />}>
<Route path="submissions" element={<SubmissionsList />} />
<Route path="clarifications" element={'Clarifications'} />
<Route path="scoreboard" element={'Scoreboard'} />
<Route path="*" element={<Navigate to="submissions" replace />} />
</Route>
<Route path="problems" element={<ProblemsList />} />
<Route path="problems/:id" element={<ProblemView />} />
<Route path="languages" element={<LanguagesList />} />
<Route path="executables" element={<ExecutablesList />} />
<Route path="judge-hosts" element={<JudgeHostsList />} />
<Route path="*" element={<Navigate to="" replace />} />
{/*
<Route exact path="/submissions" component={SubmissionsList} />
<Route path="/submissions/:id" component={SubmissionsView} />
<Route exact path="/clarifications" component={ClarificationsList} />
<Route exact path="/scoreboard" component={Scoreboard} />
<Route exact path="submissions" component={SubmissionsList} />
<Route path="submissions/:id" component={SubmissionsView} />
<Route exact path="clarifications" component={ClarificationsList} />
<Route exact path="scoreboard" component={Scoreboard} />
<Route render={() => <Redirect to="/" />} />
*/}
</Routes>
Expand Down
92 changes: 92 additions & 0 deletions apps/client/src/pages/admin/ContestsSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { MessagesSquareIcon, PresentationIcon, SendIcon, StarsIcon } from 'lucide-react';
import { FC, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { DropdownMenu, Flex, Sidebar, useSidebar } from 'tw-react-components';

import { useFindManyContest } from '@models';

export const ContestsSection: FC = () => {
const { isMobile } = useSidebar();
const [now, setNow] = useState(new Date());

const { data: contests } = useFindManyContest({
where: {
enabled: true,
activateTime: { lte: now },
},
select: { id: true, name: true, scoreCaches: { select: { restrictedPending: true } } },
orderBy: { activateTime: 'asc' },
});

useEffect(() => {
if (import.meta.env.MODE === 'development') return;

const interval = setInterval(() => {
setNow(new Date());
}, 5000);

return () => clearInterval(interval);
}, []);

return (
<Sidebar.Group>
<Sidebar.GroupLabel>Active Contests</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{contests?.map((contest) => {
const totalPendingSubmissions = contest.scoreCaches.reduce(
(acc, { restrictedPending }) => acc + restrictedPending,
0,
);

return (
<DropdownMenu key={contest.id}>
<Sidebar.MenuItem>
<DropdownMenu.Trigger asChild>
<Sidebar.MenuButton>
<StarsIcon />
{contest.name}
{totalPendingSubmissions > 0 && (
<Flex
className="ml-auto h-5 w-5 rounded bg-orange-500"
align="center"
justify="center"
>
{totalPendingSubmissions}
</Flex>
)}
</Sidebar.MenuButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content
className="min-w-56 rounded-lg text-sm"
side={isMobile ? 'bottom' : 'right'}
align={isMobile ? 'end' : 'start'}
>
<Link to={`/contests/${contest.id}/submissions`}>
<DropdownMenu.Item className="cursor-pointer">
<DropdownMenu.Icon icon={SendIcon} />
Submissions
</DropdownMenu.Item>
</Link>
<Link to={`/contests/${contest.id}/clarifications`}>
<DropdownMenu.Item className="cursor-pointer">
<DropdownMenu.Icon icon={MessagesSquareIcon} />
Clarifications
</DropdownMenu.Item>
</Link>
<Link to={`/contests/${contest.id}/scoreboard`}>
<DropdownMenu.Item className="cursor-pointer">
<DropdownMenu.Icon icon={PresentationIcon} />
Scoreboard
</DropdownMenu.Item>
</Link>
</DropdownMenu.Content>
</Sidebar.MenuItem>
</DropdownMenu>
);
})}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
);
};
36 changes: 36 additions & 0 deletions apps/client/src/pages/admin/views/contests/ContestView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { GraduationCapIcon } from 'lucide-react';
import { FC } from 'react';
import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom';
import { Flex, Tabs } from 'tw-react-components';

import { PageTemplate } from '@core/components';
import { useFindFirstContest } from '@models';

export const ContestView: FC = () => {
const location = useLocation();
const navigate = useNavigate();
const { id: contestId } = useParams();
const currentTab = location.pathname.split('/').pop();

const { data: contest } = useFindFirstContest({ where: { id: parseInt(contestId ?? '-1') } });

return (
<Tabs className="w-full" value={currentTab} onValueChange={(tab) => navigate(tab)}>
<PageTemplate
icon={GraduationCapIcon}
title={
<Flex align="center" justify="between" fullWidth>
{contest?.name}
<Tabs.List className="ml-2 w-fit text-sm [&>button]:h-6">
<Tabs.Trigger value="submissions">Submissions</Tabs.Trigger>
<Tabs.Trigger value="clarifications">Clarifications</Tabs.Trigger>
<Tabs.Trigger value="scoreboard">Scoreboard</Tabs.Trigger>
</Tabs.List>
</Flex>
}
>
<Outlet />
</PageTemplate>
</Tabs>
);
};
Loading

0 comments on commit 5a8c72b

Please sign in to comment.