From 7de03ecf8631cd51125369a8fbe8d57b93039ad0 Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Sun, 2 Mar 2025 22:28:57 +0900 Subject: [PATCH] continued working on session fetch --- .../components/ConstructorHeader/index.tsx | 114 ++++++------ .../pages/CompetitionConstructor/index.tsx | 156 ++++++++-------- .../modules/TaskCreationModal/index.tsx | 176 +++++++++--------- .../components/TaskContent/index.tsx | 57 +++++- .../src/pages/CompetitionSession/index.tsx | 30 +-- .../components/ActionButtons/index.tsx | 53 ++---- .../components/SolutionHistorySheet/index.tsx | 8 +- .../components/SolutionStatus/index.tsx | 37 ++-- .../modules/TaskSolution/index.tsx | 41 +++- .../pages/CompetitionSession/utils/utils.ts | 38 +++- services/frontend/src/shared/api/session.ts | 11 +- services/frontend/src/shared/types/task.ts | 24 ++- 12 files changed, 431 insertions(+), 314 deletions(-) diff --git a/services/frontend/src/pages/CompetitionConstructor/components/ConstructorHeader/index.tsx b/services/frontend/src/pages/CompetitionConstructor/components/ConstructorHeader/index.tsx index 04442d1..b0f3adb 100644 --- a/services/frontend/src/pages/CompetitionConstructor/components/ConstructorHeader/index.tsx +++ b/services/frontend/src/pages/CompetitionConstructor/components/ConstructorHeader/index.tsx @@ -1,63 +1,63 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; -import { Task } from "@/shared/types"; -import { Settings, Plus } from 'lucide-react'; -import { Button } from "@/components/ui/button"; +// import React from 'react'; +// import { Link } from 'react-router-dom'; +// import { Task } from "@/shared/types"; +// import { Settings, Plus } from 'lucide-react'; +// import { Button } from "@/components/ui/button"; -interface ConstructorHeaderProps { - title: string; - tasks: Task[]; - competitionId: string; - onAddTaskClick: () => void; -} +// interface ConstructorHeaderProps { +// title: string; +// tasks: Task[]; +// competitionId: string; +// onAddTaskClick: () => void; +// } -const ConstructorHeader: React.FC = ({ - title, - tasks, - competitionId, - onAddTaskClick -}) => { - return ( -
-
-
-

- {title} -

-
+// const ConstructorHeader: React.FC = ({ +// title, +// tasks, +// competitionId, +// onAddTaskClick +// }) => { +// return ( +//
+//
+//
+//

+// {title} +//

