Merge remote-tracking branch 'origin/master'

This commit is contained in:
Timur
2025-03-02 15:19:41 +03:00
4 changed files with 87 additions and 113 deletions
@@ -2,40 +2,57 @@ import { useParams, Link, useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import { mockTasks } from "@/shared/mocks/mocks"; import { useQuery, useMutation } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query"; import { getCompetition, startCompetition } from "@/shared/api/competitions";
import { getCompetition } from "@/shared/api/competitions"; import { getCompetitionTasks } from "@/shared/api/session";
import { Loading } from "@/components/ui/loading"; import { Loading } from "@/components/ui/loading";
const CompetitionPage = () => { const CompetitionPage = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const competitionId = id || "";
const { data: competition, isLoading } = useQuery({ const competitionQuery = useQuery({
queryKey: ["competition", id], queryKey: ["competition", competitionId],
queryFn: async () => getCompetition(id || ""), queryFn: () => getCompetition(competitionId),
enabled: !!competitionId,
}); });
if (isLoading) { const startMutation = useMutation({
mutationFn: () => startCompetition(competitionId),
onSuccess: async () => {
try {
const tasks = await getCompetitionTasks(competitionId);
if (tasks && tasks.length > 0) {
navigate(`/competition/${competitionId}/tasks/${tasks[0].id}`);
} else {
navigate(`/competition/${competitionId}/tasks`);
}
} catch (error) {
console.error("Failed to fetch tasks:", error);
navigate(`/competition/${competitionId}/tasks`);
}
},
onError: (error) => {
console.error("Failed to start competition:", error);
}
});
const handleStart = () => {
startMutation.mutate();
};
if (competitionQuery.isLoading) {
return <Loading />; return <Loading />;
} }
if (!id || !competition) { if (!competitionId || !competitionQuery.data) {
return <></>; return <></>;
} }
const handleContinue = () => { const competition = competitionQuery.data;
if (competition?.id) {
if (mockTasks && mockTasks.length > 0) {
const firstTaskId = mockTasks[0].id;
navigate(`/competition/${competition.id}/tasks/${firstTaskId}`);
} else {
navigate(`/competition/${competition.id}/tasks`);
}
}
};
console.log(competition)
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<Link <Link
@@ -47,13 +64,15 @@ const CompetitionPage = () => {
</Link> </Link>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="aspect-2 h-auto w-full overflow-hidden rounded-xl"> {competition.image_url && (
<img <div className="aspect-2 h-auto w-full overflow-hidden rounded-xl">
src={competition.image_url ? competition.image_url : '/DANO.png'} <img
alt={competition.title} src={competition.image_url}
className="h-full w-full object-cover object-center" alt={competition.title}
/> className="h-full w-full object-cover object-center"
</div> />
</div>
)}
<div className="flex flex-col-reverse gap-8 md:flex-row"> <div className="flex flex-col-reverse gap-8 md:flex-row">
<div className="flex flex-1 flex-col gap-5"> <div className="flex flex-1 flex-col gap-5">
@@ -65,8 +84,12 @@ const CompetitionPage = () => {
</div> </div>
</div> </div>
<div className="w-full *:w-full md:w-96"> <div className="w-full *:w-full md:w-96">
<Button size={"lg"} onClick={handleContinue}> <Button
Приступить к выполнению size={"lg"}
onClick={handleStart}
disabled={startMutation.isPending}
>
{startMutation.isPending ? "Загрузка..." : "Начать"}
</Button> </Button>
</div> </div>
</div> </div>
@@ -75,4 +98,4 @@ const CompetitionPage = () => {
); );
}; };
export default CompetitionPage; export default CompetitionPage;
@@ -20,13 +20,11 @@ export function CompetitionCard({
className={cn("aspect-square h-full w-auto overflow-hidden", className)} className={cn("aspect-square h-full w-auto overflow-hidden", className)}
> >
<div className="relative h-full overflow-hidden"> <div className="relative h-full overflow-hidden">
{competition.image_url && ( <img
<img src={competition.image_url ? competition.image_url : '/DANO.png'}
src={competition.image_url ? competition.image_url : '/DANO.png'} alt={competition.title}
alt={competition.title} className="h-full w-full object-cover object-center"
className="h-full w-full object-cover object-center" />
/>
)}
</div> </div>
<CardContent> <CardContent>
@@ -12,3 +12,9 @@ export const getCompetitions = async (participating?: boolean) => {
export const getCompetition = async (id: string) => { export const getCompetition = async (id: string) => {
return await userFetch<Competition>(`/competition/${id}`); return await userFetch<Competition>(`/competition/${id}`);
}; };
export const startCompetition = async (competitionId: string) => {
return await userFetch(`/competitions/${competitionId}/start`, {
method: 'POST'
});
};
+24 -77
View File
@@ -1,82 +1,29 @@
import { apiFetch } from './index'; import { userFetch } from ".";
import { Task, TaskStatus } from '@/shared/types'; import { Task } from "../types/task";
interface ApiTask { export const getCompetitionTasks = async (competitionId: string) => {
id: string; return await userFetch<Task[]>(`/competitions/${competitionId}/tasks`);
title: string;
description: string;
type: 'input' | 'file' | 'code';
in_competition_position: number;
points: number;
status?: TaskStatus;
}
/**
* Fetches tasks for a specific competition
* @param competitionId - The ID of the competition
* @returns Promise with an array of tasks in the application's format
*/
export const getCompetitionTasks = async (competitionId: string): Promise<Task[]> => {
try {
const apiTasks: ApiTask[] = await apiFetch(`/api/v1/competitions/${competitionId}/tasks`);
// Transform API tasks to application Task format
return apiTasks.map(apiTask => transformApiTask(apiTask));
} catch (error) {
console.error('Failed to fetch competition tasks:', error);
throw error;
}
}; };
/** export const submitTaskSolution = async (
* Transforms an API task to the application's Task format competitionId: string,
*/ taskId: string,
const transformApiTask = (apiTask: ApiTask): Task => { solution: string | File
return { ) => {
id: apiTask.id, const endpoint = `/competitions/${competitionId}/tasks/${taskId}/submit`;
number: String(apiTask.in_competition_position),
status: apiTask.status || TaskStatus.Uncleared,
solutionType: apiTask.type,
description: apiTask.description,
maxScore: apiTask.points
};
};
// export const submitTaskSolution = async (
// competitionId: string,
// taskId: string,
// solution: string | File
// ): Promise<void> => {
// const endpoint = `/api/v1/competitions/${competitionId}/tasks/${taskId}/submit`;
// // Handle different solution types if (typeof solution === 'string') {
// if (typeof solution === 'string') { return await userFetch(endpoint, {
// // Text or code solution method: 'POST',
// await apiFetch(endpoint, { body: { answer: solution }
// method: 'POST', });
// body: { answer: solution } } else {
// }); const formData = new FormData();
// } else { formData.append('file', solution);
// // File solution
// const formData = new FormData();
// formData.append('file', solution);
// await apiFetch(endpoint, { return await userFetch(endpoint, {
// method: 'POST', method: 'POST',
// body: formData body: formData
// }); });
// } }
// }; };
/**
* Gets the status of a task submission
* This would be used to poll for updates after submission
*/
// export const getTaskSubmissionStatus = async (
// competitionId: string,
// taskId: string
// ): Promise<TaskStatus> => {
// const response = await apiFetch(`/api/v1/competitions/${competitionId}/tasks/${taskId}/status`);
// return response.status;
// };