This commit is contained in:
Виталий Лавшонок
2025-11-07 19:04:45 +03:00
parent 93a5366fd5
commit 69655dda82
7 changed files with 334 additions and 178 deletions

View File

@@ -1,19 +1,66 @@
import { useEffect } from 'react'; import { FC, useEffect } from "react";
import { useAppDispatch } from '../../../../redux/hooks'; import { useAppDispatch } from "../../../../redux/hooks";
import { setMenuActiveProfilePage } from '../../../../redux/slices/store'; import { setMenuActiveProfilePage } from "../../../../redux/slices/store";
import { cn } from "../../../../lib/cn";
interface ItemProps {
count: number;
totalCount: number;
title: string;
color?: "default" | "red" | "green" | "orange";
}
const Item: FC<ItemProps> = ({count, totalCount, title, color = "default"}) => {
return <div className={cn("flex flex-row rounded-full bg-liquid-lighter px-[16px] py-[8px] gap-[10px] text-[14px]",
color == "default" && "text-liquid-light",
color == "red" && "text-liquid-red",
color == "green" && "text-liquid-green",
color == "orange" && "text-liquid-orange",
)}>
<div>{count}/{totalCount}</div>
<div>{title}</div>
</div>
};
const MissionsBlock = () => { const MissionsBlock = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
dispatch(setMenuActiveProfilePage('missions')); dispatch(setMenuActiveProfilePage("missions"));
}, []); }, []);
return ( return (
<div className="h-full w-full relative flex items-center justify-center text-[60px] font-bold"> <div className="h-full w-full relative overflow-y-scroll medium-scrollbar">
Пока пусто :( <div className="w-full flex flex-col">
<div className="p-[20px] flex flex-col gap-[20px]">
<div className="text-[24px] font-bold text-liquid-white">Решенные задачи</div>
<div className="flex flex-row justify-between items-start">
<div className="flex gap-[10px]">
<Item count={14} totalCount={123} title="Задачи"/>
</div>
<div className="flex gap-[20px]">
<Item count={14} totalCount={123} title="Easy" color="green"/>
<Item count={14} totalCount={123} title="Medium" color="orange"/>
<Item count={14} totalCount={123} title="Hard" color="red"/>
</div>
</div>
<div className="text-[24px] font-bold text-liquid-white">Компетенции</div>
<div className="flex flex-wrap gap-[10px]">
<Item count={14} totalCount={123} title="Массивы"/>
<Item count={14} totalCount={123} title="Списки"/>
<Item count={14} totalCount={123} title="Стэк"/>
</div>
</div> </div>
); <div>Недавиние задачи</div>
<div>Мои задачи</div>
</div>
</div>
);
}; };
export default MissionsBlock; export default MissionsBlock;

View File

