Skip to content

Commit 8f08e2d

Browse files
committed
New: v1 result page
1 parent 75cd9d6 commit 8f08e2d

File tree

2 files changed

+272
-0
lines changed

2 files changed

+272
-0
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
"use client";
2+
3+
import { useParams } from "next/navigation";
4+
import MainNavbar from "@/components/shared/main-navbar";
5+
import Footer from "@/components/shared/footer";
6+
import { useQuery } from "@tanstack/react-query";
7+
import {
8+
getContestMatchInfo,
9+
getContestResults,
10+
} from "@/controllers/contest.controller";
11+
import { MeshGradient } from "@/layouts/mesh-gradient";
12+
import MatchmakingTree from "@/components/league/matchmaking-tree";
13+
import { getUser } from "@/controllers/supabase.controller";
14+
import { useEffect, useState } from "react";
15+
import { ContestResultStudent } from "@/controllers/contest.controller";
16+
17+
const navLinks = [
18+
{ key: "home", label: "Inicio", href: "/" },
19+
{ key: "league", label: "Liga", href: "/league" },
20+
{ key: "rank", label: "Ranking", href: "/rank" },
21+
];
22+
23+
const StudentPositionRow = ({
24+
student,
25+
isCurrentUser = false,
26+
}: {
27+
student: ContestResultStudent;
28+
isCurrentUser?: boolean;
29+
}) => {
30+
const position = student.position;
31+
const positionClass =
32+
position === 1
33+
? "text-yellow-500"
34+
: position === 2
35+
? "text-neutral-500"
36+
: position === 3
37+
? "text-orange-500"
38+
: "text-(--azul-electrico)";
39+
40+
return (
41+
<div
42+
className={`flex gap-2 p-2 px-4 rounded-md bg-white shadow-md lg:px-6 text-xs lg:text-base hover:scale-[1.01] transition hover:shadow-lg ${
43+
isCurrentUser ? "ring-2 ring-(--azul-electrico) dark:ring-(--azul-niebla)" : ""
44+
}`}
45+
>
46+
<p className={`w-10 m-0 ${positionClass} font-semibold`}>
47+
{position}.
48+
</p>
49+
<div className="flex justify-between w-full">
50+
<p className="flex gap-1 m-0 w-[90%] truncate text-ellipsis">
51+
{student.name}
52+
<span className="md:flex hidden">{student.surname}</span>
53+
{isCurrentUser && (
54+
<span className="text-(--azul-electrico) dark:text-(--azul-niebla) font-semibold">
55+
(Tú)
56+
</span>
57+
)}
58+
</p>
59+
</div>
60+
</div>
61+
);
62+
};
63+
64+
export default function ContestResultPage() {
65+
const params = useParams();
66+
const contestId = params.contestId as string;
67+
const [userId, setUserId] = useState<string | null>(null);
68+
69+
useEffect(() => {
70+
const fetchUser = async () => {
71+
const user = await getUser();
72+
if (user) {
73+
setUserId(user.id);
74+
}
75+
};
76+
fetchUser();
77+
}, []);
78+
79+
const { data: matchData, isLoading: isLoadingMatch } = useQuery({
80+
queryKey: ["matchmaking", contestId],
81+
queryFn: async () => getContestMatchInfo(Number(contestId)),
82+
});
83+
84+
const { data: resultsData, isLoading: isLoadingResults } = useQuery({
85+
queryKey: ["contest-results", contestId, userId],
86+
queryFn: async () => {
87+
const user = await getUser();
88+
return getContestResults(Number(contestId), user?.id);
89+
},
90+
});
91+
92+
const isLoading = isLoadingMatch || isLoadingResults;
93+
94+
return (
95+
<>
96+
<MainNavbar navLinks={navLinks} />
97+
<MeshGradient>
98+
<div className="flex flex-col gap-10 items-center justify-center mt-[8%] mx-[5%] md:mx-[10%] lg:mx-[20%] pb-20">
99+
{/* Título del contest */}
100+
<h1 className="text-white text-3xl md:text-4xl lg:text-5xl text-center">
101+
{isLoading
102+
? "Cargando..."
103+
: resultsData?.contest?.name || matchData?.contest?.name || "Resultados del Contest"}
104+
</h1>
105+
106+
{/* Posición del usuario */}
107+
{resultsData?.userPosition !== undefined && (
108+
<div className="w-full max-w-2xl">
109+
<div className="p-6 rounded-md bg-white/20 backdrop-blur-lg border border-white shadow-lg">
110+
<h2 className="text-white text-2xl md:text-3xl font-semibold text-center mb-4">
111+
Tu Posición
112+
</h2>
113+
<div className="flex items-center justify-center">
114+
<span
115+
className={`text-6xl md:text-8xl font-bold ${
116+
resultsData.userPosition === 1
117+
? "text-yellow-500"
118+
: resultsData.userPosition === 2
119+
? "text-neutral-500"
120+
: resultsData.userPosition === 3
121+
? "text-orange-500"
122+
: "text-white"
123+
}`}
124+
>
125+
{resultsData.userPosition}
126+
</span>
127+
<span className="text-white text-2xl md:text-3xl ml-2">
128+
{resultsData.userPosition === 1
129+
? "🥇"
130+
: resultsData.userPosition === 2
131+
? "🥈"
132+
: resultsData.userPosition === 3
133+
? "🥉"
134+
: ""}
135+
</span>
136+
</div>
137+
</div>
138+
</div>
139+
)}
140+
141+
{/* Árbol de matchmaking */}
142+
<div className="w-full">
143+
<h2 className="text-white text-2xl md:text-3xl font-semibold text-center mb-6">
144+
Árbol de Matchmaking
145+
</h2>
146+
{!isLoadingMatch && matchData?.tree && (
147+
<MatchmakingTree
148+
tree={matchData.tree}
149+
students={matchData.students || []}
150+
/>
151+
)}
152+
{!isLoadingMatch && !matchData?.tree && (
153+
<div className="flex items-center justify-center p-8 text-white/50">
154+
No hay árbol de matchmaking disponible
155+
</div>
156+
)}
157+
</div>
158+
159+
{/* Tabla de posiciones */}
160+
{resultsData?.students && resultsData.students.length > 0 && (
161+
<div className="w-full max-w-4xl">
162+
<h2 className="text-white text-2xl md:text-3xl font-semibold text-center mb-6">
163+
Clasificación Final
164+
</h2>
165+
<div className="p-2 rounded-md bg-white shadow-md">
166+
<div className="p-2">
167+
<div className="flex flex-col gap-2">
168+
{resultsData.students.map((student) => {
169+
const isCurrentUser =
170+
resultsData.userStudentId !== undefined &&
171+
student.id === resultsData.userStudentId;
172+
return (
173+
<StudentPositionRow
174+
key={student.id}
175+
student={student}
176+
isCurrentUser={isCurrentUser}
177+
/>
178+
);
179+
})}
180+
</div>
181+
</div>
182+
</div>
183+
</div>
184+
)}
185+
186+
{!isLoadingResults && (!resultsData?.students || resultsData.students.length === 0) && (
187+
<div className="flex items-center justify-center p-8 text-white/50">
188+
No hay resultados disponibles aún
189+
</div>
190+
)}
191+
</div>
192+
</MeshGradient>
193+
<Footer />
194+
</>
195+
);
196+
}

