diff --git a/services/frontend/src/pages/Competitions/index.tsx b/services/frontend/src/pages/Competitions/index.tsx index cd09103..97aa23e 100644 --- a/services/frontend/src/pages/Competitions/index.tsx +++ b/services/frontend/src/pages/Competitions/index.tsx @@ -1,18 +1,35 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Competition, CompetitionStatus } from "@/shared/types"; import { CompetitionGrid } from "./modules/CompetitionGrid"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { mockCompetitions } from "@/shared/mocks/mocks"; +import { Loader2 } from "lucide-react"; +import { getAllCompetitions } from "@/shared/api/competitions"; const CompetitionsPage = () => { - const [competitions] = useState(mockCompetitions); + const [myCompetitions, setMyCompetitions] = useState([]); + const [availableCompetitions, setAvailableCompetitions] = useState([]); const [activeTab, setActiveTab] = useState("ongoing"); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - const myCompetitions = competitions.filter( - (comp) => - comp.status === CompetitionStatus.InProgress || - comp.status === CompetitionStatus.Completed, - ); + useEffect(() => { + const fetchCompetitions = async () => { + try { + setLoading(true); + const { participating, nonParticipating } = await getAllCompetitions(); + setMyCompetitions(participating); + setAvailableCompetitions(nonParticipating); + setError(null); + } catch (err) { + console.error("Failed to fetch competitions:", err); + setError("Не удалось загрузить события. Пожалуйста, попробуйте позже."); + } finally { + setLoading(false); + } + }; + + fetchCompetitions(); + }, []); const filteredMyCompetitions = myCompetitions.filter((comp) => activeTab === "ongoing" @@ -20,9 +37,22 @@ const CompetitionsPage = () => { : comp.status === CompetitionStatus.Completed, ); - const availableCompetitions = competitions.filter( - (comp) => comp.status === "Не участвую", - ); + if (loading) { + return ( +
+ +

Загрузка событий...

+
+ ); + } + + if (error) { + return ( +
+

{error}

+
+ ); + } return (
@@ -36,14 +66,22 @@ const CompetitionsPage = () => { - + {filteredMyCompetitions.length > 0 ? ( + + ) : ( + + )}
События - + {availableCompetitions.length > 0 ? ( + + ) : ( + + )}
); @@ -65,4 +103,12 @@ const SectionTitle = ({ children }: { children: React.ReactNode }) => { return

{children}

; }; -export default CompetitionsPage; +const EmptyState = ({ message }: { message: string }) => { + return ( +
+

{message}

+
+ ); +}; + +export default CompetitionsPage; \ No newline at end of file diff --git a/services/frontend/src/shared/api/competitions.ts b/services/frontend/src/shared/api/competitions.ts new file mode 100644 index 0000000..6ba02ad --- /dev/null +++ b/services/frontend/src/shared/api/competitions.ts @@ -0,0 +1,83 @@ +import { apiFetch } from '.'; +import { Competition, CompetitionStatus, ParticipationType } from '@/shared/types'; + +interface ApiCompetition { + id: string; + state: 'started' | 'not_started' | 'finished'; + title: string; + description: string; + image_url: string | null; + end_date: string; + start_date: string; + type: string; + participation_type: ParticipationType; +} + +const mapStateToStatus = (state: string, isParticipating: boolean): CompetitionStatus => { + if (!isParticipating) { + return CompetitionStatus.NotParticipating; + } + + switch (state) { + case 'started': + return CompetitionStatus.InProgress; + case 'finished': + return CompetitionStatus.Completed; + case 'not_started': + return CompetitionStatus.InProgress; + default: + return CompetitionStatus.NotParticipating; + } +}; + +const transformApiCompetition = (apiComp: ApiCompetition, isParticipating: boolean): Competition => { + return { + id: apiComp.id, + name: apiComp.title, + imageUrl: apiComp.image_url || '/DANO.png', + isOlympics: apiComp.type !== 'edu', + status: mapStateToStatus(apiComp.state, isParticipating), + description: apiComp.description, + startDate: new Date(apiComp.start_date), + endDate: new Date(apiComp.end_date), + participationType: apiComp.participation_type + }; +}; + +export const getParticipatingCompetitions = async (): Promise => { + try { + const apiCompetitions: ApiCompetition[] = await apiFetch('/api/v1/competitions', { + query: { is_participating: true } + }); + + return apiCompetitions.map(comp => transformApiCompetition(comp, true)); + } catch (error) { + console.error('Failed to fetch participating competitions:', error); + throw error; + } +}; + +export const getNonParticipatingCompetitions = async (): Promise => { + try { + const apiCompetitions: ApiCompetition[] = await apiFetch('/api/v1/competitions', { + query: { is_participating: false } + }); + + return apiCompetitions.map(comp => transformApiCompetition(comp, false)); + } catch (error) { + console.error('Failed to fetch non-participating competitions:', error); + throw error; + } +}; + +export const getAllCompetitions = async (): Promise<{ + participating: Competition[]; + nonParticipating: Competition[]; +}> => { + const [participating, nonParticipating] = await Promise.all([ + getParticipatingCompetitions(), + getNonParticipatingCompetitions() + ]); + + return { participating, nonParticipating }; +}; \ No newline at end of file diff --git a/services/frontend/src/shared/types.ts b/services/frontend/src/shared/types.ts index a67e044..083614d 100644 --- a/services/frontend/src/shared/types.ts +++ b/services/frontend/src/shared/types.ts @@ -12,6 +12,11 @@ enum TaskStatus { Wrong = "wrong" } +enum ParticipationType { + Solo = "solo", + Team = "team" +} + interface Competition { id: string; name: string; @@ -19,6 +24,9 @@ interface Competition { isOlympics: boolean; status: CompetitionStatus; description?: string; + startDate: Date; + endDate: Date; + participationType: ParticipationType } type SolutionType = "input" | "file" | "code"; @@ -42,5 +50,5 @@ interface Task { attachments?: string[]; } -export { CompetitionStatus, TaskStatus }; +export { CompetitionStatus, TaskStatus, ParticipationType }; export type { Solution, Competition, Task };