feat: started working on events fetch

This commit is contained in:
rngsurrounded
2025-03-02 20:07:43 +09:00
parent f95d6c1c6c
commit 823bb76ee9
3 changed files with 152 additions and 15 deletions
@@ -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<Competition[]>(mockCompetitions);
const [myCompetitions, setMyCompetitions] = useState<Competition[]>([]);
const [availableCompetitions, setAvailableCompetitions] = useState<Competition[]>([]);
const [activeTab, setActiveTab] = useState("ongoing");
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<div className="flex flex-col items-center justify-center h-[400px]">
<Loader2 className="h-12 w-12 animate-spin text-gray-400 mb-4" />
<p className="font-hse-sans text-gray-500">Загрузка событий...</p>
</div>
);
}
if (error) {
return (
<div className="flex justify-center items-center h-[400px]">
<p className="font-hse-sans text-red-500">{error}</p>
</div>
);
}
return (
<div className="flex flex-col gap-6 sm:gap-8">
@@ -36,14 +66,22 @@ const CompetitionsPage = () => {
</TabsList>
</Tabs>
</SectionHeader>
<CompetitionGrid competitions={filteredMyCompetitions} />
{filteredMyCompetitions.length > 0 ? (
<CompetitionGrid competitions={filteredMyCompetitions} />
) : (
<EmptyState message={`У вас нет ${activeTab === "ongoing" ? "текущих" : "завершенных"} событий`} />
)}
</Section>
<Section>
<SectionHeader>
<SectionTitle>События</SectionTitle>
</SectionHeader>
<CompetitionGrid competitions={availableCompetitions} />
{availableCompetitions.length > 0 ? (
<CompetitionGrid competitions={availableCompetitions} />
) : (
<EmptyState message="Нет доступных событий" />
)}
</Section>
</div>
);
@@ -65,4 +103,12 @@ const SectionTitle = ({ children }: { children: React.ReactNode }) => {
return <h1 className="w-full text-3xl font-semibold">{children}</h1>;
};
export default CompetitionsPage;
const EmptyState = ({ message }: { message: string }) => {
return (
<div className="flex justify-center items-center p-12 bg-gray-50 rounded-lg">
<p className="font-hse-sans text-gray-500">{message}</p>
</div>
);
};
export default CompetitionsPage;
@@ -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<Competition[]> => {
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<Competition[]> => {
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 };
};
+9 -1
View File
@@ -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 };