src/controllers/contest.controller.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { User } from "@supabase/supabase-js";
1212
import { Student } from "@/models/student.model";
1313
import { queryStudentsByBulkIds } from "./student.controller";
1414
import { BACKEND_URL } from "@/config/env";
15+
import { getUserTableFromSupabaseId } from "./supabase.controller";
1516

1617
export async function getContests(): Promise<Contest[]> {
1718
const res = await fetch(new URL(`/contests`, BACKEND_URL));
@@ -200,3 +201,78 @@ export const getContestMatchInfo = async (
200201

201202
return result;
202203
};
204+
205+
export type ContestResultStudent = Student & {
206+
position: number;
207+
};
208+
209+
export type ContestResults = {
210+
ok: boolean;
211+
contest?: Contest;
212+
students?: ContestResultStudent[];
213+
userPosition?: number;
214+
userStudentId?: number;
215+
};
216+
217+
export const getContestResults = async (
218+
contestId: number,
219+
userId?: string,
220+
): Promise<ContestResults> => {
221+
try {
222+
const contest: Contest = await getContestById(contestId);
223+
const participations = await getParticipationByContestId(contestId);
224+
225+
// Filtrar participaciones que tengan posición asignada
226+
const participationsWithPosition = participations.filter(
227+
(p) => p.position !== null && p.position !== undefined,
228+
);
229+
230+
if (participationsWithPosition.length === 0) {
231+
return { ok: false };
232+
}
233+
234+
// Ordenar por posición
235+
participationsWithPosition.sort((a, b) => a.position - b.position);
236+
237+
const studentIds = participationsWithPosition.map((p) => p.student_id);
238+
const students = await queryStudentsByBulkIds(studentIds);
239+
240+
// Combinar estudiantes con sus posiciones
241+
const studentsWithPosition: ContestResultStudent[] =
242+
participationsWithPosition.map((participation) => {
243+
const student = students.find((s) => s.id === participation.student_id);
244+
if (!student) {
245+
throw new Error(`Estudiante con id ${participation.student_id} no encontrado`);
246+
}
247+
return {
248+
...student,
249+
position: participation.position,
250+
};
251+
});
252+
253+
// Obtener posición del usuario si está logueado
254+
let userPosition: number | undefined;
255+
let userStudentId: number | undefined;
256+
if (userId) {
257+
const user = await getUserTableFromSupabaseId(userId);
258+
if (user) {
259+
userStudentId = user.id;
260+
const userParticipation = participations.find(
261+
(p) => p.student_id === user.id,
262+
);
263+
userPosition = userParticipation?.position ?? undefined;
264+
}
265+
}
266+
267+
return {
268+
ok: true,
269+
contest,
270+
students: studentsWithPosition,
271+
userPosition,
272+
userStudentId,
273+
};
274+
} catch (e) {
275+
console.error("Error al obtener resultados del contest:", e);
276+
return { ok: false };
277+
}
278+
};

0 commit comments

Comments
 (0)