@@ -24,6 +24,7 @@ const Contest = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const contest = useAppSelector((state) => state.contests.fetchContestById.contest); const contest = useAppSelector((state) => state.contests.fetchContestById.contest);
useEffect(() => { useEffect(() => {
dispatch(setMenuActivePage('contest')); dispatch(setMenuActivePage('contest'));
}, []); }, []);
@@ -33,17 +34,16 @@ const Contest = () => {
}, [contestIdNumber]); }, [contestIdNumber]);
return ( return (
<div> <div className='w-full h-full'>
<PrimaryButton onClick={() => {navigate(`/contest/${contestIdNumber}/submissions`)}} text='Мои посылки' />
<Routes> <Routes>
<Route <Route
path="submissions" path="submissions"
element={<Submissions contestId={contestIdNumber} />} element={<Submissions contest={contest}/>}
/> />
<Route <Route
path="*" path="*"
element={<ContestMissions contest={contest} />} element={<ContestMissions contest={contest}/>}
/> />
</Routes> </Routes>

View File

@@ -10,7 +10,7 @@ export interface MissionItemProps {
timeLimit?: number; timeLimit?: number;
memoryLimit?: number; memoryLimit?: number;
type?: 'first' | 'second'; type?: 'first' | 'second';
status?: 'empty' | 'success' | 'error'; status?: 'success' | 'error';
} }
export function formatMilliseconds(ms: number): string { export function formatMilliseconds(ms: number): string {

View File

@@ -1,45 +1,124 @@
import { FC } from 'react'; import { FC, useEffect } from "react";
import MissionItem from './MissionItem'; import MissionItem from "./MissionItem";
import { Contest } from '../../../redux/slices/contests'; import {
Contest,
fetchMySubmissions,
setContestStatus,
} from "../../../redux/slices/contests";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import { PrimaryButton } from "../../../components/button/PrimaryButton";
import { useNavigate } from "react-router-dom";
import { arrowLeft } from "../../../assets/icons/header";
export interface Article { export interface Article {
id: number; id: number;
name: string; name: string;
tags: string[]; tags: string[];
} }
interface ContestMissionsProps { interface ContestMissionsProps {
contest?: Contest; contest?: Contest;
} }
const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => { const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
if (!contest) { const navigate = useNavigate();
return <></>; const dispatch = useAppDispatch();
} const { submissions, status } = useAppSelector(
(state) => state.contests.fetchMySubmissions
);
return ( useEffect(() => {
<div className=" h-screen grid grid-rows-[74px,1fr] p-[20px] gap-[20px]"> if (contest) dispatch(fetchMySubmissions(contest.id));
<div className=""></div> }, [contest]);
<div className="h-full min-h-0 overflow-y-scroll medium-scrollbar flex flex-col gap-[20px]">
<div className="h-[40px] w-ufll "> useEffect(() => {
{contest?.name} {contest.id} if (status == "successful") {
</div> dispatch(setContestStatus({ key: "fetchMySubmissions", status: "idle" }));
<div className="w-full"> }
{(contest.missions ?? []).map((v, i) => ( }, [status]);
<MissionItem
contestId={contest.id} if (!contest) {
key={i} return <></>;
id={v.id} }
name={v.name}
timeLimit={v.timeLimitMilliseconds} const solvedCount = (contest.missions ?? []).filter((mission) =>
memoryLimit={v.memoryLimitBytes} submissions.some(
type={i % 2 ? 'second' : 'first'} (s) =>
/> s.solution.missionId === mission.id &&
))} s.solution.status === "Accepted: All tests passed"
</div> )
</div> ).length;
const totalCount = contest.missions?.length ?? 0;
return (
<div className=" h-screen grid grid-rows-[74px,40px,1fr] p-[20px] gap-[20px]">
<div className="">
<div className="h-[50px] text-[40px] text-liquid-white font-bold">
{contest.name}
</div> </div>
); <div className="flex justify-between h-[24px] items-center gap-[10px]">
<div className="flex items-center">
<img
src={arrowLeft}
className="cursor-pointer"
onClick={() => {
navigate(`/home/contests`);
}}
/>
<span className="text-liquid-light font-bold text-[18px]">
Контест #{contest.id}
</span>
</div>
<div>{contest.attemptDurationMinutes ?? 0} минут</div>
</div>
</div>
<div className="flex justify-between items-center">
<div className="text-liquid-white text-[16px] font-bold">{`${solvedCount}/${totalCount} Решено`}</div>
<PrimaryButton
onClick={() => {
navigate(`/contest/${contest.id}/submissions`);
}}
text="Мои посылки"
/>
</div>
<div className="h-full min-h-0 overflow-y-scroll medium-scrollbar flex flex-col gap-[20px]">
<div className="w-full">
{(contest.missions ?? []).map((v, i) => {
const missionSubmissions = submissions.filter(
(s) => s.solution.missionId === v.id
);
const hasSuccess = missionSubmissions.some(
(s) => s.solution.status == "Accepted: All tests passed"
);
console.log(missionSubmissions);
const status = hasSuccess
? "success"
: missionSubmissions.length > 0
? "error"
: undefined;
return (
<MissionItem
contestId={contest.id}
key={i}
id={v.id}
name={v.name}
timeLimit={v.timeLimitMilliseconds}
memoryLimit={v.memoryLimitBytes}
status={status}
type={i % 2 ? "second" : "first"}
/>
);
})}
</div>
</div>
</div>
);
}; };
export default ContestMissions; export default ContestMissions;

View File

@@ -4,9 +4,12 @@ import { cn } from '../../../lib/cn';
export interface SubmissionItemProps { export interface SubmissionItemProps {
id: number; id: number;
datetime: string;
missionId: number;
language: string; language: string;
time: string;
verdict: string; verdict: string;
duration: number;
memory: number;
type: 'first' | 'second'; type: 'first' | 'second';
status?: 'success' | 'wronganswer' | 'timelimit'; status?: 'success' | 'wronganswer' | 'timelimit';
} }
@@ -37,20 +40,23 @@ function formatDate(dateString: string): string {
const SubmissionItem: React.FC<SubmissionItemProps> = ({ const SubmissionItem: React.FC<SubmissionItemProps> = ({
id, id,
datetime,
missionId,
language, language,
time,
verdict, verdict,
duration,
memory,
type, type,
status, status
}) => { }) => {
// const navigate = useNavigate(); // const navigate = useNavigate();
return ( return (
<div <div
className={cn( className={cn(
' w-full relative rounded-[10px] text-liquid-white', ' w-full relative rounded-[10px] text-liquid-white text-center text-bold text-[16px] py-[8px]',
type == 'first' ? 'bg-liquid-lighter' : 'bg-liquid-background', type == 'first' ? 'bg-liquid-lighter' : 'bg-liquid-background',
'grid grid-cols-[80px,1fr,1fr,2fr] grid-flow-col gap-[20px] px-[20px] box-border items-center', 'grid grid-cols-7 grid-flow-col gap-[20px] px-[20px] box-border items-center',
status == 'wronganswer' && status == 'wronganswer' &&
'border-l-[11px] border-l-liquid-red pl-[9px]', 'border-l-[11px] border-l-liquid-red pl-[9px]',
status == 'timelimit' && status == 'timelimit' &&
@@ -63,8 +69,9 @@ const SubmissionItem: React.FC<SubmissionItemProps> = ({
> >
<div className="text-[18px] font-bold">#{id}</div> <div className="text-[18px] font-bold">#{id}</div>
<div className="text-[18px] font-bold text-center"> <div className="text-[18px] font-bold text-center">
{formatDate(time)} {formatDate(datetime)}
</div> </div>
<div>{missionId} </div>
<div className="text-[18px] font-bold text-center">{language}</div> <div className="text-[18px] font-bold text-center">{language}</div>
<div <div
className={cn( className={cn(
@@ -75,6 +82,10 @@ const SubmissionItem: React.FC<SubmissionItemProps> = ({
)} )}
> >
{verdict} {verdict}
</div>
<div>{formatMilliseconds(duration)}</div>
<div>
{formatBytesToMB(memory)}
</div> </div>
</div> </div>
); );

View File

@@ -1,73 +1,129 @@
import SubmissionItem from './SubmissionItem'; import SubmissionItem from "./SubmissionItem";
import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import { FC, useEffect } from 'react'; import { FC, useEffect } from "react";
import { fetchMySubmissions, setContestStatus } from '../../../redux/slices/contests'; import {
Contest,
fetchMySubmissions,
setContestStatus,
} from "../../../redux/slices/contests";
import { arrowLeft } from "../../../assets/icons/header";
import { useNavigate } from "react-router-dom";
export interface Mission { export interface Mission {
id: number; id: number;
authorId: number; authorId: number;
name: string; name: string;
difficulty: 'Easy' | 'Medium' | 'Hard'; difficulty: "Easy" | "Medium" | "Hard";
tags: string[]; tags: string[];
timeLimit: number; timeLimit: number;
memoryLimit: number; memoryLimit: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} }
interface SubmissionsProps { interface SubmissionsProps {
contestId: number; contest: Contest;
} }
const Submissions: FC<SubmissionsProps> = ({ contestId }) => { const Submissions: FC<SubmissionsProps> = ({ contest }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigate = useNavigate();
const {submissions, status} = useAppSelector(
(state) => state.contests.fetchMySubmissions
);
useEffect(() => { const { submissions, status } = useAppSelector(
dispatch(fetchMySubmissions(contestId)); (state) => state.contests.fetchMySubmissions
}, [contestId]); );
useEffect(() => { useEffect(() => {
if (status == "successful"){ if (contest && contest.id) dispatch(fetchMySubmissions(contest.id));
dispatch(setContestStatus({key:"fetchMySubmissions", status: "idle"})); }, [contest]);
}
}, [status])
const checkStatus = (status: string) => { useEffect(() => {
if (status == 'IncorrectAnswer') return 'wronganswer'; if (status == "successful") {
if (status == 'TimeLimitError') return 'timelimit'; dispatch(setContestStatus({ key: "fetchMySubmissions", status: "idle" }));
return undefined; }
}; }, [status]);
return ( const checkStatus = (status: string) => {
<div className="h-full w-full box-border overflow-y-scroll overflow-x-hidden thin-scrollbar pr-[10px]"> if (status == "IncorrectAnswer") return "wronganswer";
{submissions && if (status == "TimeLimitError") return "timelimit";
submissions.map((v, i) => ( return undefined;
<SubmissionItem };
key={i}
id={v.id??0} const solvedCount = (contest.missions ?? []).filter((mission) =>
language={v.solution.language} submissions.some(
time={v.solution.time} (s) =>
verdict={ s.solution.missionId === mission.id &&
v.solution.testerMessage?.includes( s.solution.status === "Accepted: All tests passed"
'Compilation failed', )
) ).length;
? 'Compilation failed'
: v.solution.testerMessage const totalCount = contest.missions?.length ?? 0;
}
type={i % 2 ? 'second' : 'first'} return (
status={ <div className="h-full w-[calc(100%+250px)] box-border overflow-y-scroll overflow-x-hidden thin-scrollbar p-[20px] flex flex-col gap-[20px]">
v.solution.testerMessage == 'All tests passed' <div className="">
? 'success' <div className="h-[50px] text-[40px] text-liquid-white font-bold">
: checkStatus(v.solution.testerErrorCode) {contest.name}
}
/>
))}
</div> </div>
); <div className="flex justify-between h-[24px] items-center gap-[10px]">
<div className="flex items-center">
<img
src={arrowLeft}
className="cursor-pointer"
onClick={() => {
navigate(`/contest/${contest.id}`);
}}
/>
<span className="text-liquid-light font-bold text-[18px]">
Контест #{contest.id}
</span>
</div>
<div className="text-liquid-white text-[16px] font-bold">{`${solvedCount}/${totalCount} Решено`}</div>
</div>
</div>
<div>
<div className="grid grid-cols-7 text-center items-center h-[43px] mb-[10px] text-[16px] font-bold text-liquid-white">
<div>Посылка</div>
<div>Когда</div>
<div>Задача</div>
<div>Язык</div>
<div>Вердикт</div>
<div>Время</div>
<div>Память</div>
</div>
{!submissions || submissions.length == 0 ? (
<div className="text-liquid-brightmain text-[16px] font-medium text-center mt-[50px]">Вы еще ничего не отсылали</div>
) : (
<>
{submissions.map((v, i) => (
<SubmissionItem
key={i}
id={v.id ?? 0}
datetime={v.solution.time}
missionId={v.solution.missionId}
language={v.solution.language}
verdict={
v.solution.testerMessage?.includes("Compilation failed")
? "Compilation failed"
: v.solution.testerMessage
}
duration={1000}
memory={256 * 1024 * 1024}
type={i % 2 ? "second" : "first"}
status={
v.solution.testerMessage == "All tests passed"
? "success"
: checkStatus(v.solution.testerErrorCode)
}
/>
))}
</>
)}
</div>
</div>
);
}; };
export default Submissions; export default Submissions;

View File

@@ -1,7 +1,6 @@
import SubmissionItem from './SubmissionItem'; import SubmissionItem from './SubmissionItem';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { useAppSelector } from '../../../redux/hooks';
import { FC, useEffect } from 'react'; import { FC } from 'react';
import { fetchMySubmissions } from '../../../redux/slices/contests';
export interface Mission { export interface Mission {
id: number; id: number;
@@ -21,78 +20,42 @@ interface MissionSubmissionsProps {
} }
const MissionSubmissions: FC<MissionSubmissionsProps> = ({ missionId, contestId }) => { const MissionSubmissions: FC<MissionSubmissionsProps> = ({ missionId, contestId }) => {
const dispatch = useAppDispatch();
const submissions = useAppSelector( const submissions = useAppSelector(
(state) => state.submin.submitsById[missionId], (state) => state.submin.submitsById[missionId] || []
); );
const {submissions: contestSubmission, status: contestStatus} = useAppSelector((state) => state.contests.fetchMySubmissions);
useEffect(() => {}, []);
const checkStatus = (status: string) => { const checkStatus = (status: string) => {
if (status == 'IncorrectAnswer') return 'wronganswer'; if (status === 'IncorrectAnswer') return 'wronganswer';
if (status == 'TimeLimitError') return 'timelimit'; if (status === 'TimeLimitError') return 'timelimit';
return undefined; return undefined;
}; };
// Если contestId передан, фильтруем по нему, иначе показываем все
const filteredSubmissions = contestId
? submissions.filter((v) => v.contestId === contestId)
: submissions;
useEffect(() => {
if (contestId){
dispatch(fetchMySubmissions(contestId));
}
}, [contestId, missionId])
return ( return (
<div className="h-full w-full box-border overflow-y-scroll overflow-x-hidden thin-scrollbar pr-[10px]"> <div className="h-full w-full box-border overflow-y-scroll overflow-x-hidden thin-scrollbar pr-[10px]">
{filteredSubmissions.map((v, i) => (
{contestId ? <SubmissionItem
contestSubmission && key={v.id}
contestSubmission.filter(v => v.solution.missionId == missionId).map((v, i) => ( id={v.id}
<SubmissionItem language={v.solution.language}
key={i} time={v.solution.time}
id={v.id} verdict={
language={v.solution.language} v.solution.testerMessage?.includes('Compilation failed')
time={v.solution.time} ? 'Compilation failed'
verdict={ : v.solution.testerMessage
v.solution.testerMessage?.includes( }
'Compilation failed', type={i % 2 ? 'second' : 'first'}
) status={
? 'Compilation failed' v.solution.testerMessage === 'All tests passed'
: v.solution.testerMessage ? 'success'
} : checkStatus(v.solution.testerErrorCode)
type={i % 2 ? 'second' : 'first'} }
status={ />
v.solution.testerMessage == 'All tests passed' ))}
? 'success'
: checkStatus(v.solution.testerErrorCode)
}
/>
))
:
submissions &&
submissions.map((v, i) => (
<SubmissionItem
key={i}
id={v.id}
language={v.solution.language}
time={v.solution.time}
verdict={
v.solution.testerMessage?.includes(
'Compilation failed',
)
? 'Compilation failed'
: v.solution.testerMessage
}
type={i % 2 ? 'second' : 'first'}
status={
v.solution.testerMessage == 'All tests passed'
? 'success'
: checkStatus(v.solution.testerErrorCode)
}
/>
))
}
</div> </div>
); );
}; };