diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx index ca1953e..cf02e94 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx @@ -1,14 +1,19 @@ import React from 'react'; import { Button } from "@/components/ui/button"; +import { Loader2 } from "lucide-react"; interface ActionButtonsProps { onSubmit: () => void; onHistoryClick: () => void; + isSubmitting?: boolean; + hasSubmissionsLeft?: boolean; } const ActionButtons: React.FC = ({ onSubmit, - onHistoryClick + onHistoryClick, + isSubmitting = false, + hasSubmissionsLeft = true }) => { return (
@@ -16,15 +21,31 @@ const ActionButtons: React.FC = ({ variant="ghost" className="font-hse-sans bg-white hover:bg-gray-100" onClick={onHistoryClick} + disabled={isSubmitting} > История - + + {hasSubmissionsLeft ? ( + + ) : ( +
+ Лимит посылок исчерпан +
+ )}
); }; diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx index 59c883e..3a5ae53 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx @@ -68,6 +68,7 @@ const FileSolution: React.FC = ({ ? existingFileUrl.split('/').pop() || 'file' : ''; + console.log(firstSolution, "IS FIRST SOLUTION") const hasFile = !!selectedFile || (!!existingFileUrl && !firstSolution); return ( diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index e7233d5..1bf87d1 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -1,170 +1,200 @@ - import React, { useState, useRef, useEffect } from 'react'; - import { useParams } from 'react-router-dom'; - import { Task, TaskType, Solution } from '@/shared/types/task'; - import { useQuery } from '@tanstack/react-query'; - import { getTaskSolutionHistory } from '@/shared/api/session'; - import SolutionStatus from './components/SolutionStatus'; - import InputSolution from './components/InputSolution'; - import FileSolution from './components/FileSolution'; - import CodeSolution from './components/CodeSolution'; - import ActionButtons from './components/ActionButtons'; - import SolutionHistorySheet from './components/SolutionHistorySheet'; +import React, { useState, useRef, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { Task, TaskType, Solution } from '@/shared/types/task'; +import { useQuery } from '@tanstack/react-query'; +import { getTaskSolutionHistory } from '@/shared/api/session'; +import SolutionStatus from './components/SolutionStatus'; +import InputSolution from './components/InputSolution'; +import FileSolution from './components/FileSolution'; +import CodeSolution from './components/CodeSolution'; +import ActionButtons from './components/ActionButtons'; +import SolutionHistorySheet from './components/SolutionHistorySheet'; - interface TaskSolutionProps { - task: Task; - answer: string; - setAnswer: (value: string) => void; - selectedFile: File | null; - setSelectedFile: (file: File | null) => void; - onSubmit: () => void; - } +interface TaskSolutionProps { + task: Task; + answer: string; + setAnswer: (value: string) => void; + selectedFile: File | null; + setSelectedFile: (file: File | null) => void; + onSubmit: () => void; + isSubmitting?: boolean; +} - const TaskSolution: React.FC = ({ - task, - answer, - setAnswer, - selectedFile, - setSelectedFile, - onSubmit, - }) => { - const fileInputRef = useRef(null); - const [isHistoryOpen, setIsHistoryOpen] = useState(false); - const [selectedSolutionUrl, setSelectedSolutionUrl] = useState(null); - const [displayedSolution, setDisplayedSolution] = useState(null); - const { id: competitionId } = useParams<{ id: string }>(); - const prevTaskIdRef = useRef(null); +const TaskSolution: React.FC = ({ + task, + answer, + setAnswer, + selectedFile, + setSelectedFile, + onSubmit, + isSubmitting = false +}) => { + const fileInputRef = useRef(null); + const [isHistoryOpen, setIsHistoryOpen] = useState(false); + const [selectedSolutionUrl, setSelectedSolutionUrl] = useState(null); + const [displayedSolution, setDisplayedSolution] = useState(null); + const { id: competitionId } = useParams<{ id: string }>(); + const prevTaskIdRef = useRef(null); - const solutionsQuery = useQuery({ - queryKey: ['solutionHistory', competitionId, task.id], - queryFn: () => getTaskSolutionHistory(competitionId || '', task.id), - enabled: !!(competitionId && task.id), - }); + const solutionsQuery = useQuery({ + queryKey: ['solutionHistory', competitionId, task.id], + queryFn: () => getTaskSolutionHistory(competitionId || '', task.id), + enabled: !!(competitionId && task.id), + }); - const solutionHistory = solutionsQuery.data || []; + const solutionHistory = solutionsQuery.data || []; + + const maxAttempts = task.max_attempts || -1; + const submissionsUsed = solutionHistory.length; + const submissionsLeft = Math.max(0, maxAttempts - submissionsUsed); + const hasSubmissionsLeft = submissionsLeft > 0; - useEffect(() => { - if (solutionHistory.length > 0 && !displayedSolution) { + useEffect(() => { + if (solutionHistory.length > 0 && !displayedSolution) { + const latestSolution = solutionHistory[solutionHistory.length - 1]; + setDisplayedSolution(latestSolution); + } + }, [solutionHistory, displayedSolution]); + + useEffect(() => { + if (prevTaskIdRef.current !== task.id) { + setDisplayedSolution(null); + setSelectedSolutionUrl(null); + + if (solutionHistory.length > 0) { const latestSolution = solutionHistory[solutionHistory.length - 1]; setDisplayedSolution(latestSolution); } - }, [solutionHistory, displayedSolution]); + + prevTaskIdRef.current = task.id; + } + }, [task.id, solutionHistory]); - useEffect(() => { - if (prevTaskIdRef.current !== task.id) { - setDisplayedSolution(null); - setSelectedSolutionUrl(null); - - if (solutionHistory.length > 0) { - const latestSolution = solutionHistory[solutionHistory.length - 1]; - setDisplayedSolution(latestSolution); - } - - prevTaskIdRef.current = task.id; + useEffect(() => { + if (solutionHistory.length > 0 && displayedSolution) { + const latestSolution = solutionHistory[solutionHistory.length - 1]; + + if (latestSolution.id !== displayedSolution.id) { + setDisplayedSolution(latestSolution); } - }, [task.id, solutionHistory]); + } + }, [solutionHistory, displayedSolution]); - useEffect(() => { - if (solutionHistory.length > 0 && displayedSolution) { - const latestSolution = solutionHistory[solutionHistory.length - 1]; - - if (latestSolution.id !== displayedSolution.id) { - setDisplayedSolution(latestSolution); - } - } - }, [solutionHistory, displayedSolution]); - - // Load solution content when the displayed solution changes - useEffect(() => { - const loadSolutionContent = async () => { - if (!displayedSolution || !displayedSolution.content) return; - - try { - if (task.type === TaskType.FILE) { - setSelectedFile(null); - setSelectedSolutionUrl(displayedSolution.content); - } else { - 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); + useEffect(() => { + const loadSolutionContent = async () => { + if (!displayedSolution || !displayedSolution.content) return; + + try { + if (task.type === TaskType.FILE) { + setSelectedFile(null); + setSelectedSolutionUrl(displayedSolution.content); + } else { + const response = await fetch(displayedSolution.content); + if (!response.ok) { + throw new Error(`Failed to fetch solution content: ${response.status}`); } - } catch (error) { - console.error('Error loading solution content:', error); + const text = await response.text(); + setAnswer(text); } - }; - - loadSolutionContent(); - }, [displayedSolution, task.type, setAnswer, setSelectedFile]); - - const handleOpenHistory = () => { - setIsHistoryOpen(true); + } catch (error) { + console.error('Error loading solution content:', error); + } }; - const handleSolutionSelect = (solution: Solution) => { - setDisplayedSolution(solution); - }; + loadSolutionContent(); + }, [displayedSolution, task.type, setAnswer, setSelectedFile]); - const handleClearExistingFile = () => { - setSelectedSolutionUrl(null); - }; - - return ( -
- {displayedSolution ? ( - <> -
- Последнее решение -
- - - - ) : ( -
- Решение еще не отправлено -
- )} - - {task.type === TaskType.INPUT && ( - - )} - - {task.type === TaskType.FILE && ( - 0} - /> - )} - - {task.type === TaskType.CODE && ( - - )} - - - - -
- ); + const handleOpenHistory = () => { + setIsHistoryOpen(true); }; - export default TaskSolution; \ No newline at end of file + const handleSolutionSelect = (solution: Solution) => { + setDisplayedSolution(solution); + }; + + const handleClearExistingFile = () => { + setSelectedSolutionUrl(null); + }; + + return ( +
+ {displayedSolution ? ( + <> +
+ Результат последней посылки: +
+ + + ) : ( +
+ Решение еще не отправлено +
+ )} + + {task.type === TaskType.INPUT && ( + + )} + + {task.type === TaskType.FILE && ( + 0} + /> + )} + + {task.type === TaskType.CODE && ( + + )} + +
+ {hasSubmissionsLeft ? ( + <> + + Осталось посылок: {submissionsLeft === Infinity ? '∞' : submissionsLeft} + + {maxAttempts !== -1 && ( + + (из {maxAttempts}) + + )} + + ) : ( + + Вы использовали все посылки + + )} +
+ + + + +
+ ); +}; + +export default TaskSolution; \ No newline at end of file diff --git a/services/frontend/src/shared/types/task.ts b/services/frontend/src/shared/types/task.ts index 64e829e..54a68e5 100644 --- a/services/frontend/src/shared/types/task.ts +++ b/services/frontend/src/shared/types/task.ts @@ -5,6 +5,7 @@ interface Task { type: TaskType; in_competition_position: number; points: number; + max_attempts: number; } export interface TaskAttachment {