Files
FarmaFinder/apps/frontend/src/views/ProfileView.jsx
T
Antoni Nuñez Romeu 4404db62ee
Run Tests on Branches / Backend Tests (push) Successful in 2m3s
Run Tests on Branches / Frontend Tests (push) Successful in 1m56s
feat: avatar popup options, profile redesign, and search dosage parsing
- Added avatar selection popup with predefined avatar options
- Redesigned ProfileView (web + mobile) with avatar upload and editable fields
- Added AvatarSelectionModal for mobile profile
- Fixed medicine search: parse dosage terms (e.g. 'Paracetamol 1G')
  to query CIMA only by name and filter by dosage field
- Updated styles for profile, search, and medicine results
2026-07-07 12:09:42 +02:00

726 lines
30 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useState, useRef } from 'react';
import './ProfileView.css';
const AVATARS = [
'/avatars/avatar1.png',
'/avatars/avatar2.png',
'/avatars/avatar3.png',
'/avatars/avatar4.png',
'/avatars/avatar5.png',
'/avatars/avatar6.png',
];
const COLOR_CIRCLES = [
'/avatars/color1.png',
'/avatars/color2.png',
'/avatars/color3.png',
'/avatars/color4.png',
'/avatars/color5.png',
'/avatars/color6.png',
];
function resolveAvatarUrl(url) {
if (!url) return '';
const matchPreset = url.match(/^preset_avatar_(\d+)$/);
if (matchPreset) {
const idx = parseInt(matchPreset[1], 10);
if (idx >= 1 && idx <= 6) return AVATARS[idx - 1];
}
const matchColor = url.match(/^color_circle_(\d+)$/);
if (matchColor) {
const idx = parseInt(matchColor[1], 10);
if (idx >= 1 && idx <= 6) return COLOR_CIRCLES[idx - 1];
}
return url;
}
function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
const [firstName, setFirstName] = useState(currentUser?.first_name || '');
const [lastName, setLastName] = useState(currentUser?.last_name || '');
const [avatarUrl, setAvatarUrl] = useState(resolveAvatarUrl(currentUser?.avatar_url));
const [searchHistory, setSearchHistory] = useState([]);
const [uploading, setUploading] = useState(false);
const [uploadError, setUploadError] = useState('');
const fileInputRef = useRef(null);
// Avatar modal state
const [showAvatarModal, setShowAvatarModal] = useState(false);
const [avatarTab, setAvatarTab] = useState('presets');
// 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(null);
// Addresses modal state
const [showAddresses, setShowAddresses] = useState(false);
const [addresses, setAddresses] = useState([]);
const [addressesLoading, setAddressesLoading] = useState(false);
const [showAddressForm, setShowAddressForm] = useState(false);
const [editingAddressId, setEditingAddressId] = useState(null);
const [formAddress, setFormAddress] = useState('');
const [formLabel, setFormLabel] = useState('');
const [formDefault, setFormDefault] = useState(false);
const [formSaving, setFormSaving] = useState(false);
const [formError, setFormError] = useState('');
useEffect(() => {
setFirstName(currentUser?.first_name || '');
setLastName(currentUser?.last_name || '');
setAvatarUrl(resolveAvatarUrl(currentUser?.avatar_url));
loadSearchHistory();
}, [currentUser?.id]);
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(currentUser?.first_name || '');
setConfigLastName(currentUser?.last_name || '');
setConfigEmail(currentUser?.email || '');
setConfigCity(currentUser?.city || '');
setConfigAddress(currentUser?.address || '');
setConfigFeedback(null);
setShowConfig(true);
}
async function loadAddresses() {
setAddressesLoading(true);
try {
const res = await fetch('/api/addresses', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
setAddresses(data);
}
} catch (err) {
console.error('Error loading addresses:', err);
} finally {
setAddressesLoading(false);
}
}
function openAddresses() {
setShowAddresses(true);
loadAddresses();
}
function openAddAddressForm() {
setEditingAddressId(null);
setFormAddress('');
setFormLabel('');
setFormDefault(addresses.length === 0);
setFormError('');
setShowAddressForm(true);
}
function openEditAddressForm(addr) {
setEditingAddressId(addr.id);
setFormAddress(addr.address);
setFormLabel(addr.label || '');
setFormDefault(Boolean(addr.is_default));
setFormError('');
setShowAddressForm(true);
}
async function handleAddressSave(e) {
e?.preventDefault();
const addr = formAddress.trim();
if (!addr) {
setFormError('La dirección es obligatoria');
return;
}
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,
}),
});
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) {
setFormError(err.message);
} finally {
setFormSaving(false);
}
}
async function handleDeleteAddress(id) {
try {
const res = await fetch(`/api/addresses/${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (res.ok) {
setAddresses(prev => prev.filter(a => a.id !== id));
}
} catch (err) {
console.error('Error deleting address:', err);
}
}
async function handleSetDefault(id) {
try {
const res = await fetch(`/api/addresses/${id}/default`, {
method: 'PUT',
credentials: 'include',
});
if (res.ok) {
loadAddresses();
}
} catch (err) {
console.error('Error setting default address:', err);
}
}
async function handleConfigSave(e) {
e?.preventDefault();
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 (HTTP ${res.status})`);
}
const updated = await res.json();
onProfileSaved?.(updated);
setFirstName(updated.first_name || '');
setLastName(updated.last_name || '');
setConfigFeedback({ type: 'ok', text: 'Perfil guardado.' });
setTimeout(() => setShowConfig(false), 1200);
} catch (err) {
setConfigFeedback({ type: 'err', text: err.message || 'Error al guardar' });
} finally {
setConfigSaving(false);
}
}
async function handleAvatarUpload(e) {
const file = e.target.files?.[0];
if (!file) return;
setUploadError('');
setShowAvatarModal(false);
setUploading(true);
if (file.size > 5 * 1024 * 1024) {
setUploadError('La imagen no puede superar los 5 MB');
setUploading(false);
return;
}
try {
const reader = new FileReader();
reader.onload = async () => {
const base64 = reader.result;
setAvatarUrl(base64);
try {
const res = await fetch('/api/users/me', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ avatar_url: base64 }),
});
if (res.ok) {
const updated = await res.json();
onProfileSaved?.(updated);
} else {
setAvatarUrl(resolveAvatarUrl(currentUser?.avatar_url));
setUploadError('Error al guardar la imagen. Intenta con otra foto.');
}
} catch (err) {
setAvatarUrl(resolveAvatarUrl(currentUser?.avatar_url));
setUploadError('Error de conexión al guardar la imagen.');
}
setUploading(false);
};
reader.readAsDataURL(file);
} catch (err) {
setUploadError('Error al procesar la imagen.');
setUploading(false);
}
}
async function handleSelectPresetAvatar(index) {
const url = AVATARS[index];
setAvatarUrl(url);
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();
onProfileSaved?.(updated);
}
} catch (err) {
console.error('Error saving avatar:', err);
}
}
async function handleSelectColor(index) {
const url = COLOR_CIRCLES[index];
setAvatarUrl(url);
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();
onProfileSaved?.(updated);
}
} catch (err) {
console.error('Error saving avatar:', err);
}
}
function handleGalleryUpload() {
fileInputRef.current?.click();
}
async function handleDeleteSearch(id) {
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 displayName = [firstName, lastName].filter(Boolean).join(' ') || currentUser?.username || 'Usuario';
return (
<div className="profile-view">
{/* Avatar Section */}
<div className="profile-avatar-section">
<div
className="profile-avatar-circle profile-avatar-editable"
onClick={() => setShowAvatarModal(true)}
>
{avatarUrl ? (
<img src={avatarUrl} alt="Avatar" className="profile-avatar-image" />
) : (
<svg width="80" height="80" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
</svg>
)}
<div className="profile-avatar-overlay">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
</svg>
</div>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleAvatarUpload}
style={{ display: 'none' }}
/>
{uploading && <p className="profile-section-sub">Subiendo foto...</p>}
{uploadError && <p className="profile-feedback profile-feedback--err">{uploadError}</p>}
</div>
{/* Read-only Name Section */}
<div className="profile-info-cards">
<div className="profile-info-card">
<p className="info-card-label">Nombre</p>
<p className="info-card-value">{firstName || '—'}</p>
</div>
<div className="profile-info-card">
<p className="info-card-label">Apellidos</p>
<p className="info-card-value">{lastName || '—'}</p>
</div>
</div>
{/* Menu Section */}
<div className="profile-menu">
<button className="profile-menu-item" onClick={openConfig}>
<div className="menu-item-icon menu-item-icon--secondary">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" />
</svg>
</div>
<span className="menu-item-label">Configuración</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="menu-item-chevron">
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
<button className="profile-menu-item" onClick={openAddresses}>
<div className="menu-item-icon menu-item-icon--primary">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" />
</svg>
</div>
<span className="menu-item-label">Mis Direcciones</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="menu-item-chevron">
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
{searchHistory.length > 0 && (
<div className="profile-search-history">
<p className="profile-section-sub">Tus búsquedas recientes:</p>
{searchHistory.map((item) => (
<div key={item.id} className="profile-search-item">
<span className="profile-search-address">{item.address}</span>
<button
className="profile-search-delete"
onClick={() => handleDeleteSearch(item.id)}
title="Eliminar"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
</svg>
</button>
</div>
))}
</div>
)}
{currentUser?.is_admin && (
<button className="profile-menu-item" onClick={onAdminClick}>
<div className="menu-item-icon menu-item-icon--primary">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" />
</svg>
</div>
<span className="menu-item-label">Panel de Administración</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="menu-item-chevron">
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
)}
</div>
<button className="profile-logout-btn" onClick={onLogout}>
<div className="menu-item-icon menu-item-icon--error-outline">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z" />
</svg>
</div>
<span>Cerrar Sesión</span>
</button>
{/* Avatar Modal */}
{showAvatarModal && (
<div className="profile-modal-backdrop" onClick={() => setShowAvatarModal(false)}>
<div className="profile-modal profile-modal-avatar" onClick={(e) => e.stopPropagation()}>
<div className="profile-modal-header">
<h3>Cambiar Avatar</h3>
<button className="profile-modal-close" onClick={() => setShowAvatarModal(false)}>×</button>
</div>
<div className="profile-avatar-tabs">
<button
className={`profile-avatar-tab ${avatarTab === 'presets' ? 'profile-avatar-tab--active' : ''}`}
onClick={() => setAvatarTab('presets')}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
</svg>
<span>Prediseñado</span>
</button>
<button
className={`profile-avatar-tab ${avatarTab === 'colors' ? 'profile-avatar-tab--active' : ''}`}
onClick={() => setAvatarTab('colors')}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-1.01 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z" />
</svg>
<span>Colores</span>
</button>
<button
className={`profile-avatar-tab ${avatarTab === 'upload' ? 'profile-avatar-tab--active' : ''}`}
onClick={() => setAvatarTab('upload')}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" />
</svg>
<span>Subir</span>
</button>
</div>
<div className="profile-modal-body">
{avatarTab === 'presets' && (
<div className="profile-avatar-grid">
{AVATARS.map((src, index) => (
<button
key={index}
className="profile-avatar-option"
onClick={() => handleSelectPresetAvatar(index)}
>
<img src={src} alt={`Avatar ${index + 1}`} className="profile-avatar-option-img" />
</button>
))}
</div>
)}
{avatarTab === 'colors' && (
<div className="profile-avatar-grid">
{COLOR_CIRCLES.map((src, index) => (
<button
key={index}
className="profile-avatar-option"
onClick={() => handleSelectColor(index)}
>
<img src={src} alt={`Color ${index + 1}`} className="profile-avatar-option-img" />
</button>
))}
</div>
)}
{avatarTab === 'upload' && (
<div className="profile-avatar-upload">
<button className="profile-avatar-upload-option" onClick={handleGalleryUpload}>
<div className="profile-avatar-upload-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" />
</svg>
</div>
<div>
<p className="profile-avatar-upload-title">Elegir de galería</p>
<p className="profile-avatar-upload-sub">Selecciona una imagen existente</p>
</div>
</button>
</div>
)}
</div>
</div>
</div>
)}
{/* Config Modal */}
{showConfig && (
<div className="profile-modal-backdrop" onClick={() => setShowConfig(false)}>
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
<div className="profile-modal-header">
<h3>Configuración</h3>
<button className="profile-modal-close" onClick={() => setShowConfig(false)}>×</button>
</div>
<form onSubmit={handleConfigSave} className="profile-modal-body">
<div className="profile-modal-field">
<label>Nombre</label>
<input
type="text"
value={configFirstName}
onChange={(e) => setConfigFirstName(e.target.value)}
placeholder="Tu nombre"
disabled={configSaving}
/>
</div>
<div className="profile-modal-field">
<label>Apellidos</label>
<input
type="text"
value={configLastName}
onChange={(e) => setConfigLastName(e.target.value)}
placeholder="Tus apellidos"
disabled={configSaving}
/>
</div>
<div className="profile-modal-field">
<label>Correo electrónico</label>
<input
type="email"
value={configEmail}
onChange={(e) => setConfigEmail(e.target.value)}
placeholder="tu@email.com"
disabled={configSaving}
/>
</div>
<div className="profile-modal-field">
<label>Ciudad</label>
<input
type="text"
value={configCity}
onChange={(e) => setConfigCity(e.target.value)}
placeholder="Tu ciudad"
disabled={configSaving}
/>
</div>
<div className="profile-modal-field">
<label>Dirección</label>
<input
type="text"
value={configAddress}
onChange={(e) => setConfigAddress(e.target.value)}
placeholder="Calle Mayor 1, Madrid"
disabled={configSaving}
/>
</div>
{configFeedback && (
<p className={`profile-feedback profile-feedback--${configFeedback.type}`}>{configFeedback.text}</p>
)}
<div className="profile-modal-actions">
<button type="button" className="profile-btn-cancel" onClick={() => setShowConfig(false)} disabled={configSaving}>
Cancelar
</button>
<button type="submit" className="profile-btn-primary" disabled={configSaving}>
{configSaving ? 'Guardando...' : 'Guardar'}
</button>
</div>
</form>
</div>
</div>
)}
{/* Addresses Modal */}
{showAddresses && (
<div className="profile-modal-backdrop" onClick={() => { setShowAddresses(false); setShowAddressForm(false); }}>
<div className="profile-modal profile-modal-addresses" onClick={(e) => e.stopPropagation()}>
<div className="profile-modal-header">
<h3>Mis Direcciones</h3>
<button className="profile-modal-close" onClick={() => { setShowAddresses(false); setShowAddressForm(false); }}>×</button>
</div>
<div className="profile-modal-body">
{showAddressForm && (
<form onSubmit={handleAddressSave} className="profile-address-form">
<div className="profile-modal-field">
<label>Dirección</label>
<input
type="text"
value={formAddress}
onChange={(e) => setFormAddress(e.target.value)}
placeholder="Calle Mayor 1, Madrid"
disabled={formSaving}
/>
</div>
<div className="profile-modal-field">
<label>Etiqueta (opcional)</label>
<input
type="text"
value={formLabel}
onChange={(e) => setFormLabel(e.target.value)}
placeholder="Ej: Casa, Trabajo, Segunda residencia"
disabled={formSaving}
/>
</div>
<label className="profile-address-default-label">
<input
type="checkbox"
checked={formDefault}
onChange={(e) => setFormDefault(e.target.checked)}
disabled={formSaving}
/>
<span>Dirección predeterminada</span>
</label>
{formError && <p className="profile-feedback profile-feedback--err">{formError}</p>}
<div className="profile-modal-actions">
<button type="button" className="profile-btn-cancel" onClick={() => { setShowAddressForm(false); setFormError(''); }} disabled={formSaving}>
Cancelar
</button>
<button type="submit" className="profile-btn-primary" disabled={formSaving}>
{formSaving ? 'Guardando...' : editingAddressId ? 'Actualizar' : 'Añadir'}
</button>
</div>
</form>
)}
{!showAddressForm && (
<>
{addressesLoading ? (
<p className="profile-section-sub">Cargando direcciones...</p>
) : (
<div className="profile-address-list">
{currentUser?.address && (
<div className="profile-address-item profile-address-item--default">
<div className="profile-address-item-info">
<span className="profile-address-label">Dirección principal</span>
<span className="profile-address-text">{currentUser.address}</span>
<span className="profile-address-badge">Predeterminada</span>
</div>
</div>
)}
{addresses.map((addr) => (
<div key={addr.id} className="profile-address-item">
<div className="profile-address-item-info">
{addr.label && <span className="profile-address-label">{addr.label}</span>}
<span className="profile-address-text">{addr.address}</span>
<button className="profile-address-set-default" onClick={() => handleSetDefault(addr.id)}>
Establecer como predeterminada
</button>
</div>
<div className="profile-address-item-actions">
<button className="profile-address-btn-icon" onClick={() => openEditAddressForm(addr)} title="Editar">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
</svg>
</button>
<button className="profile-address-btn-icon profile-address-btn-icon--delete" onClick={() => handleDeleteAddress(addr.id)} title="Eliminar">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
</svg>
</button>
</div>
</div>
))}
</div>
)}
<button className="profile-address-add-btn" onClick={openAddAddressForm}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</svg>
<span>Añadir más</span>
</button>
</>
)}
</div>
</div>
</div>
)}
</div>
);
}
export default ProfileView;