added submissions count

This commit is contained in:
rngsurrounded
2025-03-03 21:09:57 +09:00
parent 515b051587
commit 39143116c4
4 changed files with 212 additions and 159 deletions
@@ -1,14 +1,19 @@
import React from 'react'; import React from 'react';
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
interface ActionButtonsProps { interface ActionButtonsProps {
onSubmit: () => void; onSubmit: () => void;
onHistoryClick: () => void; onHistoryClick: () => void;
isSubmitting?: boolean;
hasSubmissionsLeft?: boolean;
} }
const ActionButtons: React.FC<ActionButtonsProps> = ({ const ActionButtons: React.FC<ActionButtonsProps> = ({
onSubmit, onSubmit,
onHistoryClick onHistoryClick,
isSubmitting = false,
hasSubmissionsLeft = true
}) => { }) => {
return ( return (
<div className="flex gap-8"> <div className="flex gap-8">
@@ -16,15 +21,31 @@ const ActionButtons: React.FC<ActionButtonsProps> = ({
variant="ghost" variant="ghost"
className="font-hse-sans bg-white hover:bg-gray-100" className="font-hse-sans bg-white hover:bg-gray-100"
onClick={onHistoryClick} onClick={onHistoryClick}
disabled={isSubmitting}
> >
История История
</Button> </Button>
<Button
onClick={onSubmit} {hasSubmissionsLeft ? (
className="font-hse-sans flex-grow" <Button
> onClick={onSubmit}
Отправить решение className="font-hse-sans flex-grow"
</Button> disabled={isSubmitting}
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Отправка...
</>
) : (
"Отправить решение"
)}
</Button>
) : (
<div className="flex-grow text-right text-gray-500 flex items-center justify-end font-hse-sans">
Лимит посылок исчерпан
</div>
)}
</div> </div>
); );
}; };
@@ -68,6 +68,7 @@ const FileSolution: React.FC<FileSolutionProps> = ({
? existingFileUrl.split('/').pop() || 'file' ? existingFileUrl.split('/').pop() || 'file'
: ''; : '';
console.log(firstSolution, "IS FIRST SOLUTION")
const hasFile = !!selectedFile || (!!existingFileUrl && !firstSolution); const hasFile = !!selectedFile || (!!existingFileUrl && !firstSolution);
return ( return (
@@ -1,170 +1,200 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Task, TaskType, Solution } from '@/shared/types/task'; import { Task, TaskType, Solution } from '@/shared/types/task';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { getTaskSolutionHistory } from '@/shared/api/session'; import { getTaskSolutionHistory } from '@/shared/api/session';
import SolutionStatus from './components/SolutionStatus'; import SolutionStatus from './components/SolutionStatus';
import InputSolution from './components/InputSolution'; import InputSolution from './components/InputSolution';
import FileSolution from './components/FileSolution'; import FileSolution from './components/FileSolution';
import CodeSolution from './components/CodeSolution'; import CodeSolution from './components/CodeSolution';
import ActionButtons from './components/ActionButtons'; import ActionButtons from './components/ActionButtons';
import SolutionHistorySheet from './components/SolutionHistorySheet'; import SolutionHistorySheet from './components/SolutionHistorySheet';
interface TaskSolutionProps { interface TaskSolutionProps {
task: Task; task: Task;
answer: string; answer: string;
setAnswer: (value: string) => void; setAnswer: (value: string) => void;
selectedFile: File | null; selectedFile: File | null;
setSelectedFile: (file: File | null) => void; setSelectedFile: (file: File | null) => void;
onSubmit: () => void; onSubmit: () => void;
} isSubmitting?: boolean;
}
const TaskSolution: React.FC<TaskSolutionProps> = ({ const TaskSolution: React.FC<TaskSolutionProps> = ({
task, task,
answer, answer,
setAnswer, setAnswer,
selectedFile, selectedFile,
setSelectedFile, setSelectedFile,
onSubmit, onSubmit,
}) => { isSubmitting = false
const fileInputRef = useRef<HTMLInputElement>(null); }) => {
const [isHistoryOpen, setIsHistoryOpen] = useState(false); const fileInputRef = useRef<HTMLInputElement>(null);
const [selectedSolutionUrl, setSelectedSolutionUrl] = useState<string | null>(null); const [isHistoryOpen, setIsHistoryOpen] = useState(false);
const [displayedSolution, setDisplayedSolution] = useState<Solution | null>(null); const [selectedSolutionUrl, setSelectedSolutionUrl] = useState<string | null>(null);
const { id: competitionId } = useParams<{ id: string }>(); const [displayedSolution, setDisplayedSolution] = useState<Solution | null>(null);
const prevTaskIdRef = useRef<string | null>(null); const { id: competitionId } = useParams<{ id: string }>();
const prevTaskIdRef = useRef<string | null>(null);
const solutionsQuery = useQuery({ const solutionsQuery = useQuery({
queryKey: ['solutionHistory', competitionId, task.id], queryKey: ['solutionHistory', competitionId, task.id],
queryFn: () => getTaskSolutionHistory(competitionId || '', task.id), queryFn: () => getTaskSolutionHistory(competitionId || '', task.id),
enabled: !!(competitionId && task.id), enabled: !!(competitionId && task.id),
}); });
const solutionHistory = solutionsQuery.data || []; const solutionHistory = solutionsQuery.data || [];
useEffect(() => { const maxAttempts = task.max_attempts || -1;
if (solutionHistory.length > 0 && !displayedSolution) { const submissionsUsed = solutionHistory.length;
const submissionsLeft = Math.max(0, maxAttempts - submissionsUsed);
const hasSubmissionsLeft = submissionsLeft > 0;
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]; const latestSolution = solutionHistory[solutionHistory.length - 1];
setDisplayedSolution(latestSolution); setDisplayedSolution(latestSolution);
} }
}, [solutionHistory, displayedSolution]);
useEffect(() => { prevTaskIdRef.current = task.id;
if (prevTaskIdRef.current !== task.id) { }
setDisplayedSolution(null); }, [task.id, solutionHistory]);
setSelectedSolutionUrl(null);
if (solutionHistory.length > 0) { useEffect(() => {
const latestSolution = solutionHistory[solutionHistory.length - 1]; if (solutionHistory.length > 0 && displayedSolution) {
setDisplayedSolution(latestSolution); const latestSolution = solutionHistory[solutionHistory.length - 1];
}
prevTaskIdRef.current = task.id; if (latestSolution.id !== displayedSolution.id) {
setDisplayedSolution(latestSolution);
} }
}, [task.id, solutionHistory]); }
}, [solutionHistory, displayedSolution]);
useEffect(() => { useEffect(() => {
if (solutionHistory.length > 0 && displayedSolution) { const loadSolutionContent = async () => {
const latestSolution = solutionHistory[solutionHistory.length - 1]; if (!displayedSolution || !displayedSolution.content) return;
if (latestSolution.id !== displayedSolution.id) { try {
setDisplayedSolution(latestSolution); if (task.type === TaskType.FILE) {
} setSelectedFile(null);
} setSelectedSolutionUrl(displayedSolution.content);
}, [solutionHistory, displayedSolution]); } else {
const response = await fetch(displayedSolution.content);
// Load solution content when the displayed solution changes if (!response.ok) {
useEffect(() => { throw new Error(`Failed to fetch solution content: ${response.status}`);
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);
} }
} catch (error) { const text = await response.text();
console.error('Error loading solution content:', error); setAnswer(text);
} }
}; } catch (error) {
console.error('Error loading solution content:', error);
loadSolutionContent(); }
}, [displayedSolution, task.type, setAnswer, setSelectedFile]);
const handleOpenHistory = () => {
setIsHistoryOpen(true);
}; };
const handleSolutionSelect = (solution: Solution) => { loadSolutionContent();
setDisplayedSolution(solution); }, [displayedSolution, task.type, setAnswer, setSelectedFile]);
};
const handleClearExistingFile = () => { const handleOpenHistory = () => {
setSelectedSolutionUrl(null); setIsHistoryOpen(true);
};
return (
<div className="md:w-[500px] flex flex-col gap-4">
{displayedSolution ? (
<>
<div className="bg-gray-100 rounded-lg p-4 text-gray-600 font-hse-sans">
Последнее решение
</div>
<SolutionStatus solution={displayedSolution} maxPoints={task.points}/>
</>
) : (
<div className="bg-gray-100 rounded-lg p-4 text-gray-600 font-hse-sans">
Решение еще не отправлено
</div>
)}
{task.type === TaskType.INPUT && (
<InputSolution
answer={answer}
setAnswer={setAnswer}
/>
)}
{task.type === TaskType.FILE && (
<FileSolution
selectedFile={selectedFile}
setSelectedFile={setSelectedFile}
fileInputRef={fileInputRef}
existingFileUrl={selectedSolutionUrl}
onClearExistingFile={handleClearExistingFile}
firstSolution={solutionHistory.length > 0}
/>
)}
{task.type === TaskType.CODE && (
<CodeSolution
answer={answer}
setAnswer={setAnswer}
/>
)}
<ActionButtons
onSubmit={onSubmit}
onHistoryClick={handleOpenHistory}
/>
<SolutionHistorySheet
isOpen={isHistoryOpen}
onOpenChange={setIsHistoryOpen}
solutions={solutionHistory}
maxPoints={task.points}
onSolutionSelect={handleSolutionSelect}
/>
</div>
);
}; };
export default TaskSolution; const handleSolutionSelect = (solution: Solution) => {
setDisplayedSolution(solution);
};
const handleClearExistingFile = () => {
setSelectedSolutionUrl(null);
};
return (
<div className="md:w-[500px] flex flex-col gap-4">
{displayedSolution ? (
<>
<div className="bg-gray-100 rounded-lg p-4 text-gray-600 font-hse-sans">
Результат последней посылки:
</div>
<SolutionStatus key={displayedSolution.id} solution={displayedSolution} maxPoints={task.points}/>
</>
) : (
<div className="bg-gray-100 rounded-lg p-4 text-gray-600 font-hse-sans">
Решение еще не отправлено
</div>
)}
{task.type === TaskType.INPUT && (
<InputSolution
answer={answer}
setAnswer={setAnswer}
/>
)}
{task.type === TaskType.FILE && (
<FileSolution
selectedFile={selectedFile}
setSelectedFile={setSelectedFile}
fileInputRef={fileInputRef}
existingFileUrl={selectedSolutionUrl}
onClearExistingFile={handleClearExistingFile}
firstSolution={solutionHistory.length > 0}
/>
)}
{task.type === TaskType.CODE && (
<CodeSolution
answer={answer}
setAnswer={setAnswer}
/>
)}
<div className={`rounded-lg p-3 font-hse-sans text-sm flex items-center
${hasSubmissionsLeft
? 'bg-blue-50 text-blue-700'
: 'bg-red-50 text-red-700'}`}
>
{hasSubmissionsLeft ? (
<>
<span className="font-medium">
Осталось посылок: {submissionsLeft === Infinity ? '∞' : submissionsLeft}
</span>
{maxAttempts !== -1 && (
<span className="text-blue-500 ml-1">
(из {maxAttempts})
</span>
)}
</>
) : (
<span className="font-medium">
Вы использовали все посылки
</span>
)}
</div>
<ActionButtons
onSubmit={onSubmit}
onHistoryClick={handleOpenHistory}
isSubmitting={isSubmitting}
hasSubmissionsLeft={hasSubmissionsLeft}
/>
<SolutionHistorySheet
isOpen={isHistoryOpen}
onOpenChange={setIsHistoryOpen}
solutions={solutionHistory}
maxPoints={task.points}
onSolutionSelect={handleSolutionSelect}
/>
</div>
);
};
export default TaskSolution;
@@ -5,6 +5,7 @@ interface Task {
type: TaskType; type: TaskType;
in_competition_position: number; in_competition_position: number;
points: number; points: number;
max_attempts: number;
} }
export interface TaskAttachment { export interface TaskAttachment {