+//
-
- - - +//
+// +// +// - {tasks.map((task) => ( - - {task.number} - - ))} +// {tasks.map((task) => ( +// +// {task.number} +// +// ))} - -
-
-
- ); -}; +// +//
+// +//
+// ); +// }; -export default ConstructorHeader; \ No newline at end of file +// export default ConstructorHeader; \ No newline at end of file diff --git a/services/frontend/src/pages/CompetitionConstructor/index.tsx b/services/frontend/src/pages/CompetitionConstructor/index.tsx index 4f7f247..fcfe774 100644 --- a/services/frontend/src/pages/CompetitionConstructor/index.tsx +++ b/services/frontend/src/pages/CompetitionConstructor/index.tsx @@ -1,89 +1,89 @@ -import { useState } from "react"; -import { useParams, Navigate, useNavigate } from "react-router-dom"; -import { Task, TaskStatus } from "@/shared/types"; -import ConstructorHeader from "./components/ConstructorHeader"; -import TaskCreationModal from "./modules/TaskCreationModal"; +// import { useState } from "react"; +// import { useParams, Navigate, useNavigate } from "react-router-dom"; +// import { Task, TaskStatus } from "@/shared/types"; +// import ConstructorHeader from "./components/ConstructorHeader"; +// import TaskCreationModal from "./modules/TaskCreationModal"; -const CompetitionConstructor = () => { - const { id, taskId } = useParams<{ id: string; taskId?: string }>(); - const navigate = useNavigate(); - const [competitionTitle, setCompetitionTitle] = useState("Новая олимпиада"); - const [tasks, setTasks] = useState([]); - const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); +// const CompetitionConstructor = () => { +// const { id, taskId } = useParams<{ id: string; taskId?: string }>(); +// const navigate = useNavigate(); +// const [competitionTitle, setCompetitionTitle] = useState("Новая олимпиада"); +// const [tasks, setTasks] = useState([]); +// const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); - const isSettings = taskId === "settings"; +// const isSettings = taskId === "settings"; - const handleOpenTaskModal = () => { - setIsTaskModalOpen(true); - }; +// const handleOpenTaskModal = () => { +// setIsTaskModalOpen(true); +// }; - const handleCloseTaskModal = () => { - setIsTaskModalOpen(false); - }; +// const handleCloseTaskModal = () => { +// setIsTaskModalOpen(false); +// }; - const handleCreateTask = (taskData: Partial) => { - const newTask: Task = { - id: `task-${Date.now()}`, - number: taskData.number || `${tasks.length + 1}`, - status: TaskStatus.Uncleared, - solutionType: taskData.solutionType || "input", - description: taskData.description || "", - requirements: taskData.requirements, - attachments: taskData.attachments || [] - }; +// const handleCreateTask = (taskData: Partial) => { +// const newTask: Task = { +// id: `task-${Date.now()}`, +// number: taskData.number || `${tasks.length + 1}`, +// status: TaskStatus.Uncleared, +// solutionType: taskData.solutionType || "input", +// description: taskData.description || "", +// requirements: taskData.requirements, +// attachments: taskData.attachments || [] +// }; - setTasks([...tasks, newTask]); - setIsTaskModalOpen(false); - navigate(`/constructor/${id}/tasks/${newTask.id}`); - }; +// setTasks([...tasks, newTask]); +// setIsTaskModalOpen(false); +// navigate(`/constructor/${id}/tasks/${newTask.id}`); +// }; - if (!taskId) { - if (tasks.length > 0) { - return ; - } else { - return ; - } - } +// if (!taskId) { +// if (tasks.length > 0) { +// return ; +// } else { +// return ; +// } +// } - return ( -
- +// return ( +//
+// - +// -
-
- {isSettings ? ( -
-

Настройки олимпиады

-

- Здесь будет форма настроек олимпиады -

-
- ) : ( -
-

- {`Редактирование задачи ${tasks.find(t => t.id === taskId)?.number || ""}`} -

-

- Здесь будет форма редактирования задачи -

-
- )} -
-
-
- ); -}; +//
+//
+// {isSettings ? ( +//
+//

Настройки олимпиады

+//

+// Здесь будет форма настроек олимпиады +//

+//
+// ) : ( +//
+//

+// {`Редактирование задачи ${tasks.find(t => t.id === taskId)?.number || ""}`} +//

+//

+// Здесь будет форма редактирования задачи +//

