diff --git a/services/backend/apps/task/tasks.py b/services/backend/apps/task/tasks.py index e5fd7cd..568602a 100644 --- a/services/backend/apps/task/tasks.py +++ b/services/backend/apps/task/tasks.py @@ -15,6 +15,7 @@ def analyze_data_task(self, submission_id): submission = CompetitionTaskSubmission.objects.get(id=submission_id) try: code = submission.content.read() + print("YA SSF") files = [ { "url": ( @@ -32,7 +33,7 @@ def analyze_data_task(self, submission_id): f"{settings.CHECKER_API_ENDPOINT}/execute", json={ "files": files, - "code": base64.encode(code), + "code": base64.b64encode(code).decode("utf-8"), "answer_file_path": submission.task.answer_file_path, "expected_hash": hashlib.sha256( submission.task.correct_answer_file.read() @@ -42,6 +43,7 @@ def analyze_data_task(self, submission_id): ) response.raise_for_status() result = response.json() + print("HOHOHO") submission.stdout.save("output.txt", ContentFile(result["output"])) submission.result = { diff --git a/services/frontend/src/pages/Competition/components/CompetitionResultModal/index.tsx b/services/frontend/src/pages/Competition/components/CompetitionResultModal/index.tsx new file mode 100644 index 0000000..b691e77 --- /dev/null +++ b/services/frontend/src/pages/Competition/components/CompetitionResultModal/index.tsx @@ -0,0 +1,134 @@ +// src/components/competition/CompetitionResultsModal.tsx +import React from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { Loader2 } from 'lucide-react'; + +export interface CompetitionResult { + task_name: string; + result: number; + max_points: number +} + +interface CompetitionResultsModalProps { + competitionTitle: string; + results: CompetitionResult[] | undefined; + isLoading: boolean; + error: unknown; + isOpen: boolean; + onOpenChange: (open: boolean) => void; +} + +export const CompetitionResultsModal: React.FC = ({ + competitionTitle, + results, + isLoading, + error, + isOpen, + onOpenChange, +}) => { + const renderResultValue = (result: number, maxPoints: number) => { + if (result === -1) { + return ( + + На проверке + + ); + } else if (result === -2) { + return ( + + Нет ответа + + ); + } else if (result === 0) { + return ( + + Неверно (0/{maxPoints}) + + ); + } else if (result < maxPoints) { + return ( + + Частично верно ({result}/{maxPoints}) + + ); + } else { + return ( + + Верно ({result}/{maxPoints}) + + ); + } + }; + + return ( + + + + Результаты + + Ваши результаты по соревнованию "{competitionTitle}" + + + +
+ {isLoading ? ( +
+ +
+ ) : error ? ( +
+ Произошла ошибка при загрузке результатов +
+ ) : results && results.length > 0 ? ( + results.map((result, index) => ( +
+
{result.task_name}
+
+ {renderResultValue(result.result, result.max_points)} +
+
+ )) + ) : ( +
+ Нет доступных результатов +
+ )} +
+
+
+ ); +}; \ No newline at end of file diff --git a/services/frontend/src/pages/Competition/index.tsx b/services/frontend/src/pages/Competition/index.tsx index ed1320c..4cf4911 100644 --- a/services/frontend/src/pages/Competition/index.tsx +++ b/services/frontend/src/pages/Competition/index.tsx @@ -1,20 +1,23 @@ +import { useState } from "react"; import { useParams, Link, useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; -import { ArrowLeft, Clock, Trophy, BookOpen, BarChart2, AlertCircle } from "lucide-react"; +import { ArrowLeft, Clock, Trophy, BookOpen, AlertCircle, BarChart2 } from "lucide-react"; import ReactMarkdown from "react-markdown"; import { useQuery, useMutation } from "@tanstack/react-query"; -import { getCompetition, startCompetition } from "@/shared/api/competitions"; +import { getCompetition, startCompetition, getCompetitionResults } 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"; +import { CompetitionResultsModal } from "./components/CompetitionResultModal"; const CompetitionPage = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const competitionId = id || ""; + const [isResultsModalOpen, setIsResultsModalOpen] = useState(false); const competitionQuery = useQuery({ queryKey: ["competition", competitionId], @@ -22,6 +25,12 @@ const CompetitionPage = () => { enabled: !!competitionId, }); + const resultsQuery = useQuery({ + queryKey: ["competitionResults", competitionId], + queryFn: () => getCompetitionResults(competitionId), + enabled: !!competitionId, + }); + const startMutation = useMutation({ mutationFn: () => startCompetition(competitionId), onSuccess: async () => { @@ -60,10 +69,10 @@ const CompetitionPage = () => { const handleStart = () => { startMutation.mutate(); }; - - const handleViewResults = () => { - console.log("sorryan"); - }; + + const hasResults = resultsQuery.data && + resultsQuery.data.length > 0 && + resultsQuery.data.some(result => result.result !== -2); if (competitionQuery.isLoading) { return ; @@ -75,15 +84,6 @@ 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; @@ -92,9 +92,18 @@ const CompetitionPage = () => { return now < startDate; }; + + const isCompetitionEnded = () => { + if (!competition?.end_date) return false; + + const endDate = new Date(competition.end_date); + const now = new Date(); + + return now > endDate; + }; - const competitionEnded = isCompetitionEnded(); const competitionNotStarted = isCompetitionNotStarted(); + const competitionEnded = isCompetitionEnded(); return (
@@ -133,17 +142,17 @@ const CompetitionPage = () => { )}
- {competitionEnded && competition.type === CompetitionType.COMPETITIVE && ( -
- Завершено -
- )} - {competitionNotStarted && competition.type === CompetitionType.COMPETITIVE && (
Скоро начнется
)} + + {competitionEnded && competition.type === CompetitionType.COMPETITIVE && ( +
+ Завершено +
+ )}

@@ -178,17 +187,8 @@ const CompetitionPage = () => { -
- {competitionEnded && competition.type === CompetitionType.COMPETITIVE ? ( - - ) : competitionNotStarted && competition.type === CompetitionType.COMPETITIVE ? ( +
+ {competitionNotStarted && competition.type === CompetitionType.COMPETITIVE ? ( - ) : ( + ) : !competitionEnded ? ( + ) : null} + + {hasResults && ( + + )} + + {competitionEnded && !hasResults && competition.type === CompetitionType.COMPETITIVE && !resultsQuery.isLoading && ( +
+

Соревнование завершено. Увы

+
)}
+ + ); }; diff --git a/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx index d912f1a..9042c91 100644 --- a/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useNavigate, useParams } from 'react-router-dom'; import { Task } from '@/shared/types/task'; -import { ArrowLeft, Clock } from 'lucide-react'; +import { ArrowLeft } from 'lucide-react'; import { CompetitionType } from '@/shared/types/competition'; +import { CompetitionResult } from '@/shared/types/competition'; interface CompetitionHeaderProps { title: string; @@ -11,8 +12,9 @@ interface CompetitionHeaderProps { setAnswer: (value: string) => void; setSelectedFile: (file: File | null) => void; competitionType?: CompetitionType; - startDate?: Date; - endDate?: Date; + startDate?: Date | string; + endDate?: Date | string; + taskResults?: CompetitionResult[]; } const CompetitionHeader: React.FC = ({ @@ -23,9 +25,11 @@ const CompetitionHeader: React.FC = ({ setSelectedFile, competitionType, startDate, - endDate + endDate, + taskResults = [] }) => { const navigate = useNavigate(); + const { taskId } = useParams<{ taskId?: string }>(); const [timeLeft, setTimeLeft] = useState(''); const handleTaskSelect = (taskId: string) => { @@ -34,7 +38,7 @@ const CompetitionHeader: React.FC = ({ navigate(`/competition/${competitionId}/tasks/${taskId}`); } - const formatDate = (date?: Date) => { + const formatDate = (date?: Date | string) => { if (!date) return ''; const dateObj = typeof date === 'string' ? new Date(date) : date; @@ -74,6 +78,42 @@ const CompetitionHeader: React.FC = ({ return () => clearInterval(timerInterval); }, [endDate, competitionId, navigate, competitionType]); + const getTaskStatus = (task: Task) => { + const result = taskResults.find(r => r.task_name === task.title); + + let bgColor = 'var(--color-task-uncleared)'; + let textColor = 'var(--color-task-text-uncleared)'; + + if (result) { + if (result.result === -1) { + bgColor = 'var(--color-task-checking)'; + textColor = 'var(--color-task-text-checking)'; + } else if (result.result === -2) { + bgColor = 'var(--color-task-uncleared)'; + textColor = 'var(--color-task-text-uncleared)'; + } else if (result.result === 0) { + bgColor = 'var(--color-task-wrong)'; + textColor = 'var(--color-task-text-wrong)'; + } else if (result.result < result.max_points) { + bgColor = 'var(--color-task-partial)'; + textColor = 'var(--color-task-text-partial)'; + } else if (result.result === result.max_points) { + bgColor = 'var(--color-task-correct)'; + textColor = 'var(--color-task-text-correct)'; + } + } + + const isActive = task.id === taskId; + const activeBorder = isActive ? 'border-2 border-blue-500' : ''; + + return { + backgroundColor: bgColor, + color: textColor, + className: `rounded-lg px-3 py-1.5 font-medium text-sm font-hse-sans cursor-pointer + transition-all hover:brightness-95 flex-shrink-0 ${activeBorder}` + }; + }; + const showTimeSection = competitionType === CompetitionType.COMPETITIVE && (startDate || endDate); return ( @@ -120,18 +160,19 @@ const CompetitionHeader: React.FC = ({
- {tasks.map((task) => ( - - ))} + {tasks.map((task) => { + const { backgroundColor, color, className } = getTaskStatus(task); + return ( + + ); + })}
diff --git a/services/frontend/src/pages/CompetitionSession/index.tsx b/services/frontend/src/pages/CompetitionSession/index.tsx index 42ea9c4..b2c8de5 100644 --- a/services/frontend/src/pages/CompetitionSession/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/index.tsx @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; -import { useParams, Navigate, useNavigate } from "react-router-dom"; +import { useParams, Navigate } from "react-router-dom"; import CompetitionHeader from "./components/CompetitionHeader"; import TaskContent from "./components/TaskContent"; import TaskSolution from "./modules/TaskSolution"; import { getCompetitionTasks, submitTaskSolution } from "@/shared/api/session"; -import { getCompetition } from "@/shared/api/competitions"; +import { getCompetition, getCompetitionResults } from "@/shared/api/competitions"; import { Loader2 } from "lucide-react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { TaskType } from "@/shared/types/task"; @@ -16,7 +16,6 @@ const CompetitionSession = () => { const [isReloading, setIsReloading] = useState(false); const competitionId = id || ""; const queryClient = useQueryClient(); - const navigate = useNavigate(); const competitionQuery = useQuery({ queryKey: ["competition", competitionId], @@ -30,6 +29,12 @@ const CompetitionSession = () => { enabled: !!competitionId, }); + const resultsQuery = useQuery({ + queryKey: ["competitionResults", competitionId], + queryFn: () => getCompetitionResults(competitionId), + enabled: !!competitionId, + }); + const submitMutation = useMutation({ mutationFn: () => { if (!currentTask || !competitionId) throw new Error("Missing task or competition ID"); @@ -47,10 +52,14 @@ const CompetitionSession = () => { queryKey: ['solutionHistory', competitionId, taskId] }); + queryClient.invalidateQueries({ + queryKey: ['competitionResults', competitionId] + }); + setIsReloading(true); setTimeout(() => { - window.location.reload() + window.location.reload(); setIsReloading(false); }, 2500); }, @@ -61,6 +70,7 @@ const CompetitionSession = () => { const competition = competitionQuery.data; const tasks = tasksQuery.data || []; + const results = resultsQuery.data || []; const isLoading = tasksQuery.isLoading || competitionQuery.isLoading; const error = tasksQuery.error || competitionQuery.error ? "Не удалось загрузить данные. Пожалуйста, попробуйте позже." @@ -113,6 +123,7 @@ const CompetitionSession = () => { competitionType={competition?.type} startDate={competition?.start_date} endDate={competition?.end_date} + taskResults={results} />
@@ -138,6 +149,16 @@ const CompetitionSession = () => { onSubmit={handleSubmit} isSubmitting={isSubmitting} /> + {isReloading && ( +
+
+ +

+ Решение отправлено! Страница обновится через несколько секунд... +

+
+
+ )} ) : (
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 d22ae9c..2aaca0f 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 @@ -30,10 +30,8 @@ const ActionButtons: React.FC = ({ {isCleared ? ( ) : hasSubmissionsLeft ? ( diff --git a/services/frontend/src/pages/Profile/components/user-stat-block.tsx b/services/frontend/src/pages/Profile/components/user-stat-block.tsx new file mode 100644 index 0000000..3fec3af --- /dev/null +++ b/services/frontend/src/pages/Profile/components/user-stat-block.tsx @@ -0,0 +1,14 @@ +export const UserStatBlock = ({ + value, + description, +}: { + value: number; + description: string; +}) => { + return ( +
+

{value}

+

{description}

+
+ ); +}; diff --git a/services/frontend/src/pages/Profile/index.tsx b/services/frontend/src/pages/Profile/index.tsx index 75cd2ff..d9b61c1 100644 --- a/services/frontend/src/pages/Profile/index.tsx +++ b/services/frontend/src/pages/Profile/index.tsx @@ -1,6 +1,6 @@ import { UserInfo } from "./widgets/user-info"; import { UserAchievements } from "./widgets/user-achievements"; -import { UserStats } from "./widgets/user-stats"; +import { UserStatsSections } from "./widgets/user-stats"; import { useQuery } from "@tanstack/react-query"; import { getCurrentUser } from "@/shared/api/user"; import { Loading } from "@/components/ui/loading"; @@ -25,11 +25,11 @@ const ProfilePage = () => { return (
-
+
- +
); }; diff --git a/services/frontend/src/pages/Profile/widgets/user-achievements.tsx b/services/frontend/src/pages/Profile/widgets/user-achievements.tsx index decd815..8231fbf 100644 --- a/services/frontend/src/pages/Profile/widgets/user-achievements.tsx +++ b/services/frontend/src/pages/Profile/widgets/user-achievements.tsx @@ -11,7 +11,7 @@ export const UserAchievements = ({

Достижения

{achievements && ( -
+
{achievements.map((a) => ( diff --git a/services/frontend/src/pages/Profile/widgets/user-info.tsx b/services/frontend/src/pages/Profile/widgets/user-info.tsx index 3b3b927..808619b 100644 --- a/services/frontend/src/pages/Profile/widgets/user-info.tsx +++ b/services/frontend/src/pages/Profile/widgets/user-info.tsx @@ -2,7 +2,7 @@ import { User } from "@/shared/types/user"; export const UserInfo = ({ user }: { user: User }) => { return ( -
+
{user.avatar && (
{ +import { Loading } from "@/components/ui/loading"; +import { UserStatBlock } from "../components/user-stat-block"; +import { getCurrentUserStats } from "@/shared/api/user"; +import { useQuery } from "@tanstack/react-query"; + +export const UserStatsSections = () => { + const { data: stats, isLoading } = useQuery({ + queryKey: ["user-stats"], + queryFn: getCurrentUserStats, + }); + return ( -
+

Аналитика

-
+ {isLoading ? ( +
+ +
+ ) : stats ? ( +
+ + +
+ ) : ( +
+

+ Что-то пошло не так 😔 +

+
+ )} +
); }; diff --git a/services/frontend/src/shared/api/competitions.ts b/services/frontend/src/shared/api/competitions.ts index 2c96f4c..4c46882 100644 --- a/services/frontend/src/shared/api/competitions.ts +++ b/services/frontend/src/shared/api/competitions.ts @@ -14,7 +14,7 @@ export const getCompetition = async (id: string) => { }; export const getCompetitionResults = async (id: string) => { - return await userFetch(`/competitions/${id}/results`); + return await userFetch(`/competitions/${id}/results`); } export const startCompetition = async (competitionId: string) => { diff --git a/services/frontend/src/shared/api/user.ts b/services/frontend/src/shared/api/user.ts index 84b000d..7640390 100644 --- a/services/frontend/src/shared/api/user.ts +++ b/services/frontend/src/shared/api/user.ts @@ -1,6 +1,10 @@ import { userFetch } from "."; -import { User } from "../types/user"; +import { User, UserStats } from "../types/user"; export const getCurrentUser = async () => { return await userFetch("/me"); }; + +export const getCurrentUserStats = async () => { + return await userFetch("/me/stat"); +}; diff --git a/services/frontend/src/shared/types/competition.ts b/services/frontend/src/shared/types/competition.ts index cff4cd9..2326419 100644 --- a/services/frontend/src/shared/types/competition.ts +++ b/services/frontend/src/shared/types/competition.ts @@ -28,4 +28,5 @@ export enum CompetitionParticipationType { export interface CompetitionResult { task_name: string; result: number; + max_points: number; } \ No newline at end of file diff --git a/services/frontend/src/shared/types/user.ts b/services/frontend/src/shared/types/user.ts index 650c8d8..b9e927f 100644 --- a/services/frontend/src/shared/types/user.ts +++ b/services/frontend/src/shared/types/user.ts @@ -12,3 +12,8 @@ export interface Achievement { received_at: Date; icon?: string; } + +export interface UserStats { + total_attempts: number; + solved_tasks: number; +}