diff --git a/apps/frontend-mobile/app/(tabs)/profile.tsx b/apps/frontend-mobile/app/(tabs)/profile.tsx index f471c2e..3353067 100644 --- a/apps/frontend-mobile/app/(tabs)/profile.tsx +++ b/apps/frontend-mobile/app/(tabs)/profile.tsx @@ -1,22 +1,128 @@ -import React from 'react'; -import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native'; +import React, { useState, useEffect } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator } from 'react-native'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; +import * as ImagePicker from 'expo-image-picker'; import { useAuth } from '../../hooks/useAuth'; import { colors, spacing, borderRadius } from '../../constants/theme'; +interface SearchHistoryItem { + id: number; + address: string; + latitude: number | null; + longitude: number | null; + created_at: string; +} + export default function ProfileScreen() { const router = useRouter(); const { user, isAuthenticated, isLoading, logout, isAdmin } = useAuth(); + const [firstName, setFirstName] = useState(user?.first_name || ''); + const [lastName, setLastName] = useState(user?.last_name || ''); + const [avatarUrl, setAvatarUrl] = useState(user?.avatar_url || ''); + const [searchHistory, setSearchHistory] = useState([]); + const [saving, setSaving] = useState(false); + const [feedback, setFeedback] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); + + useEffect(() => { + if (isAuthenticated) { + loadSearchHistory(); + } + }, [isAuthenticated]); + + useEffect(() => { + setFirstName(user?.first_name || ''); + setLastName(user?.last_name || ''); + setAvatarUrl(user?.avatar_url || ''); + }, [user]); + + async function loadSearchHistory() { + try { + const res = await fetch('/api/search-history', { credentials: 'include' }); + if (res.ok) { + const data = await res.json(); + setSearchHistory(data); + } + } catch (err) { + console.error('Error loading search history:', err); + } + } + + async function handleSave() { + setSaving(true); + setFeedback(null); + try { + const res = await fetch('/api/users/me', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ + first_name: firstName.trim() || null, + last_name: lastName.trim() || null, + avatar_url: avatarUrl || null, + }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error || 'Error al guardar'); + } + setFeedback({ type: 'ok', text: 'Perfil guardado.' }); + } catch (err: any) { + setFeedback({ type: 'err', text: err.message || 'Error al guardar' }); + } finally { + setSaving(false); + } + } + + async function handlePickImage() { + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: 0.8, + }); + + if (!result.canceled && result.assets[0]) { + setAvatarUrl(result.assets[0].uri); + try { + const res = await fetch('/api/users/me', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ avatar_url: result.assets[0].uri }), + }); + if (res.ok) { + setFeedback({ type: 'ok', text: 'Foto de perfil actualizada.' }); + } + } catch (err) { + setFeedback({ type: 'err', text: 'Error al guardar la foto' }); + } + } + } + + async function handleDeleteSearch(id: number) { + try { + const res = await fetch(`/api/search-history/${id}`, { + method: 'DELETE', + credentials: 'include', + }); + if (res.ok) { + setSearchHistory(prev => prev.filter(item => item.id !== id)); + } + } catch (err) { + console.error('Error deleting search:', err); + } + } + const handleLogout = async () => { Alert.alert( 'Cerrar Sesión', '¿Estás seguro que deseas cerrar sesión?', [ { text: 'Cancelar', style: 'cancel' }, - { - text: 'Cerrar Sesión', + { + text: 'Cerrar Sesión', style: 'destructive', onPress: async () => { await logout(); @@ -62,16 +168,89 @@ export default function ProfileScreen() { } return ( - - - - - - {user?.username} - {isAdmin && Administrador} + + {/* Avatar Section */} + + + {avatarUrl ? ( + + ) : ( + + )} + + + + + Toca para cambiar foto + {/* Editable Fields */} + + + Nombre + + + + Apellidos + + + + {feedback && ( + + {feedback.text} + + )} + + + {saving ? ( + + ) : ( + Guardar Cambios + )} + + + + {/* Menu Section */} + router.push('/saved')}> + + Mis Direcciones + + + + {searchHistory.length > 0 && ( + + Tus búsquedas recientes: + {searchHistory.map((item) => ( + + {item.address} + handleDeleteSearch(item.id)} + style={styles.searchDelete} + > + + + + ))} + + )} + {isAdmin && ( @@ -79,31 +258,14 @@ export default function ProfileScreen() { )} - - - - Favoritos - - - - - - Notificaciones - - - - - - Ayuda - - + {/* Logout Button */} Cerrar Sesión - + ); } @@ -136,33 +298,94 @@ const styles = StyleSheet.create({ marginTop: spacing.sm, marginBottom: spacing.xl, }, - header: { + avatarSection: { alignItems: 'center', padding: spacing.xl, backgroundColor: colors.card, }, - avatar: { - width: 80, - height: 80, - borderRadius: 40, + avatarContainer: { + width: 100, + height: 100, + borderRadius: 50, backgroundColor: colors.background, justifyContent: 'center', alignItems: 'center', + overflow: 'hidden', }, - username: { - fontSize: 20, - fontWeight: 'bold', - color: colors.text, + avatarImage: { + width: 100, + height: 100, + borderRadius: 50, + }, + avatarOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + opacity: 0.8, + }, + editAvatarText: { + marginTop: spacing.sm, + color: colors.textSecondary, + fontSize: 14, + }, + formSection: { + padding: spacing.xl, + backgroundColor: colors.card, marginTop: spacing.md, }, - adminBadge: { - fontSize: 12, - color: colors.primary, - backgroundColor: '#E3F2FD', - paddingHorizontal: spacing.sm, - paddingVertical: spacing.xs, - borderRadius: borderRadius.sm, - marginTop: spacing.sm, + inputGroup: { + marginBottom: spacing.md, + }, + label: { + fontSize: 14, + fontWeight: '600', + color: colors.textSecondary, + marginBottom: spacing.xs, + }, + input: { + borderWidth: 1, + borderColor: colors.border || '#E0E0E0', + borderRadius: borderRadius.md, + padding: spacing.md, + fontSize: 16, + color: colors.text, + }, + feedback: { + padding: spacing.md, + borderRadius: borderRadius.md, + marginBottom: spacing.md, + }, + feedbackOk: { + backgroundColor: 'rgba(0, 69, 13, 0.1)', + borderWidth: 1, + borderColor: 'rgba(0, 69, 13, 0.2)', + }, + feedbackErr: { + backgroundColor: 'rgba(186, 26, 26, 0.1)', + borderWidth: 1, + borderColor: 'rgba(186, 26, 26, 0.2)', + }, + feedbackText: { + fontSize: 14, + }, + saveButton: { + backgroundColor: colors.primary, + borderRadius: borderRadius.md, + padding: spacing.md, + alignItems: 'center', + }, + saveButtonDisabled: { + opacity: 0.6, + }, + saveButtonText: { + color: colors.textInverse, + fontSize: 16, + fontWeight: '600', }, menuSection: { marginTop: spacing.md, @@ -181,6 +404,33 @@ const styles = StyleSheet.create({ fontSize: 16, color: colors.text, }, + searchHistorySection: { + padding: spacing.md, + backgroundColor: colors.background, + borderBottomWidth: 1, + borderBottomColor: colors.separator, + }, + searchHistoryTitle: { + fontSize: 14, + color: colors.textSecondary, + marginBottom: spacing.sm, + }, + searchItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: spacing.sm, + borderBottomWidth: 1, + borderBottomColor: colors.separator, + }, + searchAddress: { + flex: 1, + fontSize: 14, + color: colors.text, + }, + searchDelete: { + padding: spacing.xs, + }, button: { backgroundColor: colors.primary, borderRadius: borderRadius.md, diff --git a/apps/frontend-mobile/package.json b/apps/frontend-mobile/package.json index c49d1fc..10102c9 100644 --- a/apps/frontend-mobile/package.json +++ b/apps/frontend-mobile/package.json @@ -12,6 +12,7 @@ "expo-constants": "~57.0.3", "expo-dev-client": "~57.0.5", "expo-device": "~7.0.2", + "expo-image-picker": "~57.0.2", "expo-linking": "~57.0.1", "expo-local-authentication": "~57.0.0", "expo-notifications": "~57.0.3",