Restructure with Turborepo
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import './ProfileView.css';
|
||||
import { getUserPosition } from '../utils/geo';
|
||||
|
||||
function ProfileView({ currentUser, onProfileSaved, onShowSaved, onLogout, onAdminClick }) {
|
||||
const [address, setAddress] = useState(currentUser?.address || '');
|
||||
const [latitude, setLatitude] = useState(
|
||||
currentUser?.latitude != null ? String(currentUser.latitude) : ''
|
||||
);
|
||||
const [longitude, setLongitude] = useState(
|
||||
currentUser?.longitude != null ? String(currentUser.longitude) : ''
|
||||
);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [geocoding, setGeocoding] = useState(false);
|
||||
const [locating, setLocating] = useState(false);
|
||||
const [feedback, setFeedback] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setAddress(currentUser?.address || '');
|
||||
setLatitude(currentUser?.latitude != null ? String(currentUser.latitude) : '');
|
||||
setLongitude(currentUser?.longitude != null ? String(currentUser.longitude) : '');
|
||||
setFeedback(null);
|
||||
}, [currentUser?.id]);
|
||||
|
||||
async function handleSave(e) {
|
||||
e?.preventDefault();
|
||||
setSaving(true);
|
||||
setFeedback(null);
|
||||
try {
|
||||
const res = await fetch('/api/users/me', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
address: address.trim() || null,
|
||||
latitude: latitude === '' ? null : latitude,
|
||||
longitude: longitude === '' ? null : longitude,
|
||||
}),
|
||||
});
|
||||
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);
|
||||
setFeedback({ type: 'ok', text: 'Perfil guardado.' });
|
||||
} catch (err) {
|
||||
setFeedback({ type: 'err', text: err.message || 'Error al guardar' });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUseLocation() {
|
||||
setLocating(true);
|
||||
setFeedback(null);
|
||||
try {
|
||||
const pos = await getUserPosition();
|
||||
setLatitude(String(pos.lat));
|
||||
setLongitude(String(pos.lon));
|
||||
setFeedback({ type: 'ok', text: 'Ubicación obtenida — recuerda Guardar.' });
|
||||
} catch (err) {
|
||||
let msg = 'No se pudo obtener la ubicación';
|
||||
if (err && typeof err.code === 'number') {
|
||||
if (err.code === 1) msg = 'Permiso de ubicación denegado';
|
||||
else if (err.code === 2) msg = 'Ubicación no disponible';
|
||||
else if (err.code === 3) msg = 'La solicitud expiró';
|
||||
}
|
||||
setFeedback({ type: 'err', text: msg });
|
||||
} finally {
|
||||
setLocating(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGeocode() {
|
||||
const q = address.trim();
|
||||
if (!q) {
|
||||
setFeedback({ type: 'err', text: 'Ingresa una dirección primero.' });
|
||||
return;
|
||||
}
|
||||
setGeocoding(true);
|
||||
setFeedback(null);
|
||||
try {
|
||||
const res = await fetch(`/api/geocode?q=${encodeURIComponent(q)}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || `Error en la búsqueda (HTTP ${res.status})`);
|
||||
}
|
||||
const data = await res.json();
|
||||
setLatitude(String(data.lat));
|
||||
setLongitude(String(data.lon));
|
||||
setFeedback({
|
||||
type: 'ok',
|
||||
text: `Ubicado: ${data.displayName} — recuerda Guardar.`,
|
||||
});
|
||||
} catch (err) {
|
||||
setFeedback({ type: 'err', text: err.message || 'Error en la búsqueda' });
|
||||
} finally {
|
||||
setGeocoding(false);
|
||||
}
|
||||
}
|
||||
|
||||
const username = currentUser?.username || 'Usuario';
|
||||
|
||||
return (
|
||||
<div className="profile-view">
|
||||
<div className="profile-avatar-section">
|
||||
<div className="profile-avatar-circle">
|
||||
<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>
|
||||
<h2 className="profile-name">Mi Perfil</h2>
|
||||
</div>
|
||||
|
||||
<div className="profile-info-cards">
|
||||
<div className="profile-info-card">
|
||||
<p className="info-card-label">Nombre</p>
|
||||
<p className="info-card-value">{username}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-menu">
|
||||
<button className="profile-menu-item" onClick={onShowSaved}>
|
||||
<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>
|
||||
|
||||
<button className="profile-menu-item">
|
||||
<div className="menu-item-icon menu-item-icon--error">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M22 3H2C.9 3 0 3.9 0 5v14c0 1.1.9 2 2 2h20c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM8 6c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm6 12H2v-1c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1zm8-5h-5V9h5v4z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="menu-item-label">Contactos de Emergencia</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">
|
||||
<div className="menu-item-icon menu-item-icon--secondary">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-7 2h2.5v12H13V6zm-2 12H8.5V6H11v12zM4 6h2.5v12H4V6zm16 12h-2.5V6H20v12z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="menu-item-label">Configuración de Texto</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>
|
||||
|
||||
{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>
|
||||
|
||||
<div className="profile-location-section">
|
||||
<h3>Tu Ubicación</h3>
|
||||
<p className="profile-section-sub">Guarda tu dirección para ordenar por distancia sin pedir permiso cada vez.</p>
|
||||
<form onSubmit={handleSave} className="profile-form">
|
||||
<div className="profile-field">
|
||||
<label htmlFor="profile-address">Dirección</label>
|
||||
<input
|
||||
id="profile-address"
|
||||
type="text"
|
||||
placeholder="Calle Mayor 1, Madrid"
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
autoComplete="street-address"
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
<div className="profile-field-row">
|
||||
<div className="profile-field">
|
||||
<label htmlFor="profile-lat">Latitud</label>
|
||||
<input
|
||||
id="profile-lat"
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="40.4168"
|
||||
value={latitude}
|
||||
onChange={(e) => setLatitude(e.target.value)}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
<div className="profile-field">
|
||||
<label htmlFor="profile-lon">Longitud</label>
|
||||
<input
|
||||
id="profile-lon"
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="-3.7038"
|
||||
value={longitude}
|
||||
onChange={(e) => setLongitude(e.target.value)}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="profile-helper-actions">
|
||||
<button type="button" className="profile-btn-secondary" onClick={handleGeocode} disabled={geocoding || saving}>
|
||||
{geocoding ? 'Buscando…' : '📍 Buscar desde dirección'}
|
||||
</button>
|
||||
<button type="button" className="profile-btn-secondary" onClick={handleUseLocation} disabled={locating || saving}>
|
||||
{locating ? 'Localizando…' : '🛰️ Usar mi ubicación'}
|
||||
</button>
|
||||
</div>
|
||||
{feedback && (
|
||||
<p className={`profile-feedback profile-feedback--${feedback.type}`}>{feedback.text}</p>
|
||||
)}
|
||||
<div className="profile-actions">
|
||||
<button type="submit" className="profile-btn-primary" disabled={saving}>
|
||||
{saving ? 'Guardando…' : 'Guardar'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProfileView;
|
||||
Reference in New Issue
Block a user