635 lines
18 KiB
TypeScript
635 lines
18 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator, Modal } 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<SearchHistoryItem[]>([]);
|
|
|
|
// Config modal state
|
|
const [showConfig, setShowConfig] = useState(false);
|
|
const [configFirstName, setConfigFirstName] = useState('');
|
|
const [configLastName, setConfigLastName] = useState('');
|
|
const [configEmail, setConfigEmail] = useState('');
|
|
const [configCity, setConfigCity] = useState('');
|
|
const [configAddress, setConfigAddress] = useState('');
|
|
const [configSaving, setConfigSaving] = useState(false);
|
|
const [configFeedback, setConfigFeedback] = 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);
|
|
}
|
|
}
|
|
|
|
function openConfig() {
|
|
setConfigFirstName(user?.first_name || '');
|
|
setConfigLastName(user?.last_name || '');
|
|
setConfigEmail(user?.email || '');
|
|
setConfigCity(user?.city || '');
|
|
setConfigAddress(user?.address || '');
|
|
setConfigFeedback(null);
|
|
setShowConfig(true);
|
|
}
|
|
|
|
async function handleConfigSave() {
|
|
setConfigSaving(true);
|
|
setConfigFeedback(null);
|
|
try {
|
|
const res = await fetch('/api/users/me', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
first_name: configFirstName.trim() || null,
|
|
last_name: configLastName.trim() || null,
|
|
email: configEmail.trim() || null,
|
|
city: configCity.trim() || null,
|
|
address: configAddress.trim() || null,
|
|
}),
|
|
});
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
throw new Error(err.error || 'Error al guardar');
|
|
}
|
|
const updated = await res.json();
|
|
setFirstName(updated.first_name || '');
|
|
setLastName(updated.last_name || '');
|
|
setConfigFeedback({ type: 'ok', text: 'Perfil guardado.' });
|
|
setTimeout(() => setShowConfig(false), 1200);
|
|
} catch (err: any) {
|
|
setConfigFeedback({ type: 'err', text: err.message || 'Error al guardar' });
|
|
} finally {
|
|
setConfigSaving(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) {
|
|
const updated = await res.json();
|
|
// Trigger auth refresh to update user state
|
|
}
|
|
} catch (err) {
|
|
console.error('Error saving avatar:', err);
|
|
}
|
|
}
|
|
}
|
|
|
|
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',
|
|
style: 'destructive',
|
|
onPress: async () => {
|
|
await logout();
|
|
router.replace('/auth/login');
|
|
}
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.loadingText}>Cargando...</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<View style={styles.authPrompt}>
|
|
<Ionicons name="person-outline" size={64} color={colors.textSecondary} />
|
|
<Text style={styles.authTitle}>Inicia Sesión</Text>
|
|
<Text style={styles.authSubtitle}>
|
|
Inicia sesión para acceder a todas las funcionalidades
|
|
</Text>
|
|
<TouchableOpacity
|
|
style={styles.button}
|
|
onPress={() => router.push('/auth/login')}
|
|
>
|
|
<Text style={styles.buttonText}>Iniciar Sesión</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={styles.linkButton}
|
|
onPress={() => router.push('/auth/register')}
|
|
>
|
|
<Text style={styles.linkText}>Crear cuenta nueva</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ScrollView style={styles.container}>
|
|
{/* Avatar Section */}
|
|
<View style={styles.avatarSection}>
|
|
<TouchableOpacity style={styles.avatarContainer} onPress={handlePickImage}>
|
|
{avatarUrl ? (
|
|
<Image source={{ uri: avatarUrl }} style={styles.avatarImage} />
|
|
) : (
|
|
<Ionicons name="person" size={40} color={colors.primary} />
|
|
)}
|
|
<View style={styles.avatarOverlay}>
|
|
<Ionicons name="camera" size={24} color="white" />
|
|
</View>
|
|
</TouchableOpacity>
|
|
<Text style={styles.editAvatarText}>Toca para cambiar foto</Text>
|
|
</View>
|
|
|
|
{/* Read-only Name Section */}
|
|
<View style={styles.infoSection}>
|
|
<View style={styles.infoCard}>
|
|
<Text style={styles.infoLabel}>Nombre</Text>
|
|
<Text style={styles.infoValue}>{firstName || '—'}</Text>
|
|
</View>
|
|
<View style={styles.infoCard}>
|
|
<Text style={styles.infoLabel}>Apellidos</Text>
|
|
<Text style={styles.infoValue}>{lastName || '—'}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Menu Section */}
|
|
<View style={styles.menuSection}>
|
|
<TouchableOpacity style={styles.menuItem} onPress={openConfig}>
|
|
<Ionicons name="settings" size={24} color={colors.textSecondary} />
|
|
<Text style={styles.menuText}>Configuración</Text>
|
|
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
|
|
</TouchableOpacity>
|
|
|
|
<View style={styles.menuItem}>
|
|
<Ionicons name="location" size={24} color={colors.primary} />
|
|
<Text style={styles.menuText}>Mis Direcciones</Text>
|
|
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
|
|
</View>
|
|
|
|
{searchHistory.length > 0 && (
|
|
<View style={styles.searchHistorySection}>
|
|
<Text style={styles.searchHistoryTitle}>Tus búsquedas recientes:</Text>
|
|
{searchHistory.map((item) => (
|
|
<View key={item.id} style={styles.searchItem}>
|
|
<Text style={styles.searchAddress}>{item.address}</Text>
|
|
<TouchableOpacity
|
|
onPress={() => handleDeleteSearch(item.id)}
|
|
style={styles.searchDelete}
|
|
>
|
|
<Ionicons name="close-circle" size={20} color={colors.textSecondary} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{isAdmin && (
|
|
<TouchableOpacity style={styles.menuItem}>
|
|
<Ionicons name="settings" size={24} color={colors.text} />
|
|
<Text style={styles.menuText}>Panel Admin</Text>
|
|
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
|
|
{/* Logout Button */}
|
|
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
|
|
<Ionicons name="log-out" size={20} color={colors.danger} />
|
|
<Text style={styles.logoutText}>Cerrar Sesión</Text>
|
|
</TouchableOpacity>
|
|
|
|
{/* Config Modal */}
|
|
<Modal visible={showConfig} animationType="slide" transparent>
|
|
<View style={styles.modalBackdrop}>
|
|
<View style={styles.modalContent}>
|
|
<View style={styles.modalHeader}>
|
|
<Text style={styles.modalTitle}>Configuración</Text>
|
|
<TouchableOpacity onPress={() => setShowConfig(false)}>
|
|
<Ionicons name="close" size={24} color={colors.textSecondary} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
<ScrollView style={styles.modalBody}>
|
|
<View style={styles.modalField}>
|
|
<Text style={styles.modalLabel}>Nombre</Text>
|
|
<TextInput
|
|
style={styles.modalInput}
|
|
value={configFirstName}
|
|
onChangeText={setConfigFirstName}
|
|
placeholder="Tu nombre"
|
|
editable={!configSaving}
|
|
/>
|
|
</View>
|
|
<View style={styles.modalField}>
|
|
<Text style={styles.modalLabel}>Apellidos</Text>
|
|
<TextInput
|
|
style={styles.modalInput}
|
|
value={configLastName}
|
|
onChangeText={setConfigLastName}
|
|
placeholder="Tus apellidos"
|
|
editable={!configSaving}
|
|
/>
|
|
</View>
|
|
<View style={styles.modalField}>
|
|
<Text style={styles.modalLabel}>Correo electrónico</Text>
|
|
<TextInput
|
|
style={styles.modalInput}
|
|
value={configEmail}
|
|
onChangeText={setConfigEmail}
|
|
placeholder="tu@email.com"
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
editable={!configSaving}
|
|
/>
|
|
</View>
|
|
<View style={styles.modalField}>
|
|
<Text style={styles.modalLabel}>Ciudad</Text>
|
|
<TextInput
|
|
style={styles.modalInput}
|
|
value={configCity}
|
|
onChangeText={setConfigCity}
|
|
placeholder="Tu ciudad"
|
|
editable={!configSaving}
|
|
/>
|
|
</View>
|
|
<View style={styles.modalField}>
|
|
<Text style={styles.modalLabel}>Dirección</Text>
|
|
<TextInput
|
|
style={styles.modalInput}
|
|
value={configAddress}
|
|
onChangeText={setConfigAddress}
|
|
placeholder="Calle Mayor 1, Madrid"
|
|
editable={!configSaving}
|
|
/>
|
|
</View>
|
|
|
|
{configFeedback && (
|
|
<View style={[styles.modalFeedback, configFeedback.type === 'ok' ? styles.modalFeedbackOk : styles.modalFeedbackErr]}>
|
|
<Text style={styles.modalFeedbackText}>{configFeedback.text}</Text>
|
|
</View>
|
|
)}
|
|
|
|
<View style={styles.modalActions}>
|
|
<TouchableOpacity
|
|
style={styles.modalCancelBtn}
|
|
onPress={() => setShowConfig(false)}
|
|
disabled={configSaving}
|
|
>
|
|
<Text style={styles.modalCancelText}>Cancelar</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[styles.modalSaveBtn, configSaving && styles.modalSaveBtnDisabled]}
|
|
onPress={handleConfigSave}
|
|
disabled={configSaving}
|
|
>
|
|
{configSaving ? (
|
|
<ActivityIndicator color={colors.textInverse} />
|
|
) : (
|
|
<Text style={styles.modalSaveText}>Guardar</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</ScrollView>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: colors.background,
|
|
},
|
|
loadingText: {
|
|
textAlign: 'center',
|
|
marginTop: spacing.xxl,
|
|
color: colors.textSecondary,
|
|
},
|
|
authPrompt: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
padding: spacing.xl,
|
|
},
|
|
authTitle: {
|
|
fontSize: 24,
|
|
fontWeight: 'bold',
|
|
color: colors.text,
|
|
marginTop: spacing.lg,
|
|
},
|
|
authSubtitle: {
|
|
fontSize: 16,
|
|
color: colors.textSecondary,
|
|
textAlign: 'center',
|
|
marginTop: spacing.sm,
|
|
marginBottom: spacing.xl,
|
|
},
|
|
avatarSection: {
|
|
alignItems: 'center',
|
|
padding: spacing.xl,
|
|
backgroundColor: colors.card,
|
|
},
|
|
avatarContainer: {
|
|
width: 100,
|
|
height: 100,
|
|
borderRadius: 50,
|
|
backgroundColor: colors.background,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
overflow: 'hidden',
|
|
},
|
|
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,
|
|
},
|
|
infoSection: {
|
|
padding: spacing.xl,
|
|
backgroundColor: colors.card,
|
|
marginTop: spacing.md,
|
|
gap: spacing.md,
|
|
},
|
|
infoCard: {
|
|
backgroundColor: colors.background,
|
|
padding: spacing.md,
|
|
borderRadius: borderRadius.md,
|
|
borderWidth: 1,
|
|
borderColor: colors.border || '#E0E0E0',
|
|
},
|
|
infoLabel: {
|
|
fontSize: 12,
|
|
fontWeight: '600',
|
|
color: colors.textSecondary,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.05,
|
|
marginBottom: 4,
|
|
},
|
|
infoValue: {
|
|
fontSize: 16,
|
|
fontWeight: '500',
|
|
color: colors.text,
|
|
},
|
|
menuSection: {
|
|
marginTop: spacing.md,
|
|
backgroundColor: colors.card,
|
|
},
|
|
menuItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
padding: spacing.md,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.separator,
|
|
},
|
|
menuText: {
|
|
flex: 1,
|
|
marginLeft: spacing.md,
|
|
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,
|
|
paddingVertical: spacing.md,
|
|
paddingHorizontal: spacing.xl,
|
|
},
|
|
buttonText: {
|
|
color: colors.textInverse,
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
},
|
|
linkButton: {
|
|
marginTop: spacing.md,
|
|
},
|
|
linkText: {
|
|
color: colors.primary,
|
|
fontSize: 14,
|
|
},
|
|
logoutButton: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
margin: spacing.xl,
|
|
padding: spacing.md,
|
|
backgroundColor: colors.card,
|
|
borderRadius: borderRadius.md,
|
|
borderWidth: 1,
|
|
borderColor: colors.danger,
|
|
},
|
|
logoutText: {
|
|
marginLeft: spacing.sm,
|
|
color: colors.danger,
|
|
fontSize: 16,
|
|
fontWeight: '500',
|
|
},
|
|
// Modal styles
|
|
modalBackdrop: {
|
|
flex: 1,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
justifyContent: 'flex-end',
|
|
},
|
|
modalContent: {
|
|
backgroundColor: colors.card,
|
|
borderTopLeftRadius: 20,
|
|
borderTopRightRadius: 20,
|
|
maxHeight: '90%',
|
|
},
|
|
modalHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
padding: spacing.lg,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.separator,
|
|
},
|
|
modalTitle: {
|
|
fontSize: 18,
|
|
fontWeight: 'bold',
|
|
color: colors.text,
|
|
},
|
|
modalBody: {
|
|
padding: spacing.lg,
|
|
},
|
|
modalField: {
|
|
marginBottom: spacing.md,
|
|
},
|
|
modalLabel: {
|
|
fontSize: 13,
|
|
fontWeight: '600',
|
|
color: colors.textSecondary,
|
|
marginBottom: spacing.xs,
|
|
},
|
|
modalInput: {
|
|
borderWidth: 1,
|
|
borderColor: colors.border || '#E0E0E0',
|
|
borderRadius: borderRadius.md,
|
|
padding: spacing.md,
|
|
fontSize: 16,
|
|
color: colors.text,
|
|
},
|
|
modalFeedback: {
|
|
padding: spacing.md,
|
|
borderRadius: borderRadius.md,
|
|
marginBottom: spacing.md,
|
|
},
|
|
modalFeedbackOk: {
|
|
backgroundColor: 'rgba(0, 69, 13, 0.1)',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(0, 69, 13, 0.2)',
|
|
},
|
|
modalFeedbackErr: {
|
|
backgroundColor: 'rgba(186, 26, 26, 0.1)',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(186, 26, 26, 0.2)',
|
|
},
|
|
modalFeedbackText: {
|
|
fontSize: 14,
|
|
color: colors.text,
|
|
},
|
|
modalActions: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'flex-end',
|
|
gap: spacing.md,
|
|
marginTop: spacing.md,
|
|
},
|
|
modalCancelBtn: {
|
|
paddingVertical: spacing.md,
|
|
paddingHorizontal: spacing.lg,
|
|
borderRadius: borderRadius.md,
|
|
borderWidth: 1,
|
|
borderColor: colors.border || '#E0E0E0',
|
|
},
|
|
modalCancelText: {
|
|
fontSize: 16,
|
|
color: colors.text,
|
|
},
|
|
modalSaveBtn: {
|
|
backgroundColor: colors.primary,
|
|
paddingVertical: spacing.md,
|
|
paddingHorizontal: spacing.xl,
|
|
borderRadius: borderRadius.md,
|
|
minWidth: 100,
|
|
alignItems: 'center',
|
|
},
|
|
modalSaveBtnDisabled: {
|
|
opacity: 0.6,
|
|
},
|
|
modalSaveText: {
|
|
color: colors.textInverse,
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
},
|
|
});
|