From 6ecc673c6c9061f89a5a29ccc351cc4646afe6ae Mon Sep 17 00:00:00 2001 From: Kalos Lazo <118573214+kaloslazo@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:14:05 -0500 Subject: [PATCH] feat: leaderboard ippm --- api/auth.ts | 186 +++++++------- api/complaints.ts | 42 +++- components/map/ComplaintMap.tsx | 184 +++++++++----- constants/api.ts | 3 +- pages/RankingPage.tsx | 424 +++++++++++++++++++++++++++++++- 5 files changed, 681 insertions(+), 158 deletions(-) diff --git a/api/auth.ts b/api/auth.ts index 3ae3712..8247d75 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -2,106 +2,114 @@ import { API_URL } from "@/constants"; import { AuthResponse, RegisterData, LoginData } from "@/types/auth"; export const authApi = { - register: async (data: RegisterData): Promise => { - console.log('Sending register request to:', `${API_URL}/auth/register`); - console.log('Register data:', { ...data, password: '***' }); + register: async (data: RegisterData): Promise => { + console.log("Sending register request to:", `${API_URL}/auth/register`); + console.log("Register data:", { ...data, password: "***" }); - try { - const response = await fetch(`${API_URL}/auth/register`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(data), - }); + try { + const response = await fetch(`${API_URL}/auth/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", // Importante para manejar cookies + }); - const responseData = await response.text(); - console.log('Register raw response:', responseData); + // Primero verificamos si la respuesta es JSON válido + let responseData; + try { + responseData = await response.json(); + } catch (e) { + throw new Error("El servidor no devolvió una respuesta JSON válida"); + } - if (!response.ok) { - let errorMessage: string; - try { - const errorData = JSON.parse(responseData); - errorMessage = errorData.message || 'Error en el registro'; - } catch (e) { - errorMessage = `Error en el registro: ${response.status} ${response.statusText}`; - } - throw new Error(errorMessage); - } + if (!response.ok) { + throw new Error(responseData.message || "Error en el registro"); + } - try { - const parsedData = JSON.parse(responseData); - console.log('Register success:', parsedData); - return parsedData; - } catch (e) { - console.error('Error parsing success response:', e); - throw new Error('Error al procesar la respuesta del servidor'); - } - } catch (error) { - console.error("Error registering user:", error); - throw error; - } - }, + console.log("Register success:", responseData); + return responseData; + } catch (error) { + console.error("Error registering user:", error); + throw error instanceof Error + ? error + : new Error("Error desconocido en el registro"); + } + }, - login: async (credentials: LoginData): Promise => { - try { - console.log('Login request:', { ...credentials, password: '***' }); + login: async (credentials: LoginData): Promise => { + try { + console.log("Login request:", { ...credentials, password: "***" }); - const response = await fetch(`${API_URL}/auth/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify(credentials), - }); + const response = await fetch(`${API_URL}/auth/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(credentials), + credentials: "include", // Importante para manejar cookies + }); - const responseData = await response.text(); - console.log('Login response:', responseData); + // Primero verificamos si la respuesta es JSON válido + let responseData; + try { + responseData = await response.json(); + } catch (e) { + throw new Error("El servidor no devolvió una respuesta JSON válida"); + } - if (!response.ok) { - try { - const errorData = JSON.parse(responseData); - throw new Error(errorData.message || 'Error al iniciar sesión'); - } catch (e) { - throw new Error('Error al iniciar sesión: ' + response.statusText); - } - } + if (!response.ok) { + throw new Error( + responseData.message || + `Error al iniciar sesión: ${response.statusText}` + ); + } - return JSON.parse(responseData); - } catch (error) { - console.error("Error logging in user:", error); - throw error; - } - }, + return responseData; + } catch (error) { + console.error("Error logging in user:", error); + if (error instanceof Error) { + throw new Error(`Error al iniciar sesión: ${error.message}`); + } + throw new Error("Error desconocido al iniciar sesión"); + } + }, - verifyToken: async (token: string): Promise => { - try { - const response = await fetch(`${API_URL}/auth/verify`, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/json', - }, - }); + verifyToken: async (token: string): Promise => { + try { + const response = await fetch(`${API_URL}/auth/verify`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + Accept: "application/json", + }, + credentials: "include", // Importante para manejar cookies + }); - const responseData = await response.text(); - console.log('Verify token response:', responseData); + // Primero verificamos si la respuesta es JSON válido + let responseData; + try { + responseData = await response.json(); + } catch (e) { + throw new Error("El servidor no devolvió una respuesta JSON válida"); + } - if (!response.ok) { - try { - const errorData = JSON.parse(responseData); - throw new Error(errorData.message || 'Token inválido'); - } catch (e) { - throw new Error('Error al verificar token: ' + response.statusText); - } - } + if (!response.ok) { + throw new Error( + responseData.message || `Token inválido: ${response.statusText}` + ); + } - return JSON.parse(responseData); - } catch (error) { - console.error("Error verifying token:", error); - throw error; - } - }, + return responseData; + } catch (error) { + console.error("Error verifying token:", error); + if (error instanceof Error) { + throw new Error(`Error al verificar token: ${error.message}`); + } + throw new Error("Error desconocido al verificar token"); + } + }, }; diff --git a/api/complaints.ts b/api/complaints.ts index abea481..dc7463e 100644 --- a/api/complaints.ts +++ b/api/complaints.ts @@ -1,6 +1,12 @@ import { ComplaintPointInterface } from "@/types"; import { API_URL } from "@/constants"; +interface LeaderboardUser { + complaintsCount: number; + userDni: string; + userName: string; +} + export const complaintsApi = { getAll: async (): Promise => { try { @@ -20,18 +26,18 @@ export const complaintsApi = { create: async (formData: FormData): Promise => { try { const response = await fetch(`${API_URL}/complaints`, { - method: 'POST', + method: "POST", headers: { - 'Accept': 'application/json', + Accept: "application/json", }, - body: formData + body: formData, }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || response.statusText); } - + return await response.json(); } catch (error) { console.error("Error creating complaint:", error); @@ -39,18 +45,17 @@ export const complaintsApi = { } }, - findOne: async (id: string): Promise => { try { const response = await fetch(`${API_URL}/complaints/${id}`); const data = await response.json(); - + if (!response.ok) { - throw new Error(data.message || 'Error fetching complaint'); + throw new Error(data.message || "Error fetching complaint"); } if (!data) { - throw new Error('Complaint not found'); + throw new Error("Complaint not found"); } return data; @@ -59,4 +64,23 @@ export const complaintsApi = { throw error; } }, -}; \ No newline at end of file + + getLeaderboard: async (): Promise => { + try { + console.log("Fetching leaderboard from:", `${API_URL}/leaderboard`); + const response = await fetch(`${API_URL}/leaderboard`); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || "Error al obtener el ranking"); + } + + const data = await response.json(); + console.log("Leaderboard data:", data); + return data || []; + } catch (error) { + console.error("Error fetching leaderboard:", error); + throw error; + } + }, +}; diff --git a/components/map/ComplaintMap.tsx b/components/map/ComplaintMap.tsx index 49583dc..adcaad2 100644 --- a/components/map/ComplaintMap.tsx +++ b/components/map/ComplaintMap.tsx @@ -101,10 +101,7 @@ export const ComplaintMap: React.FC = ({ const handleMarkerPress = (group: LocationGroup) => { if (group.complaints.length === 1) { - router.push({ - pathname: "/complaint/[id]", - params: { id: group.complaints[0].id }, - }); + router.push(`/complaint/${group.complaints[0]._id}`); // Corrección aquí } else { setSelectedGroup(group); setShowModal(true); @@ -225,36 +222,62 @@ export const ComplaintMap: React.FC = ({ setShowModal(false)} - style={styles.closeButton} + hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > - + item.id} + contentContainerStyle={styles.listContainer} + ItemSeparatorComponent={() => } renderItem={({ item }) => ( { setShowModal(false); - router.push("complaint", { id: item._id }); + router.push(`/complaint/${item._id}`); }} > - - + + + + + + {item.title} + + + + + {item.location || "San Isidro"} + + + + Hace 3 días + + + + - {item.title} - )} /> @@ -266,6 +289,93 @@ export const ComplaintMap: React.FC = ({ }; const styles = StyleSheet.create({ + modalContainer: { + flex: 1, + justifyContent: "flex-end", + backgroundColor: "rgba(0, 0, 0, 0.5)", + }, + modalContent: { + backgroundColor: Colors.white_00, + borderTopLeftRadius: 24, + borderTopRightRadius: 24, + paddingTop: 20, + maxHeight: "80%", + }, + modalHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: 20, + paddingBottom: 16, + borderBottomWidth: 1, + borderBottomColor: Colors.white_40, + }, + modalTitle: { + fontSize: 18, + fontWeight: "600", + color: Colors.white_70, + }, + listContainer: { + paddingHorizontal: 16, + paddingVertical: 8, + }, + separator: { + height: 8, + }, + complaintItem: { + backgroundColor: Colors.white, + borderRadius: 12, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 3, + elevation: 3, + marginVertical: 4, + }, + complaintContent: { + flexDirection: "row", + alignItems: "center", + padding: 12, + }, + complaintIconContainer: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: Colors.white_40, + justifyContent: "center", + alignItems: "center", + marginRight: 12, + }, + complaintInfo: { + flex: 1, + marginRight: 8, + }, + complaintTitle: { + fontSize: 16, + fontWeight: "500", + color: Colors.black, + marginBottom: 4, + }, + complaintMeta: { + flexDirection: "row", + alignItems: "center", + }, + timeMeta: { + flexDirection: "row", + alignItems: "center", + marginLeft: 12, + }, + metaText: { + fontSize: 12, + color: Colors.grey, + marginLeft: 4, + }, + chevron: { + marginLeft: 8, + }, container: { height: "40%", }, @@ -338,40 +448,9 @@ const styles = StyleSheet.create({ borderRightColor: "transparent", marginTop: -2, }, - modalContainer: { - flex: 1, - justifyContent: "flex-end", - backgroundColor: "rgba(0, 0, 0, 0.5)", - }, - modalContent: { - backgroundColor: Colors.white, - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - padding: 16, - maxHeight: "70%", - }, - modalHeader: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - marginBottom: 16, - }, - modalTitle: { - fontSize: 18, - fontWeight: "600", - color: "#000000", - }, closeButton: { padding: 4, }, - complaintItem: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - padding: 12, - borderBottomWidth: 1, - borderBottomColor: "#ff3030", - }, complaintItemHeader: { flexDirection: "row", alignItems: "center", @@ -383,11 +462,6 @@ const styles = StyleSheet.create({ borderRadius: 6, marginRight: 8, }, - complaintTitle: { - fontSize: 16, - color: Colors.black, - flex: 1, - }, locationButton: { position: "absolute", bottom: 16, diff --git a/constants/api.ts b/constants/api.ts index bf32b5b..590fcec 100644 --- a/constants/api.ts +++ b/constants/api.ts @@ -1 +1,2 @@ -export const API_URL = "https://aabb-2800-200-e8f0-15c-10b-fc9f-72c3-9d5d.ngrok-free.app/api"; +export const API_URL = + "https://5e27-2800-4b0-4007-527e-8891-e0d9-babb-5154.ngrok-free.app/api"; diff --git a/pages/RankingPage.tsx b/pages/RankingPage.tsx index 73e5781..7d4e341 100644 --- a/pages/RankingPage.tsx +++ b/pages/RankingPage.tsx @@ -1,11 +1,427 @@ -import { View, Text } from "react-native"; +import React, { useEffect, useState, useCallback } from "react"; +import { + StyleSheet, + View, + Text, + ScrollView, + ActivityIndicator, + RefreshControl, + Animated, +} from "react-native"; +import { useHeaderHeight } from "@react-navigation/elements"; +import { MaterialIcons } from "@expo/vector-icons"; +import { Colors } from "@/constants"; +import { CreatePageStyle } from "@/utils"; +import { complaintsApi } from "@/api/complaints"; +import { LinearGradient } from "expo-linear-gradient"; + +interface LeaderboardUser { + complaintsCount: number; + userDni: string; + userName: string; +} + +const PodiumItem = ({ + rank, + data, + style, +}: { + rank: number; + data?: LeaderboardUser; + style?: any; +}) => { + if (!data) return null; + + const getMedalColor = () => { + switch (rank) { + case 1: + return "#FFD700"; + case 2: + return "#C0C0C0"; + case 3: + return "#CD7F32"; + default: + return Colors.white_60; + } + }; -const RankingPage = () => { return ( - - Here Ranking Page + + + + + + + {rank} + + + {data.userName} + + + + {data.complaintsCount} + + reportes + + ); }; +const EmptyState = () => ( + + + No hay datos disponibles + +); + +const RankingPage = () => { + const headerHeight = useHeaderHeight(); + const pageStyle = CreatePageStyle(headerHeight); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [leaderboardData, setLeaderboardData] = useState([]); + const [error, setError] = useState(null); + const scrollY = new Animated.Value(0); + + const fetchLeaderboard = useCallback(async () => { + try { + setError(null); + const data = await complaintsApi.getLeaderboard(); + if (Array.isArray(data)) { + setLeaderboardData(data); + } else { + console.error("Invalid data format:", data); + throw new Error("Formato de datos inválido"); + } + } catch (error) { + console.error("Error fetching leaderboard:", error); + setError( + error instanceof Error ? error.message : "Error al cargar el ranking" + ); + } finally { + setLoading(false); + setRefreshing(false); + } + }, []); + + useEffect(() => { + fetchLeaderboard(); + }, [fetchLeaderboard]); + + const onRefresh = useCallback(async () => { + setRefreshing(true); + await fetchLeaderboard(); + }, [fetchLeaderboard]); + + if (loading) { + return ( + + + Cargando ranking... + + ); + } + + if (error) { + return ( + + + {error} + + ); + } + + if (!leaderboardData || leaderboardData.length === 0) { + return ; + } + + const topThree = leaderboardData.slice(0, 3); + const restOfUsers = leaderboardData.slice(3); + + const headerOpacity = scrollY.interpolate({ + inputRange: [0, 100], + outputRange: [1, 0.9], + extrapolate: "clamp", + }); + + return ( + + } + onScroll={Animated.event( + [{ nativeEvent: { contentOffset: { y: scrollY } } }], + { useNativeDriver: true } + )} + > + + + + Top Ciudadanos Vigilantes + Ranking de reportes + + + + + + + + + + {restOfUsers.length > 0 && ( + + Otros participantes + {restOfUsers.map((item, index) => ( + + + {index + 4} + + + {item.userName} + DNI: {item.userDni} + + + {item.complaintsCount} + reportes + + + ))} + + )} + + ); +}; + +const styles = StyleSheet.create({ + centerContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, + loadingText: { + marginTop: 12, + fontSize: 14, + fontFamily: "Inter_Regular", + color: Colors.white_60, + }, + emptyText: { + marginTop: 12, + fontSize: 16, + fontFamily: "Inter_Regular", + color: Colors.white_60, + textAlign: "center", + }, + scrollContent: { + paddingBottom: 20, + }, + headerGradient: { + padding: 20, + alignItems: "center", + borderRadius: 20, + }, + header: { + marginTop: 20, + borderRadius: 20, + overflow: "hidden", + }, + title: { + fontSize: 24, + fontFamily: "Inter_Bold", + color: Colors.white_80, + marginTop: 12, + }, + subtitle: { + fontSize: 14, + fontFamily: "Inter_Regular", + color: Colors.white_60, + marginTop: 4, + }, + listTitle: { + fontSize: 18, + fontFamily: "Inter_SemiBold", + color: Colors.white_80, + marginBottom: 12, + }, + podiumContainer: { + flexDirection: "row", + justifyContent: "center", + alignItems: "flex-end", + paddingHorizontal: 10, + marginBottom: 32, + }, + podiumItemContainer: { + flex: 1, + alignItems: "center", + }, + podiumItem: { + width: "94%", + borderRadius: 20, + padding: 12, + alignItems: "center", + }, + firstPlace: { + paddingVertical: 24, + marginTop: -20, + zIndex: 3, + }, + secondPlace: { + paddingVertical: 20, + marginTop: -10, + zIndex: 2, + }, + thirdPlace: { + paddingVertical: 16, + zIndex: 1, + }, + medalContainer: { + padding: 12, + borderRadius: 16, + marginBottom: 8, + }, + position: { + fontSize: 20, + fontFamily: "Inter_Bold", + }, + firstPosition: { + fontSize: 28, + }, + name: { + fontSize: 14, + fontFamily: "Inter_SemiBold", + color: Colors.white_80, + marginVertical: 4, + textAlign: "center", + width: "100%", + }, + firstName: { + fontSize: 16, + }, + countContainer: { + padding: 8, + borderRadius: 12, + alignItems: "center", + marginTop: 8, + minWidth: 80, + }, + count: { + fontSize: 18, + fontFamily: "Inter_Bold", + }, + firstCount: { + fontSize: 24, + }, + label: { + fontSize: 12, + fontFamily: "Inter_Regular", + color: Colors.white_60, + }, + rankingCard: { + flexDirection: "row", + alignItems: "center", + borderRadius: 16, + padding: 12, + marginBottom: 8, + }, + rankingPositionContainer: { + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: Colors.white_10, + justifyContent: "center", + alignItems: "center", + }, + rankingPosition: { + fontSize: 16, + fontFamily: "Inter_Bold", + color: Colors.white_60, + }, + rankingInfo: { + flex: 1, + marginLeft: 12, + }, + rankingName: { + fontSize: 16, + fontFamily: "Inter_SemiBold", + color: Colors.white_80, + }, + rankingDni: { + fontSize: 12, + fontFamily: "Inter_Regular", + color: Colors.white_60, + }, + scoreContainer: { + alignItems: "center", + backgroundColor: Colors.white_10, + padding: 8, + borderRadius: 12, + minWidth: 70, + }, + scoreText: { + fontSize: 18, + fontFamily: "Inter_Bold", + color: Colors.blue_60, + }, + scoreLabel: { + fontSize: 10, + fontFamily: "Inter_Regular", + color: Colors.white_60, + }, + errorText: { + color: Colors.white_60, + marginTop: 8, + fontSize: 16, + fontFamily: "Inter_Regular", + textAlign: "center", + }, +}); + export default RankingPage;