fix: resolve 6 frontend-mobile build and runtime issues
- Add missing User type fields (first_name, last_name, email, avatar_url, city, address) used in profile.tsx - Fix expo-constants import to use namespace import (consistent with rest of codebase) - Replace placeholder production API URL with configurable value - Fix avatar preset/color selection passing require() IDs to Image uri by tracking local source separately - Replace all relative fetch() calls with configured axios instance (relative URLs don't resolve in React Native)
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator, Modal, Pressable } from 'react-native';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert, TextInput, ScrollView, Image, ActivityIndicator, Modal, Pressable, ImageSourcePropType } 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';
|
||||
import api from '../../services/api';
|
||||
|
||||
const AVATARS = [
|
||||
require('../../assets/avatars/avatar1.png'),
|
||||
@@ -49,9 +50,17 @@ export default function ProfileScreen() {
|
||||
const [firstName, setFirstName] = useState(user?.first_name || '');
|
||||
const [lastName, setLastName] = useState(user?.last_name || '');
|
||||
const [avatarUrl, setAvatarUrl] = useState(user?.avatar_url || '');
|
||||
const [avatarLocalSource, setAvatarLocalSource] = useState<number | null>(null);
|
||||
const [searchHistory, setSearchHistory] = useState<SearchHistoryItem[]>([]);
|
||||
|
||||
function getAvatarSource(): ImageSourcePropType {
|
||||
if (!avatarUrl) return { uri: '' };
|
||||
const presetMatch = avatarUrl.match(/^preset_avatar_(\d+)$/);
|
||||
if (presetMatch) return AVATARS[parseInt(presetMatch[1], 10) - 1];
|
||||
const colorMatch = avatarUrl.match(/^color_circle_(\d+)$/);
|
||||
if (colorMatch) return COLOR_CIRCLES[parseInt(colorMatch[1], 10) - 1];
|
||||
return { uri: avatarUrl };
|
||||
}
|
||||
|
||||
// Avatar modal state
|
||||
const [showAvatarModal, setShowAvatarModal] = useState(false);
|
||||
const [avatarTab, setAvatarTab] = useState<'presets' | 'colors' | 'upload'>('presets');
|
||||
@@ -92,11 +101,8 @@ export default function ProfileScreen() {
|
||||
|
||||
async function loadSearchHistory() {
|
||||
try {
|
||||
const res = await fetch('/api/search-history', { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setSearchHistory(data);
|
||||
}
|
||||
const { data } = await api.get('/search-history');
|
||||
setSearchHistory(data);
|
||||
} catch (err) {
|
||||
console.error('Error loading search history:', err);
|
||||
}
|
||||
@@ -116,29 +122,20 @@ export default function ProfileScreen() {
|
||||
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,
|
||||
}),
|
||||
const { data: updated } = await api.put('/users/me', {
|
||||
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' });
|
||||
const msg = err.response?.data?.error || err.message || 'Error al guardar';
|
||||
setConfigFeedback({ type: 'err', text: msg });
|
||||
} finally {
|
||||
setConfigSaving(false);
|
||||
}
|
||||
@@ -156,16 +153,7 @@ export default function ProfileScreen() {
|
||||
setAvatarUrl(result.assets[0].uri);
|
||||
setShowAvatarModal(false);
|
||||
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
|
||||
}
|
||||
await api.put('/users/me', { avatar_url: result.assets[0].uri });
|
||||
} catch (err) {
|
||||
console.error('Error saving avatar:', err);
|
||||
}
|
||||
@@ -189,15 +177,7 @@ export default function ProfileScreen() {
|
||||
setAvatarUrl(result.assets[0].uri);
|
||||
setShowAvatarModal(false);
|
||||
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();
|
||||
}
|
||||
await api.put('/users/me', { avatar_url: result.assets[0].uri });
|
||||
} catch (err) {
|
||||
console.error('Error saving avatar:', err);
|
||||
}
|
||||
@@ -205,38 +185,22 @@ export default function ProfileScreen() {
|
||||
}
|
||||
|
||||
async function handleSelectPresetAvatar(index: number) {
|
||||
const avatarSource = AVATARS[index];
|
||||
setAvatarUrl(avatarSource);
|
||||
const avatarValue = `preset_avatar_${index + 1}`;
|
||||
setAvatarUrl(avatarValue);
|
||||
setShowAvatarModal(false);
|
||||
try {
|
||||
const res = await fetch('/api/users/me', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ avatar_url: `preset_avatar_${index + 1}` }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
}
|
||||
await api.put('/users/me', { avatar_url: avatarValue });
|
||||
} catch (err) {
|
||||
console.error('Error saving avatar:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSelectColor(index: number) {
|
||||
const colorSource = COLOR_CIRCLES[index];
|
||||
setAvatarUrl(colorSource);
|
||||
const colorValue = `color_circle_${index + 1}`;
|
||||
setAvatarUrl(colorValue);
|
||||
setShowAvatarModal(false);
|
||||
try {
|
||||
const res = await fetch('/api/users/me', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ avatar_url: `color_circle_${index + 1}` }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const updated = await res.json();
|
||||
}
|
||||
await api.put('/users/me', { avatar_url: colorValue });
|
||||
} catch (err) {
|
||||
console.error('Error saving avatar:', err);
|
||||
}
|
||||
@@ -244,13 +208,8 @@ export default function ProfileScreen() {
|
||||
|
||||
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));
|
||||
}
|
||||
await api.delete(`/search-history/${id}`);
|
||||
setSearchHistory(prev => prev.filter(item => item.id !== id));
|
||||
} catch (err) {
|
||||
console.error('Error deleting search:', err);
|
||||
}
|
||||
@@ -260,11 +219,8 @@ export default function ProfileScreen() {
|
||||
async function loadAddresses() {
|
||||
setAddressesLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/addresses', { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setAddresses(data);
|
||||
}
|
||||
const { data } = await api.get('/addresses');
|
||||
setAddresses(data);
|
||||
} catch (err) {
|
||||
console.error('Error loading addresses:', err);
|
||||
} finally {
|
||||
@@ -304,27 +260,19 @@ export default function ProfileScreen() {
|
||||
setFormSaving(true);
|
||||
setFormError('');
|
||||
try {
|
||||
const url = editingAddressId ? `/api/addresses/${editingAddressId}` : '/api/addresses';
|
||||
const method = editingAddressId ? 'PUT' : 'POST';
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
address: addr,
|
||||
label: formLabel.trim(),
|
||||
is_default: formDefault,
|
||||
}),
|
||||
const url = editingAddressId ? `/addresses/${editingAddressId}` : '/addresses';
|
||||
const method = editingAddressId ? 'put' : 'post';
|
||||
await api[method](url, {
|
||||
address: addr,
|
||||
label: formLabel.trim(),
|
||||
is_default: formDefault,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || 'Error al guardar la dirección');
|
||||
}
|
||||
setShowAddressForm(false);
|
||||
setEditingAddressId(null);
|
||||
loadAddresses();
|
||||
} catch (err: any) {
|
||||
setFormError(err.message);
|
||||
const msg = err.response?.data?.error || err.message || 'Error al guardar la dirección';
|
||||
setFormError(msg);
|
||||
} finally {
|
||||
setFormSaving(false);
|
||||
}
|
||||
@@ -332,13 +280,8 @@ export default function ProfileScreen() {
|
||||
|
||||
async function handleDeleteAddress(id: number) {
|
||||
try {
|
||||
const res = await fetch(`/api/addresses/${id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (res.ok) {
|
||||
setAddresses(prev => prev.filter(a => a.id !== id));
|
||||
}
|
||||
await api.delete(`/addresses/${id}`);
|
||||
setAddresses(prev => prev.filter(a => a.id !== id));
|
||||
} catch (err) {
|
||||
console.error('Error deleting address:', err);
|
||||
}
|
||||
@@ -346,13 +289,8 @@ export default function ProfileScreen() {
|
||||
|
||||
async function handleSetDefault(id: number) {
|
||||
try {
|
||||
const res = await fetch(`/api/addresses/${id}/default`, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (res.ok) {
|
||||
loadAddresses();
|
||||
}
|
||||
await api.put(`/addresses/${id}/default`);
|
||||
loadAddresses();
|
||||
} catch (err) {
|
||||
console.error('Error setting default address:', err);
|
||||
}
|
||||
@@ -416,7 +354,7 @@ export default function ProfileScreen() {
|
||||
<View style={styles.avatarSection}>
|
||||
<TouchableOpacity style={styles.avatarContainer} onPress={() => setShowAvatarModal(true)}>
|
||||
{avatarUrl ? (
|
||||
<Image source={{ uri: avatarUrl }} style={styles.avatarImage} />
|
||||
<Image source={getAvatarSource()} style={styles.avatarImage} />
|
||||
) : (
|
||||
<Ionicons name="person" size={40} color={colors.primary} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user