+//
+// )} +//
+//
+//
+// ); +// }; -export default CompetitionConstructor; \ No newline at end of file +// export default CompetitionConstructor; \ No newline at end of file diff --git a/services/frontend/src/pages/CompetitionConstructor/modules/TaskCreationModal/index.tsx b/services/frontend/src/pages/CompetitionConstructor/modules/TaskCreationModal/index.tsx index 9380af4..b71b311 100644 --- a/services/frontend/src/pages/CompetitionConstructor/modules/TaskCreationModal/index.tsx +++ b/services/frontend/src/pages/CompetitionConstructor/modules/TaskCreationModal/index.tsx @@ -1,101 +1,101 @@ -import React, { useState } from 'react'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Task } from "@/shared/types"; -import TaskNumberField from './components/TaskNumberField'; -import TaskDescriptionField from './components/TaskDescriptionField'; -import TaskRequirementsField from './components/TaskRequirementsField'; -import TaskSolutionTypeSelector from './components/TaskSolutionTypeSelector'; -import TaskFileAttachments from './components/TaskFileAttachments'; +// import React, { useState } from 'react'; +// import { +// Dialog, +// DialogContent, +// DialogHeader, +// DialogTitle, +// DialogFooter +// } from "@/components/ui/dialog"; +// import { Button } from "@/components/ui/button"; +// import { Task } from "@/shared/types"; +// import TaskNumberField from './components/TaskNumberField'; +// import TaskDescriptionField from './components/TaskDescriptionField'; +// import TaskRequirementsField from './components/TaskRequirementsField'; +// import TaskSolutionTypeSelector from './components/TaskSolutionTypeSelector'; +// import TaskFileAttachments from './components/TaskFileAttachments'; -interface TaskCreationModalProps { - isOpen: boolean; - onClose: () => void; - onCreateTask: (task: Partial) => void; - taskCount: number; -} +// interface TaskCreationModalProps { +// isOpen: boolean; +// onClose: () => void; +// onCreateTask: (task: Partial) => void; +// taskCount: number; +// } -const TaskCreationModal: React.FC = ({ - isOpen, - onClose, - onCreateTask, - taskCount -}) => { - const [number, setNumber] = useState(`${taskCount + 1}`); - const [description, setDescription] = useState(''); - const [requirements, setRequirements] = useState(''); - const [solutionType, setSolutionType] = useState<'input' | 'file' | 'code'>('input'); - const [attachedFiles, setAttachedFiles] = useState([]); +// const TaskCreationModal: React.FC = ({ +// isOpen, +// onClose, +// onCreateTask, +// taskCount +// }) => { +// const [number, setNumber] = useState(`${taskCount + 1}`); +// const [description, setDescription] = useState(''); +// const [requirements, setRequirements] = useState(''); +// const [solutionType, setSolutionType] = useState<'input' | 'file' | 'code'>('input'); +// const [attachedFiles, setAttachedFiles] = useState([]); - const handleSubmit = () => { - const newTask: Partial = { - number, - description, - requirements: requirements || undefined, - solutionType, - attachments: attachedFiles.map(file => file.name) - }; +// const handleSubmit = () => { +// const newTask: Partial = { +// number, +// description, +// requirements: requirements || undefined, +// solutionType, +// attachments: attachedFiles.map(file => file.name) +// }; - onCreateTask(newTask); +// onCreateTask(newTask); - setNumber(`${taskCount + 1}`); - setDescription(''); - setRequirements(''); - setSolutionType('input'); - setAttachedFiles([]); - }; +// setNumber(`${taskCount + 1}`); +// setDescription(''); +// setRequirements(''); +// setSolutionType('input'); +// setAttachedFiles([]); +// }; - return ( - - - - Создание новой задачи - +// return ( +// +// +// +// Создание новой задачи +// -
- +//
+// - +// - +// - +// - -
+// +//
- - - - -
-
- ); -}; +// +// +// +// +//
+//
+// ); +// }; -export default TaskCreationModal; \ No newline at end of file +// export default TaskCreationModal; \ No newline at end of file diff --git a/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx b/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx index a1abc5f..0c49ca3 100644 --- a/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx @@ -3,20 +3,34 @@ import ReactMarkdown from 'react-markdown'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; import 'katex/dist/katex.min.css'; -import { Task } from "@/shared/types"; +import { Task } from '@/shared/types/task'; +import { useQuery } from '@tanstack/react-query'; +import { getTaskAttachments } from '@/shared/api/session'; +import { FileIcon, Loader2 } from 'lucide-react'; +import { useParams } from 'react-router-dom'; interface TaskContentProps { task: Task; } const TaskContent: React.FC = ({ task }) => { + const { id: competitionId } = useParams<{ id: string }>(); + + const attachmentsQuery = useQuery({ + queryKey: ['taskAttachments', competitionId, task.id], + queryFn: () => getTaskAttachments(competitionId || '', task.id), + enabled: !!(competitionId && task.id), + }); + + const attachments = attachmentsQuery.data || []; + return (

