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,17 +1,64 @@
import { useEffect } from 'react';
import { useAppDispatch } from '../../../../redux/hooks';
import { setMenuActiveProfilePage } from '../../../../redux/slices/store';
import { FC, useEffect } from "react";
import { useAppDispatch } from "../../../../redux/hooks";
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 dispatch = useAppDispatch();
useEffect(() => {
dispatch(setMenuActiveProfilePage('missions'));
dispatch(setMenuActiveProfilePage("missions"));
}, []);
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>
);
};

View File

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

View File

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

View File

@@ -1,6 +1,14 @@
import { FC } from 'react';
import MissionItem from './MissionItem';
import { Contest } from '../../../redux/slices/contests';
import { FC, useEffect } from "react";
import MissionItem from "./MissionItem";
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 {
id: number;
@@ -13,19 +21,88 @@ interface ContestMissionsProps {
}
const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { submissions, status } = useAppSelector(
(state) => state.contests.fetchMySubmissions
);
useEffect(() => {
if (contest) dispatch(fetchMySubmissions(contest.id));
}, [contest]);
useEffect(() => {
if (status == "successful") {
dispatch(setContestStatus({ key: "fetchMySubmissions", status: "idle" }));
}
}, [status]);
if (!contest) {
return <></>;
}
const solvedCount = (contest.missions ?? []).filter((mission) =>
submissions.some(
(s) =>
s.solution.missionId === mission.id &&
s.solution.status === "Accepted: All tests passed"
)
).length;
const totalCount = contest.missions?.length ?? 0;
return (
<div className=" h-screen grid grid-rows-[74px,1fr] p-[20px] gap-[20px]">
<div className=""></div>
<div className="h-full min-h-0 overflow-y-scroll medium-scrollbar flex flex-col gap-[20px]">
<div className="h-[40px] w-ufll ">
{contest?.name} {contest.id}
<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 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) => (
{(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}
@@ -33,9 +110,11 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
name={v.name}
timeLimit={v.timeLimitMilliseconds}
memoryLimit={v.memoryLimitBytes}
type={i % 2 ? 'second' : 'first'}
status={status}
type={i % 2 ? "second" : "first"}
/>
))}
);
})}
</div>
</div>
</div>

View File

@@ -4,9 +4,12 @@ import { cn } from '../../../lib/cn';
export interface SubmissionItemProps {
id: number;
datetime: string;
missionId: number;
language: string;
time: string;
verdict: string;
duration: number;
memory: number;
type: 'first' | 'second';
status?: 'success' | 'wronganswer' | 'timelimit';
}
@@ -37,20 +40,23 @@ function formatDate(dateString: string): string {
const SubmissionItem: React.FC<SubmissionItemProps> = ({
id,
datetime,
missionId,
language,
time,
verdict,
duration,
memory,
type,
status,
status
}) => {
// const navigate = useNavigate();
return (
<div
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',
'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' &&
'border-l-[11px] border-l-liquid-red pl-[9px]',
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 text-center">
{formatDate(time)}
{formatDate(datetime)}
</div>
<div>{missionId} </div>
<div className="text-[18px] font-bold text-center">{language}</div>
<div
className={cn(
@@ -76,6 +83,10 @@ const SubmissionItem: React.FC<SubmissionItemProps> = ({
>
{verdict}
</div>
<div>{formatMilliseconds(duration)}</div>
<div>
{formatBytesToMB(memory)}
</div>
</div>
);
};

View File

@@ -1,13 +1,19 @@
import SubmissionItem from './SubmissionItem';
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
import { FC, useEffect } from 'react';
import { fetchMySubmissions, setContestStatus } from '../../../redux/slices/contests';
import SubmissionItem from "./SubmissionItem";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import { FC, useEffect } from "react";
import {
Contest,
fetchMySubmissions,
setContestStatus,
} from "../../../redux/slices/contests";
import { arrowLeft } from "../../../assets/icons/header";
import { useNavigate } from "react-router-dom";
export interface Mission {
id: number;
authorId: number;
name: string;
difficulty: 'Easy' | 'Medium' | 'Hard';
difficulty: "Easy" | "Medium" | "Hard";
tags: string[];
timeLimit: number;
memoryLimit: number;
@@ -16,56 +22,106 @@ export interface Mission {
}
interface SubmissionsProps {
contestId: number;
contest: Contest;
}
const Submissions: FC<SubmissionsProps> = ({ contestId }) => {
const Submissions: FC<SubmissionsProps> = ({ contest }) => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { submissions, status } = useAppSelector(
(state) => state.contests.fetchMySubmissions
);
useEffect(() => {
dispatch(fetchMySubmissions(contestId));
}, [contestId]);
if (contest && contest.id) dispatch(fetchMySubmissions(contest.id));
}, [contest]);
useEffect(() => {
if (status == "successful") {
dispatch(setContestStatus({ key: "fetchMySubmissions", status: "idle" }));
}
}, [status])
}, [status]);
const checkStatus = (status: string) => {
if (status == 'IncorrectAnswer') return 'wronganswer';
if (status == 'TimeLimitError') return 'timelimit';
if (status == "IncorrectAnswer") return "wronganswer";
if (status == "TimeLimitError") return "timelimit";
return undefined;
};
const solvedCount = (contest.missions ?? []).filter((mission) =>
submissions.some(
(s) =>
s.solution.missionId === mission.id &&
s.solution.status === "Accepted: All tests passed"
)
).length;
const totalCount = contest.missions?.length ?? 0;
return (
<div className="h-full w-full box-border overflow-y-scroll overflow-x-hidden thin-scrollbar pr-[10px]">
{submissions &&
submissions.map((v, i) => (
<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]">
<div className="">
<div className="h-[50px] text-[40px] text-liquid-white font-bold">
{contest.name}
</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}
time={v.solution.time}
verdict={
v.solution.testerMessage?.includes(
'Compilation failed',
)
? 'Compilation failed'
v.solution.testerMessage?.includes("Compilation failed")
? "Compilation failed"
: v.solution.testerMessage
}
type={i % 2 ? 'second' : 'first'}
duration={1000}
memory={256 * 1024 * 1024}
type={i % 2 ? "second" : "first"}
status={
v.solution.testerMessage == 'All tests passed'
? 'success'
v.solution.testerMessage == "All tests passed"
? "success"
: checkStatus(v.solution.testerErrorCode)
}
/>
))}
</>
)}
</div>
</div>
);
};

View File

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