mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-23 05:07:10 +00:00
Merge branch 'master' of gitlab.prodcontest.ru:team-15/project
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, Clock, Trophy, BookOpen } from "lucide-react";
|
||||
import { ArrowLeft, Clock, Trophy, BookOpen, BarChart2, AlertCircle } from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { getCompetition, startCompetition } from "@/shared/api/competitions";
|
||||
import { getCompetitionTasks } from "@/shared/api/session";
|
||||
import { Loading } from "@/components/ui/loading";
|
||||
import { CompetitionType } from "@/shared/types/competition";
|
||||
import remarkMath from "remark-math";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
|
||||
const CompetitionPage = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -39,6 +42,7 @@ const CompetitionPage = () => {
|
||||
console.error("Failed to start competition:", error);
|
||||
}
|
||||
});
|
||||
|
||||
const formatDate = (date?: Date | string) => {
|
||||
if (!date) return "";
|
||||
|
||||
@@ -56,6 +60,10 @@ const CompetitionPage = () => {
|
||||
const handleStart = () => {
|
||||
startMutation.mutate();
|
||||
};
|
||||
|
||||
const handleViewResults = () => {
|
||||
console.log("sorryan");
|
||||
};
|
||||
|
||||
if (competitionQuery.isLoading) {
|
||||
return <Loading />;
|
||||
@@ -66,6 +74,27 @@ const CompetitionPage = () => {
|
||||
}
|
||||
|
||||
const competition = competitionQuery.data;
|
||||
|
||||
const isCompetitionEnded = () => {
|
||||
if (!competition?.end_date) return false;
|
||||
|
||||
const endDate = new Date(competition.end_date);
|
||||
const now = new Date();
|
||||
|
||||
return now > endDate;
|
||||
};
|
||||
|
||||
const isCompetitionNotStarted = () => {
|
||||
if (!competition?.start_date) return false;
|
||||
|
||||
const startDate = new Date(competition.start_date);
|
||||
const now = new Date();
|
||||
|
||||
return now < startDate;
|
||||
};
|
||||
|
||||
const competitionEnded = isCompetitionEnded();
|
||||
const competitionNotStarted = isCompetitionNotStarted();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -103,6 +132,18 @@ const CompetitionPage = () => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{competitionEnded && competition.type === CompetitionType.COMPETITIVE && (
|
||||
<div className="bg-red-100 text-red-700 px-3 py-1 rounded-full text-sm font-medium">
|
||||
Завершено
|
||||
</div>
|
||||
)}
|
||||
|
||||
{competitionNotStarted && competition.type === CompetitionType.COMPETITIVE && (
|
||||
<div className="bg-yellow-100 text-yellow-700 px-3 py-1 rounded-full text-sm font-medium">
|
||||
Скоро начнется
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className="text-[34px] leading-11 font-semibold text-balance">
|
||||
@@ -128,18 +169,43 @@ const CompetitionPage = () => {
|
||||
</div>
|
||||
|
||||
<div className="prose prose-lg max-w-none text-xl leading-10 font-normal">
|
||||
<ReactMarkdown>{competition.description || ""}</ReactMarkdown>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkMath, remarkGfm]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
>
|
||||
{competition.description || ""}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full *:w-full md:w-96">
|
||||
<Button
|
||||
size={"lg"}
|
||||
onClick={handleStart}
|
||||
disabled={startMutation.isPending}
|
||||
>
|
||||
{startMutation.isPending ? "Загрузка..." : "Приступить к выполнению"}
|
||||
</Button>
|
||||
{competitionEnded && competition.type === CompetitionType.COMPETITIVE ? (
|
||||
<Button
|
||||
size={"lg"}
|
||||
onClick={handleViewResults}
|
||||
className="bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
<BarChart2 size={18} className="mr-2" />
|
||||
Смотреть результаты
|
||||
</Button>
|
||||
) : competitionNotStarted && competition.type === CompetitionType.COMPETITIVE ? (
|
||||
<Button
|
||||
size={"lg"}
|
||||
disabled={true}
|
||||
className="bg-gray-200 text-gray-500 cursor-not-allowed"
|
||||
>
|
||||
<AlertCircle size={18} className="mr-2" />
|
||||
Скоро начнется
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size={"lg"}
|
||||
onClick={handleStart}
|
||||
disabled={startMutation.isPending}
|
||||
>
|
||||
{startMutation.isPending ? "Загрузка..." : "Приступить к выполнению"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+92
-19
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Task } from '@/shared/types/task';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Clock } from 'lucide-react';
|
||||
import { CompetitionType } from '@/shared/types/competition';
|
||||
|
||||
interface CompetitionHeaderProps {
|
||||
title: string;
|
||||
@@ -10,6 +10,9 @@ interface CompetitionHeaderProps {
|
||||
competitionId: string;
|
||||
setAnswer: (value: string) => void;
|
||||
setSelectedFile: (file: File | null) => void;
|
||||
competitionType?: CompetitionType;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
}
|
||||
|
||||
const CompetitionHeader: React.FC<CompetitionHeaderProps> = ({
|
||||
@@ -17,33 +20,103 @@ const CompetitionHeader: React.FC<CompetitionHeaderProps> = ({
|
||||
tasks,
|
||||
competitionId,
|
||||
setAnswer,
|
||||
setSelectedFile
|
||||
setSelectedFile,
|
||||
competitionType,
|
||||
startDate,
|
||||
endDate
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [timeLeft, setTimeLeft] = useState<string>('');
|
||||
|
||||
const handleTaskSelect = (taskId: string) => {
|
||||
setAnswer("");
|
||||
setSelectedFile(null);
|
||||
console.log("SETTER ERROR")
|
||||
navigate(`/competition/${competitionId}/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
|
||||
const formatDate = (date?: Date) => {
|
||||
if (!date) return '';
|
||||
|
||||
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
||||
return dateObj.toLocaleString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!endDate || competitionType !== CompetitionType.COMPETITIVE) return;
|
||||
|
||||
const endDateObj = typeof endDate === 'string' ? new Date(endDate) : endDate;
|
||||
|
||||
const updateTimer = () => {
|
||||
const now = new Date();
|
||||
const diff = endDateObj.getTime() - now.getTime();
|
||||
|
||||
if (diff <= 0) {
|
||||
navigate(`/competition/${competitionId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
||||
|
||||
setTimeLeft(`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`);
|
||||
};
|
||||
|
||||
updateTimer();
|
||||
const timerInterval = setInterval(updateTimer, 1000);
|
||||
|
||||
return () => clearInterval(timerInterval);
|
||||
}, [endDate, competitionId, navigate, competitionType]);
|
||||
|
||||
const showTimeSection = competitionType === CompetitionType.COMPETITIVE && (startDate || endDate);
|
||||
|
||||
return (
|
||||
<header className="bg-white shadow-sm sticky top-0 z-30 w-full">
|
||||
<div className="mx-auto max-w-6xl px-4">
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<Link
|
||||
to={`/competition/${competitionId}`}
|
||||
className="flex items-center text-gray-600 hover:text-gray-900 transition-colors font-hse-sans text-sm"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
</Link>
|
||||
<div>
|
||||
<Link
|
||||
to={`/competition/${competitionId}`}
|
||||
className="flex items-center text-gray-600 hover:text-gray-900 transition-colors font-hse-sans text-sm"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
</Link>
|
||||
|
||||
<h1 className="font-hse-sans text-xl font-semibold text-center flex-1">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<h1 className="font-hse-sans text-xl font-semibold text-center flex-1">
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
<div className="w-[70px]"></div>
|
||||
{showTimeSection ? (
|
||||
<div className="flex items-center text-gray-600 font-hse-sans text-sm">
|
||||
<div className="flex flex-col items-end">
|
||||
{startDate && (
|
||||
<span className="text-xs text-gray-500">
|
||||
Начало: {formatDate(startDate)}
|
||||
</span>
|
||||
)}
|
||||
{endDate && (
|
||||
<span className="text-xs text-gray-500">
|
||||
Конец: {formatDate(endDate)}
|
||||
</span>
|
||||
)}
|
||||
{timeLeft && (
|
||||
<span className="font-medium text-red-600">
|
||||
Осталось: {timeLeft}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-[70px]"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-4 pb-4 overflow-x-auto no-scrollbar">
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkMath from 'remark-math';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import { Task } from '@/shared/types/task';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@@ -24,18 +25,27 @@ const TaskContent: React.FC<TaskContentProps> = ({ task }) => {
|
||||
|
||||
const attachments = attachmentsQuery.data || [];
|
||||
|
||||
const convertToMarkdown = (text: string): string => {
|
||||
if (!text) return '';
|
||||
|
||||
let markdown = text.replace(/\n/g, '\n\n');
|
||||
return markdown;
|
||||
};
|
||||
|
||||
const markdownText = convertToMarkdown(task.description);
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-white rounded-lg p-6">
|
||||
<h2 className="text-3xl font-semibold mb-6 font-hse-sans">
|
||||
Задача {task.in_competition_position}
|
||||
{task.title}
|
||||
</h2>
|
||||
|
||||
<div className="prose prose-lg max-w-none text-gray-700 font-hse-sans mb-6">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkMath]}
|
||||
remarkPlugins={[remarkMath, remarkGfm]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
>
|
||||
{task.description}
|
||||
{markdownText}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -97,6 +97,9 @@ const CompetitionSession = () => {
|
||||
competitionId={competitionId}
|
||||
setAnswer={setAnswer}
|
||||
setSelectedFile={setSelectedFile}
|
||||
competitionType={competition?.type}
|
||||
startDate={competition?.start_date}
|
||||
endDate={competition?.end_date}
|
||||
/>
|
||||
|
||||
<main className="flex-1 bg-[#F8F8F8] pb-8">
|
||||
@@ -120,6 +123,7 @@ const CompetitionSession = () => {
|
||||
selectedFile={selectedFile}
|
||||
setSelectedFile={setSelectedFile}
|
||||
onSubmit={handleSubmit}
|
||||
isSubmitting={submitMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
+1
-3
@@ -8,7 +8,6 @@ interface FileSolutionProps {
|
||||
fileInputRef: React.RefObject<HTMLInputElement>;
|
||||
existingFileUrl?: string | null;
|
||||
onClearExistingFile?: () => void; // New prop to clear existing file URL
|
||||
firstSolution: boolean
|
||||
}
|
||||
|
||||
const FileSolution: React.FC<FileSolutionProps> = ({
|
||||
@@ -17,7 +16,6 @@ const FileSolution: React.FC<FileSolutionProps> = ({
|
||||
fileInputRef,
|
||||
existingFileUrl = null,
|
||||
onClearExistingFile,
|
||||
firstSolution
|
||||
}) => {
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
@@ -68,7 +66,7 @@ const FileSolution: React.FC<FileSolutionProps> = ({
|
||||
? existingFileUrl.split('/').pop() || 'file'
|
||||
: '';
|
||||
|
||||
const hasFile = !!selectedFile || (!!existingFileUrl && !firstSolution);
|
||||
const hasFile = !!selectedFile || !!existingFileUrl;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -54,7 +54,7 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
||||
const latestSolution = solutionHistory[solutionHistory.length - 1];
|
||||
setDisplayedSolution(latestSolution);
|
||||
}
|
||||
}, [solutionHistory, displayedSolution]);
|
||||
}, [solutionHistory]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevTaskIdRef.current !== task.id) {
|
||||
@@ -70,38 +70,41 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
||||
}
|
||||
}, [task.id, solutionHistory]);
|
||||
|
||||
useEffect(() => {
|
||||
if (solutionHistory.length > 0 &&
|
||||
(!displayedSolution ||
|
||||
(solutionHistory[solutionHistory.length - 1].id !== displayedSolution.id &&
|
||||
displayedSolution.id === solutionHistory[solutionHistory.length - 2]?.id))) {
|
||||
setDisplayedSolution(solutionHistory[solutionHistory.length - 1]);
|
||||
}
|
||||
}, [solutionHistory, displayedSolution]);
|
||||
// useEffect(() => {
|
||||
// if (solutionHistory.length > 0 &&
|
||||
// (!displayedSolution ||
|
||||
// (solutionHistory[solutionHistory.length - 1].id !== displayedSolution.id))) {
|
||||
// setDisplayedSolution(solutionHistory[solutionHistory.length - 1]);
|
||||
// }
|
||||
// }, [solutionHistory, displayedSolution]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadSolutionContent = async () => {
|
||||
if (!displayedSolution || !displayedSolution.content) return;
|
||||
|
||||
try {
|
||||
if (task.type === TaskType.FILE) {
|
||||
setAnswer("");
|
||||
setSelectedFile(null);
|
||||
setSelectedSolutionUrl(displayedSolution.content);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
setSelectedFile(null);
|
||||
setSelectedSolutionUrl(null);
|
||||
const response = await fetch(displayedSolution.content);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch solution content: ${response.status}`);
|
||||
}
|
||||
const text = await response.text();
|
||||
|
||||
setAnswer(text);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading solution content:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
loadSolutionContent();
|
||||
}, [displayedSolution, task.type, setAnswer, setSelectedFile]);
|
||||
}, [displayedSolution, setAnswer, setSelectedFile]);
|
||||
|
||||
const handleOpenHistory = () => {
|
||||
setIsHistoryOpen(true);
|
||||
@@ -144,7 +147,6 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
|
||||
fileInputRef={fileInputRef}
|
||||
existingFileUrl={selectedSolutionUrl}
|
||||
onClearExistingFile={handleClearExistingFile}
|
||||
firstSolution={solutionHistory.length > 0}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user