- Задача {task.number} + Задача {task.in_competition_position}

-
+
= ({ task }) => { {task.description}
+ + {attachmentsQuery.isLoading ? ( +
+ + Загрузка файлов... +
+ ) : attachments.length > 0 ? ( +
+

Прикрепленные файлы

+
+ {attachments.map((attachment) => ( + + + + {getFileNameFromUrl(attachment.file)} + + + ))} +
+
+ ) : null}
); }; -export default TaskContent; +const getFileNameFromUrl = (url: string): string => { + try { + const parts = url.split('/'); + return parts[parts.length - 1]; + } catch (e) { + return 'Файл'; + } +}; + +export default TaskContent; \ No newline at end of file diff --git a/services/frontend/src/pages/CompetitionSession/index.tsx b/services/frontend/src/pages/CompetitionSession/index.tsx index acf4b00..a461dbf 100644 --- a/services/frontend/src/pages/CompetitionSession/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/index.tsx @@ -1,23 +1,32 @@ import { useState } from "react"; import { useParams, Navigate } from "react-router-dom"; -import { mockSolutions } from "@/shared/mocks/mocks"; import CompetitionHeader from "./components/CompetitionHeader"; import TaskContent from "./components/TaskContent"; import TaskSolution from "./modules/TaskSolution"; -import { getCompetitionTasks } from "@/shared/api/session"; +import { getCompetitionTasks, submitTaskSolution } from "@/shared/api/session"; import { Loader2 } from "lucide-react"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; const CompetitionSession = () => { const { id, taskId } = useParams<{ id: string; taskId?: string }>(); const [answer, setAnswer] = useState(""); const competitionId = id || ""; + const queryClient = useQueryClient(); const tasksQuery = useQuery({ queryKey: ["competitionTasks", competitionId], queryFn: () => getCompetitionTasks(competitionId), enabled: !!competitionId, - // refetchOnWindowFocus: false, + }); + + const submitMutation = useMutation({ + mutationFn: () => submitTaskSolution(competitionId, taskId || "", answer), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ['submissionHistory', competitionId, taskId] + }); + setAnswer(""); + } }); const tasks = tasksQuery.data || []; @@ -35,14 +44,9 @@ const CompetitionSession = () => { ); } - const handleSubmit = async () => { - if (!currentTask || !competitionId) return; - - try { - console.log("Solution submitted successfully"); - } catch (err) { - console.error("Failed to submit solution:", err); - } + const handleSubmit = () => { + if (!currentTask || !competitionId || !answer.trim()) return; + submitMutation.mutate(); }; return ( @@ -69,7 +73,7 @@ const CompetitionSession = () => { void; - solutionHistory?: Solution[]; + onHistoryClick: () => void; } const ActionButtons: React.FC = ({ onSubmit, - solutionHistory = mockSolutions + onHistoryClick }) => { - const [isHistoryOpen, setIsHistoryOpen] = useState(false); - - const handleHistoryClick = () => { - setIsHistoryOpen(true); - }; - return ( - <> -
- - -
- {/* чуть-чуть рак */} - - +
+ + +
); }; diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionHistorySheet/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionHistorySheet/index.tsx index dcaaa82..dc8fe95 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionHistorySheet/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionHistorySheet/index.tsx @@ -3,18 +3,20 @@ import { Sheet, SheetClose, SheetContent, SheetHeader, SheetTitle } from "@/comp import { Button } from "@/components/ui/button"; import { X } from "lucide-react"; import SolutionStatus from '../SolutionStatus'; -import { Solution } from "@/shared/types"; +import { Solution } from '@/shared/types/task'; interface SolutionHistorySheetProps { isOpen: boolean; onOpenChange: (open: boolean) => void; solutions: Solution[]; + maxPoints: number } const SolutionHistorySheet: React.FC = ({ isOpen, onOpenChange, - solutions + solutions, + maxPoints }) => { return ( @@ -34,7 +36,7 @@ const SolutionHistorySheet: React.FC = ({ {solutions.length > 0 ? ( solutions.map((solution, index) => (
- +
)) ) : ( diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx index dbe77e9..33a412a 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx @@ -1,41 +1,26 @@ import React from 'react'; -import { Solution, TaskStatus } from "@/shared/types"; -import { getTaskBgColor, getTaskTextColor } from '@/pages/CompetitionSession/utils/utils'; +import { Solution } from '@/shared/types/task'; +import { getSolutionBgColor, getSolutionTextColor, getStatusText } from '@/pages/CompetitionSession/utils/utils'; interface SolutionStatusProps { solution: Solution; + maxPoints: number; } -const SolutionStatus: React.FC = ({ solution }) => { - const getStatusText = (status: TaskStatus, score?: number, maxScore?: number) => { - switch (status) { - case TaskStatus.Checking: - return 'На проверке'; - case TaskStatus.Wrong: - return 'Неверный ответ'; - case TaskStatus.Correct: - return `Зачтено ${maxScore}/${maxScore} баллов`; - case TaskStatus.Partial: - return `Зачтено ${score}/${maxScore} баллов`; - case TaskStatus.Uncleared: - return 'Не решено'; - default: - return ''; - } - }; - +const SolutionStatus: React.FC = ({ solution, maxPoints }) => { + return ( -
+
- + Решение {solution.id} - - {getStatusText(solution.status, solution.score, solution.maxScore)} + + {getStatusText(solution.status, solution.earned_points, maxPoints)}
-
- {solution.date} +
+ {solution.timestamp}
); diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index 9c25bf8..44ebfd0 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -1,10 +1,14 @@ import React, { useState, useRef } from 'react'; -import { Solution, Task } from "@/shared/types"; +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; @@ -12,7 +16,6 @@ interface TaskSolutionProps { answer: string; setAnswer: (value: string) => void; onSubmit: () => void; - } const TaskSolution: React.FC = ({ @@ -24,16 +27,30 @@ const TaskSolution: React.FC = ({ }) => { const [selectedFile, setSelectedFile] = useState(null); const fileInputRef = useRef(null); + const [isHistoryOpen, setIsHistoryOpen] = useState(false); + const { id: competitionId } = useParams<{ id: string }>(); + + const solutionsQuery = useQuery({ + queryKey: ['solutionHistory', competitionId, task.id], + queryFn: () => getTaskSolutionHistory(competitionId || '', task.id), + enabled: !!(competitionId && task.id), + }); + + const solutionHistory = solutionsQuery.data || []; + + const handleOpenHistory = () => { + setIsHistoryOpen(true); + }; return (
- + - {task.solutionType === 'input' && ( + {task.type === TaskType.INPUT && ( )} - {task.solutionType === 'file' && ( + {task.type === TaskType.FILE && ( = ({ /> )} - {task.solutionType === 'code' && ( + {task.type === TaskType.CODE && ( )} - + + +
); }; diff --git a/services/frontend/src/pages/CompetitionSession/utils/utils.ts b/services/frontend/src/pages/CompetitionSession/utils/utils.ts index 9ba336e..fa21f4c 100644 --- a/services/frontend/src/pages/CompetitionSession/utils/utils.ts +++ b/services/frontend/src/pages/CompetitionSession/utils/utils.ts @@ -1,4 +1,5 @@ import { TaskStatus } from "@/shared/types"; +import { SolutionStatus } from "@/shared/types/task"; const getTaskBgColor = (status: TaskStatus): string => { switch (status) { case TaskStatus.Uncleared: return "bg-[var(--color-task-uncleared)]"; @@ -19,4 +20,39 @@ const getTaskTextColor = (status: TaskStatus): string => { } }; -export {getTaskBgColor, getTaskTextColor} \ No newline at end of file +const getSolutionBgColor = (status: SolutionStatus, earned_points: number, maxPoints: number): string => { + switch (status) { + case SolutionStatus.SENT: return "text-[var(--color-task-uncleared)]"; + case SolutionStatus.CHECKING: return "text-[var(--color-task-checking)]"; + case SolutionStatus.CHECKED: { + if (earned_points === 0) return "text-[var(--color-task-wrong)]"; + else if (earned_points === maxPoints) "text-[var(--color-task-correct)]"; + return "text-[var(--color-task-partial)]"; + } + } +} + +const getSolutionTextColor = (status: SolutionStatus, earned_points: number, maxPoints: number): string => { + switch (status) { + case SolutionStatus.SENT: return "text-[var(--color-task-text-uncleared)]"; + case SolutionStatus.CHECKING: return "text-[var(--color-task-text-checking)]"; + case SolutionStatus.CHECKED: { + if (earned_points === 0) return "text-[var(--color-task-text-wrong)]"; + else if (earned_points === maxPoints) "text-[var(--color-task-text-correct)]"; + return "text-[var(--color-task-text-partial)]"; + } + } +} + +const getStatusText = (status: SolutionStatus, earned_points: number, maxPoints: number): string => { + switch (status) { + case SolutionStatus.SENT: return "Решение отправлено"; + case SolutionStatus.CHECKING: return "Решение проверяется"; + case SolutionStatus.CHECKED: { + if (earned_points === 0) return "Неверный ответ"; + else if (earned_points === maxPoints) `Зачтено ${maxPoints}/${maxPoints} баллов`; + return `Зачтено ${earned_points}/${maxPoints} баллов`; + } + } +} +export {getTaskBgColor, getTaskTextColor, getSolutionBgColor, getSolutionTextColor, getStatusText} \ No newline at end of file diff --git a/services/frontend/src/shared/api/session.ts b/services/frontend/src/shared/api/session.ts index 0cc6383..4709179 100644 --- a/services/frontend/src/shared/api/session.ts +++ b/services/frontend/src/shared/api/session.ts @@ -1,10 +1,19 @@ import { userFetch } from "."; -import { Task } from "../types/task"; +import { Task, Solution, TaskAttachment } from "../types/task"; export const getCompetitionTasks = async (competitionId: string) => { return await userFetch(`/competitions/${competitionId}/tasks`); }; +export const getTaskSolutionHistory = async (competitionId: string, taskId: string) => { + return await userFetch(`/competitions/${competitionId}/tasks/${taskId}/history`); +}; + +export const getTaskAttachments = async (competitionId: string, taskId: string) => { + return await userFetch(`/competitions/${competitionId}/tasks/${taskId}/attachments`); +}; + + export const submitTaskSolution = async ( competitionId: string, taskId: string, diff --git a/services/frontend/src/shared/types/task.ts b/services/frontend/src/shared/types/task.ts index 9d215d3..c1434d5 100644 --- a/services/frontend/src/shared/types/task.ts +++ b/services/frontend/src/shared/types/task.ts @@ -1,4 +1,4 @@ -export interface Task { +interface Task { id: string; title: string; description: string; @@ -7,8 +7,30 @@ export interface Task { points: number; } +export interface TaskAttachment { + id: string; + file: string; + public: boolean; +} + enum TaskType { INPUT = "input", FILE = "file", CODE = "code", } + +enum SolutionStatus { + SENT = "sent", + CHECKING = "checking", + CHECKED = "checked", +} + +interface Solution { + id: string, + status: SolutionStatus, + timestamp: string, + earned_points: number +} + +export type {Task, Solution} +export {TaskType, SolutionStatus} \ No newline at end of file