fix: resolve 6 frontend-mobile build and runtime issues
Run Tests on Branches / Backend Tests (push) Successful in 1m56s
Run Tests on Branches / Frontend Tests (push) Successful in 1m54s

- 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:
Antoni Nuñez Romeu
2026-07-07 16:07:15 +02:00
parent fc12582814
commit 2843b61f82
3 changed files with 54 additions and 110 deletions
+46 -108
View File
@@ -1,10 +1,11 @@
import React, { useState, useEffect } from 'react'; 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 { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import { useAuth } from '../../hooks/useAuth'; import { useAuth } from '../../hooks/useAuth';
import { colors, spacing, borderRadius } from '../../constants/theme'; import { colors, spacing, borderRadius } from '../../constants/theme';
import api from '../../services/api';
const AVATARS = [ const AVATARS = [
require('../../assets/avatars/avatar1.png'), require('../../assets/avatars/avatar1.png'),
@@ -49,9 +50,17 @@ export default function ProfileScreen() {
const [firstName, setFirstName] = useState(user?.first_name || ''); const [firstName, setFirstName] = useState(user?.first_name || '');
const [lastName, setLastName] = useState(user?.last_name || ''); const [lastName, setLastName] = useState(user?.last_name || '');
const [avatarUrl, setAvatarUrl] = useState(user?.avatar_url || ''); const [avatarUrl, setAvatarUrl] = useState(user?.avatar_url || '');
const [avatarLocalSource, setAvatarLocalSource] = useState<number | null>(null);
const [searchHistory, setSearchHistory] = useState<SearchHistoryItem[]>([]); 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 // Avatar modal state
const [showAvatarModal, setShowAvatarModal] = useState(false); const [showAvatarModal, setShowAvatarModal] = useState(false);
const [avatarTab, setAvatarTab] = useState<'presets' | 'colors' | 'upload'>('presets'); const [avatarTab, setAvatarTab] = useState<'presets' | 'colors' | 'upload'>('presets');
@@ -92,11 +101,8 @@ export default function ProfileScreen() {
async function loadSearchHistory() { async function loadSearchHistory() {
try { try {
const res = await fetch('/api/search-history', { credentials: 'include' }); const { data } = await api.get('/search-history');
if (res.ok) { setSearchHistory(data);
const data = await res.json();
setSearchHistory(data);
}
} catch (err) { } catch (err) {
console.error('Error loading search history:', err); console.error('Error loading search history:', err);
} }
@@ -116,29 +122,20 @@ export default function ProfileScreen() {
setConfigSaving(true); setConfigSaving(true);
setConfigFeedback(null); setConfigFeedback(null);
try { try {
const res = await fetch('/api/users/me', { const { data: updated } = await api.put('/users/me', {
method: 'PUT', first_name: configFirstName.trim() || null,
headers: { 'Content-Type': 'application/json' }, last_name: configLastName.trim() || null,
credentials: 'include', email: configEmail.trim() || null,
body: JSON.stringify({ city: configCity.trim() || null,
first_name: configFirstName.trim() || null, address: configAddress.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 || ''); setFirstName(updated.first_name || '');
setLastName(updated.last_name || ''); setLastName(updated.last_name || '');
setConfigFeedback({ type: 'ok', text: 'Perfil guardado.' }); setConfigFeedback({ type: 'ok', text: 'Perfil guardado.' });
setTimeout(() => setShowConfig(false), 1200); setTimeout(() => setShowConfig(false), 1200);
} catch (err: any) { } 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 { } finally {
setConfigSaving(false); setConfigSaving(false);
} }
@@ -156,16 +153,7 @@ export default function ProfileScreen() {
setAvatarUrl(result.assets[0].uri); setAvatarUrl(result.assets[0].uri);
setShowAvatarModal(false); setShowAvatarModal(false);
try { try {
const res = await fetch('/api/users/me', { await api.put('/users/me', { avatar_url: result.assets[0].uri });
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) { } catch (err) {
console.error('Error saving avatar:', err); console.error('Error saving avatar:', err);
} }
@@ -189,15 +177,7 @@ export default function ProfileScreen() {
setAvatarUrl(result.assets[0].uri); setAvatarUrl(result.assets[0].uri);
setShowAvatarModal(false); setShowAvatarModal(false);
try { try {
const res = await fetch('/api/users/me', { await api.put('/users/me', { avatar_url: result.assets[0].uri });
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();
}
} catch (err) { } catch (err) {
console.error('Error saving avatar:', err); console.error('Error saving avatar:', err);
} }
@@ -205,38 +185,22 @@ export default function ProfileScreen() {
} }
async function handleSelectPresetAvatar(index: number) { async function handleSelectPresetAvatar(index: number) {
const avatarSource = AVATARS[index]; const avatarValue = `preset_avatar_${index + 1}`;
setAvatarUrl(avatarSource); setAvatarUrl(avatarValue);
setShowAvatarModal(false); setShowAvatarModal(false);
try { try {
const res = await fetch('/api/users/me', { await api.put('/users/me', { avatar_url: avatarValue });
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();
}
} catch (err) { } catch (err) {
console.error('Error saving avatar:', err); console.error('Error saving avatar:', err);
} }
} }
async function handleSelectColor(index: number) { async function handleSelectColor(index: number) {
const colorSource = COLOR_CIRCLES[index]; const colorValue = `color_circle_${index + 1}`;
setAvatarUrl(colorSource); setAvatarUrl(colorValue);
setShowAvatarModal(false); setShowAvatarModal(false);
try { try {
const res = await fetch('/api/users/me', { await api.put('/users/me', { avatar_url: colorValue });
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();
}
} catch (err) { } catch (err) {
console.error('Error saving avatar:', err); console.error('Error saving avatar:', err);
} }
@@ -244,13 +208,8 @@ export default function ProfileScreen() {
async function handleDeleteSearch(id: number) { async function handleDeleteSearch(id: number) {
try { try {
const res = await fetch(`/api/search-history/${id}`, { await api.delete(`/search-history/${id}`);
method: 'DELETE', setSearchHistory(prev => prev.filter(item => item.id !== id));
credentials: 'include',
});
if (res.ok) {
setSearchHistory(prev => prev.filter(item => item.id !== id));
}
} catch (err) { } catch (err) {
console.error('Error deleting search:', err); console.error('Error deleting search:', err);
} }
@@ -260,11 +219,8 @@ export default function ProfileScreen() {
async function loadAddresses() { async function loadAddresses() {
setAddressesLoading(true); setAddressesLoading(true);
try { try {
const res = await fetch('/api/addresses', { credentials: 'include' }); const { data } = await api.get('/addresses');
if (res.ok) { setAddresses(data);
const data = await res.json();
setAddresses(data);
}
} catch (err) { } catch (err) {
console.error('Error loading addresses:', err); console.error('Error loading addresses:', err);
} finally { } finally {
@@ -304,27 +260,19 @@ export default function ProfileScreen() {
setFormSaving(true); setFormSaving(true);
setFormError(''); setFormError('');
try { try {
const url = editingAddressId ? `/api/addresses/${editingAddressId}` : '/api/addresses'; const url = editingAddressId ? `/addresses/${editingAddressId}` : '/addresses';
const method = editingAddressId ? 'PUT' : 'POST'; const method = editingAddressId ? 'put' : 'post';
const res = await fetch(url, { await api[method](url, {
method, address: addr,
headers: { 'Content-Type': 'application/json' }, label: formLabel.trim(),
credentials: 'include', is_default: formDefault,
body: JSON.stringify({
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); setShowAddressForm(false);
setEditingAddressId(null); setEditingAddressId(null);
loadAddresses(); loadAddresses();
} catch (err: any) { } catch (err: any) {
setFormError(err.message); const msg = err.response?.data?.error || err.message || 'Error al guardar la dirección';
setFormError(msg);
} finally { } finally {
setFormSaving(false); setFormSaving(false);
} }
@@ -332,13 +280,8 @@ export default function ProfileScreen() {
async function handleDeleteAddress(id: number) { async function handleDeleteAddress(id: number) {
try { try {
const res = await fetch(`/api/addresses/${id}`, { await api.delete(`/addresses/${id}`);
method: 'DELETE', setAddresses(prev => prev.filter(a => a.id !== id));
credentials: 'include',
});
if (res.ok) {
setAddresses(prev => prev.filter(a => a.id !== id));
}
} catch (err) { } catch (err) {
console.error('Error deleting address:', err); console.error('Error deleting address:', err);
} }
@@ -346,13 +289,8 @@ export default function ProfileScreen() {
async function handleSetDefault(id: number) { async function handleSetDefault(id: number) {
try { try {
const res = await fetch(`/api/addresses/${id}/default`, { await api.put(`/addresses/${id}/default`);
method: 'PUT', loadAddresses();
credentials: 'include',
});
if (res.ok) {
loadAddresses();
}
} catch (err) { } catch (err) {
console.error('Error setting default address:', err); console.error('Error setting default address:', err);
} }
@@ -416,7 +354,7 @@ export default function ProfileScreen() {
<View style={styles.avatarSection}> <View style={styles.avatarSection}>
<TouchableOpacity style={styles.avatarContainer} onPress={() => setShowAvatarModal(true)}> <TouchableOpacity style={styles.avatarContainer} onPress={() => setShowAvatarModal(true)}>
{avatarUrl ? ( {avatarUrl ? (
<Image source={{ uri: avatarUrl }} style={styles.avatarImage} /> <Image source={getAvatarSource()} style={styles.avatarImage} />
) : ( ) : (
<Ionicons name="person" size={40} color={colors.primary} /> <Ionicons name="person" size={40} color={colors.primary} />
)} )}
+2 -2
View File
@@ -1,11 +1,11 @@
import Constants from 'expo-constants'; import * as Constants from 'expo-constants';
const ENV = { const ENV = {
development: { development: {
API_BASE_URL: 'http://localhost:3001/api', API_BASE_URL: 'http://localhost:3001/api',
}, },
production: { production: {
API_BASE_URL: 'https://your-production-api.com/api', API_BASE_URL: Constants.expoConfig?.extra?.apiUrl || 'https://api.farmafinder.com/api',
}, },
}; };
+6
View File
@@ -32,6 +32,12 @@ export interface User {
id: number; id: number;
username: string; username: string;
is_admin: boolean; is_admin: boolean;
first_name?: string;
last_name?: string;
email?: string;
avatar_url?: string;
city?: string;
address?: string;
} }
export interface AuthResponse { export interface AuthResponse {