From bc8b9ecd315ed4e9d3e0d7bbc78b560034cb619e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Mon, 6 Jul 2026 18:04:37 +0200 Subject: [PATCH] feat: redesign profile view with editable fields and search history --- apps/frontend/src/views/ProfileView.css | 97 ++++++++ apps/frontend/src/views/ProfileView.jsx | 292 ++++++++++++------------ 2 files changed, 240 insertions(+), 149 deletions(-) diff --git a/apps/frontend/src/views/ProfileView.css b/apps/frontend/src/views/ProfileView.css index e3f4dd0..b0e698a 100644 --- a/apps/frontend/src/views/ProfileView.css +++ b/apps/frontend/src/views/ProfileView.css @@ -345,3 +345,100 @@ min-height: 4rem; } } + +/* Profile Avatar Editable */ +.profile-avatar-editable { + cursor: pointer; + position: relative; +} + +.profile-avatar-editable:hover .profile-avatar-overlay { + opacity: 1; +} + +.profile-avatar-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + border-radius: var(--radius-full); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.2s; + color: white; +} + +.profile-avatar-image { + width: 100%; + height: 100%; + border-radius: var(--radius-full); + object-fit: cover; +} + +/* Editable Info Cards */ +.info-card-input { + width: 100%; + padding: 0.75rem; + border: 2px solid var(--outline-variant); + border-radius: var(--radius); + background: var(--surface); + color: var(--on-surface); + font-size: 1rem; + font-family: inherit; + margin-top: 0.5rem; +} + +.info-card-input:focus { + outline: none; + border-color: var(--primary); +} + +.info-card-input:disabled { + opacity: 0.6; +} + +/* Search History */ +.profile-search-history { + background: var(--surface-container-lowest); + border: 1px solid var(--outline-variant); + border-radius: var(--radius-md); + padding: 1rem; + margin-top: 0.5rem; +} + +.profile-search-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 0; + border-bottom: 1px solid var(--outline-variant); +} + +.profile-search-item:last-child { + border-bottom: none; +} + +.profile-search-address { + flex: 1; + font-size: 0.9rem; + color: var(--on-surface); +} + +.profile-search-delete { + background: none; + border: none; + cursor: pointer; + color: var(--on-surface-variant); + padding: 0.25rem; + border-radius: var(--radius); + transition: background 0.15s; +} + +.profile-search-delete:hover { + background: rgba(186, 26, 26, 0.1); + color: var(--error); +} diff --git a/apps/frontend/src/views/ProfileView.jsx b/apps/frontend/src/views/ProfileView.jsx index a12e27d..21d4332 100644 --- a/apps/frontend/src/views/ProfileView.jsx +++ b/apps/frontend/src/views/ProfileView.jsx @@ -1,27 +1,36 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } 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 [firstName, setFirstName] = useState(currentUser?.first_name || ''); + const [lastName, setLastName] = useState(currentUser?.last_name || ''); + const [avatarUrl, setAvatarUrl] = useState(currentUser?.avatar_url || ''); const [saving, setSaving] = useState(false); - const [geocoding, setGeocoding] = useState(false); - const [locating, setLocating] = useState(false); const [feedback, setFeedback] = useState(null); + const [searchHistory, setSearchHistory] = useState([]); + const [uploading, setUploading] = useState(false); + const fileInputRef = useRef(null); useEffect(() => { - setAddress(currentUser?.address || ''); - setLatitude(currentUser?.latitude != null ? String(currentUser.latitude) : ''); - setLongitude(currentUser?.longitude != null ? String(currentUser.longitude) : ''); + setFirstName(currentUser?.first_name || ''); + setLastName(currentUser?.last_name || ''); + setAvatarUrl(currentUser?.avatar_url || ''); setFeedback(null); + 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); + } + } + async function handleSave(e) { e?.preventDefault(); setSaving(true); @@ -32,9 +41,9 @@ function ProfileView({ currentUser, onProfileSaved, onShowSaved, onLogout, onAdm headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ - address: address.trim() || null, - latitude: latitude === '' ? null : latitude, - longitude: longitude === '' ? null : longitude, + first_name: firstName.trim() || null, + last_name: lastName.trim() || null, + avatar_url: avatarUrl || null, }), }); if (!res.ok) { @@ -51,77 +60,127 @@ function ProfileView({ currentUser, onProfileSaved, onShowSaved, onLogout, onAdm } } - async function handleUseLocation() { - setLocating(true); + async function handleAvatarUpload(e) { + const file = e.target.files?.[0]; + if (!file) return; + + setUploading(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.' }); + 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); + setFeedback({ type: 'ok', text: 'Foto de perfil actualizada.' }); + } + } catch (err) { + setFeedback({ type: 'err', text: 'Error al guardar la foto' }); + } + setUploading(false); + }; + reader.readAsDataURL(file); } 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); + setFeedback({ type: 'err', text: 'Error al procesar la imagen' }); + setUploading(false); } } - async function handleGeocode() { - const q = address.trim(); - if (!q) { - setFeedback({ type: 'err', text: 'Ingresa una dirección primero.' }); - return; - } - setGeocoding(true); - setFeedback(null); + async function handleDeleteSearch(id) { try { - const res = await fetch(`/api/geocode?q=${encodeURIComponent(q)}`, { + const res = await fetch(`/api/search-history/${id}`, { + method: 'DELETE', credentials: 'include', }); - if (!res.ok) { - const err = await res.json().catch(() => ({})); - throw new Error(err.error || `Error en la búsqueda (HTTP ${res.status})`); + if (res.ok) { + setSearchHistory(prev => prev.filter(item => item.id !== id)); } - 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); + console.error('Error deleting search:', err); } } - const username = currentUser?.username || 'Usuario'; + const displayName = [firstName, lastName].filter(Boolean).join(' ') || currentUser?.username || 'Usuario'; return (
+ {/* Avatar Section */}
-
- - - +
fileInputRef.current?.click()} + > + {avatarUrl ? ( + Avatar + ) : ( + + + + )} +
+ + + +
-

Mi Perfil

+ + {uploading &&

Subiendo foto...

}
-
-
-

Nombre

-

{username}

+ {/* Editable Name Section */} +
+
+
+ + setFirstName(e.target.value)} + placeholder="Tu nombre" + disabled={saving} + /> +
+
+ + setLastName(e.target.value)} + placeholder="Tus apellidos" + disabled={saving} + /> +
-
+ {feedback && ( +

{feedback.text}

+ )} + +
+ +
+ + + {/* Search History Section */}
- +
+ ))}
- Contactos de Emergencia - - - - - - + )} {currentUser?.is_admin && (
-
-

Tu Ubicación

-

Guarda tu dirección para ordenar por distancia sin pedir permiso cada vez.

-
-
- - setAddress(e.target.value)} - autoComplete="street-address" - disabled={saving} - /> -
-
-
- - setLatitude(e.target.value)} - disabled={saving} - /> -
-
- - setLongitude(e.target.value)} - disabled={saving} - /> -
-
-
- - -
- {feedback && ( -

{feedback.text}

- )} -
- -
-
-
-