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
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -17,6 +17,7 @@ function App() {
|
||||
const [showLogin, setShowLogin] = useState(false);
|
||||
const [showSaved, setShowSaved] = useState(false);
|
||||
const [badgeCount, setBadgeCount] = useState(0);
|
||||
const [prescriptionSearch, setPrescriptionSearch] = useState('');
|
||||
const [screenSize, setScreenSize] = useState({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
@@ -131,6 +132,7 @@ function App() {
|
||||
<SearchView
|
||||
currentUser={currentUser}
|
||||
onLoginRequest={() => setShowLogin(true)}
|
||||
initialQuery={prescriptionSearch}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
@@ -139,7 +141,8 @@ function App() {
|
||||
<ScannerView
|
||||
onClose={() => setScreen('home')}
|
||||
onSelectMedicine={(name) => {
|
||||
setScreen('home');
|
||||
setPrescriptionSearch(name);
|
||||
setScreen('search');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,12 +3,18 @@
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
max-height: 60vh;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.medicine-results {
|
||||
max-height: 76vh;
|
||||
}
|
||||
}
|
||||
|
||||
.medicine-card {
|
||||
background: var(--surface-container-lowest);
|
||||
border-radius: var(--radius-md);
|
||||
|
||||
@@ -560,3 +560,288 @@
|
||||
.profile-btn-cancel:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Avatar Modal */
|
||||
.profile-modal-avatar {
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
.profile-avatar-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--outline-variant);
|
||||
}
|
||||
|
||||
.profile-avatar-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.85rem 0.5rem;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--on-surface-variant);
|
||||
font-family: inherit;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.profile-avatar-tab:hover {
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.profile-avatar-tab--active {
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
|
||||
.profile-avatar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.profile-avatar-option {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border: 2px solid var(--outline-variant);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
background: none;
|
||||
transition: border-color 0.15s, transform 0.15s;
|
||||
}
|
||||
|
||||
.profile-avatar-option:hover {
|
||||
border-color: var(--primary);
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.profile-avatar-option-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profile-avatar-upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.profile-avatar-upload-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background: var(--surface);
|
||||
border: 2px solid var(--outline-variant);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.profile-avatar-upload-option:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.profile-avatar-upload-icon {
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
border-radius: 50%;
|
||||
background: var(--surface-container-low);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-avatar-upload-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.profile-avatar-upload-sub {
|
||||
margin: 0.2rem 0 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
|
||||
/* Addresses Modal */
|
||||
.profile-modal-addresses {
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
.profile-address-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.profile-address-default-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--on-surface);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.profile-address-default-label input[type="checkbox"] {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
accent-color: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.profile-address-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.profile-address-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: var(--surface);
|
||||
border: 2px solid var(--outline-variant);
|
||||
border-radius: var(--radius);
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.profile-address-item--default {
|
||||
border-color: var(--primary);
|
||||
background: rgba(0, 69, 13, 0.03);
|
||||
}
|
||||
|
||||
.profile-address-item-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.profile-address-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.profile-address-text {
|
||||
font-size: 0.95rem;
|
||||
color: var(--on-surface);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.profile-address-badge {
|
||||
align-self: flex-start;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
color: var(--on-primary);
|
||||
background: var(--primary);
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: var(--radius-full);
|
||||
margin-top: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.profile-address-set-default {
|
||||
align-self: flex-start;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--secondary);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
margin-top: 0.25rem;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.profile-address-set-default:hover {
|
||||
color: var(--on-secondary-container);
|
||||
}
|
||||
|
||||
.profile-address-item-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-address-btn-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
color: var(--on-surface-variant);
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.profile-address-btn-icon:hover {
|
||||
background: var(--surface-container);
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.profile-address-btn-icon--delete:hover {
|
||||
background: rgba(186, 26, 26, 0.1);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.profile-address-add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.85rem;
|
||||
margin-top: 0.5rem;
|
||||
background: var(--surface);
|
||||
border: 2px dashed var(--outline-variant);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.profile-address-add-btn:hover {
|
||||
background: rgba(0, 69, 13, 0.04);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.profile-address-item {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.profile-address-item-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,52 @@
|
||||
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(currentUser?.avatar_url || '');
|
||||
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('');
|
||||
@@ -19,10 +57,22 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
|
||||
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(currentUser?.avatar_url || '');
|
||||
setAvatarUrl(resolveAvatarUrl(currentUser?.avatar_url));
|
||||
loadSearchHistory();
|
||||
}, [currentUser?.id]);
|
||||
|
||||
@@ -48,6 +98,108 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
|
||||
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);
|
||||
@@ -85,8 +237,17 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
|
||||
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 () => {
|
||||
@@ -102,19 +263,67 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
|
||||
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) {
|
||||
console.error('Error saving avatar:', err);
|
||||
setAvatarUrl(resolveAvatarUrl(currentUser?.avatar_url));
|
||||
setUploadError('Error de conexión al guardar la imagen.');
|
||||
}
|
||||
setUploading(false);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} catch (err) {
|
||||
console.error('Error processing image:', 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}`, {
|
||||
@@ -137,7 +346,7 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
|
||||
<div className="profile-avatar-section">
|
||||
<div
|
||||
className="profile-avatar-circle profile-avatar-editable"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
onClick={() => setShowAvatarModal(true)}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<img src={avatarUrl} alt="Avatar" className="profile-avatar-image" />
|
||||
@@ -160,6 +369,7 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
|
||||
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 */}
|
||||
@@ -188,7 +398,7 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="profile-menu-item">
|
||||
<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" />
|
||||
@@ -198,7 +408,7 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
|
||||
<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>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{searchHistory.length > 0 && (
|
||||
<div className="profile-search-history">
|
||||
@@ -244,6 +454,90 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
|
||||
<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)}>
|
||||
@@ -320,6 +614,110 @@ function ProfileView({ currentUser, onProfileSaved, onLogout, onAdminClick }) {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.search-view {
|
||||
max-width: 70rem;
|
||||
}
|
||||
}
|
||||
|
||||
.search-content {
|
||||
padding: 1rem var(--margin-main) 2rem;
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
@@ -195,11 +201,18 @@
|
||||
|
||||
.selected-medicine-section {
|
||||
margin-top: 1.5rem;
|
||||
max-height: 60vh;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.selected-medicine-section {
|
||||
max-height: 76vh;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.medicine-info {
|
||||
background: var(--surface-container-lowest);
|
||||
border-radius: var(--radius-md);
|
||||
|
||||
@@ -13,8 +13,8 @@ const suggestions = [
|
||||
{ name: 'Omeprazol', icon: 'emergency_home', color: 'neutral-4' },
|
||||
];
|
||||
|
||||
function SearchView({ currentUser, onLoginRequest }) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
function SearchView({ currentUser, onLoginRequest, initialQuery = '' }) {
|
||||
const [searchQuery, setSearchQuery] = useState(initialQuery);
|
||||
const [medicines, setMedicines] = useState([]);
|
||||
const [selectedMedicine, setSelectedMedicine] = useState(null);
|
||||
const [pharmacies, setPharmacies] = useState([]);
|
||||
|
||||