feat/parapharmacy-baby-products #31
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import './App.css';
|
import './App.css';
|
||||||
import HomeView from './views/HomeView';
|
import HomeView from './views/HomeView';
|
||||||
import SearchView from './views/SearchView';
|
import SearchView from './views/SearchView';
|
||||||
|
import ProductView from './views/ProductView';
|
||||||
import ScannerView from './views/ScannerView';
|
import ScannerView from './views/ScannerView';
|
||||||
import AlertsView from './views/AlertsView';
|
import AlertsView from './views/AlertsView';
|
||||||
import ProfileView from './views/ProfileView';
|
import ProfileView from './views/ProfileView';
|
||||||
@@ -19,6 +20,7 @@ function App() {
|
|||||||
const [showSaved, setShowSaved] = useState(false);
|
const [showSaved, setShowSaved] = useState(false);
|
||||||
const [badgeCount, setBadgeCount] = useState(0);
|
const [badgeCount, setBadgeCount] = useState(0);
|
||||||
const [prescriptionSearch, setPrescriptionSearch] = useState('');
|
const [prescriptionSearch, setPrescriptionSearch] = useState('');
|
||||||
|
const [productScreen, setProductScreen] = useState(null);
|
||||||
const [screenSize, setScreenSize] = useState({
|
const [screenSize, setScreenSize] = useState({
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight
|
height: window.innerHeight
|
||||||
@@ -180,6 +182,16 @@ function App() {
|
|||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
onLoginRequest={() => setShowLogin(true)}
|
onLoginRequest={() => setShowLogin(true)}
|
||||||
initialQuery={prescriptionSearch}
|
initialQuery={prescriptionSearch}
|
||||||
|
onNavigateToProduct={(source, id) => setProductScreen({ source, id })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'product':
|
||||||
|
activeView = (
|
||||||
|
<ProductView
|
||||||
|
source={productScreen?.source}
|
||||||
|
id={productScreen?.id}
|
||||||
|
onBack={() => { setProductScreen(null); setScreen('search'); }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
.product-view {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 48rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem var(--margin-main) 2rem;
|
||||||
|
animation: fadeInUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--primary, #2563eb);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-loading,
|
||||||
|
.product-error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
color: var(--on-surface-variant, #6b7280);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: var(--radius-md, 0.75rem);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--on-surface);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-brand {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--on-surface-variant, #6b7280);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-details {
|
||||||
|
background: var(--surface-container-lowest, #f9fafb);
|
||||||
|
border-radius: var(--radius-md, 0.75rem);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
border-bottom: 1px solid var(--outline-variant, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--on-surface-variant, #9ca3af);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: var(--on-surface);
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import './ProductView.css';
|
||||||
|
|
||||||
|
export default function ProductView({ source, id, onBack }) {
|
||||||
|
const [product, setProduct] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadProduct();
|
||||||
|
}, [source, id]);
|
||||||
|
|
||||||
|
async function loadProduct() {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/products/${source}/${id}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Producto no encontrado');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setProduct(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="product-view">
|
||||||
|
<div className="product-loading">Cargando...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="product-view">
|
||||||
|
<button className="back-btn" onClick={onBack}>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
Volver
|
||||||
|
</button>
|
||||||
|
<div className="product-error">{error}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product) return null;
|
||||||
|
|
||||||
|
const isCima = product.source === 'cima';
|
||||||
|
const sourceColor = isCima ? '#2563eb' : '#16a34a';
|
||||||
|
const sourceLabel = isCima ? 'CIMA' : 'Open Food Facts';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="product-view">
|
||||||
|
<button className="back-btn" onClick={onBack}>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
Volver
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="product-header">
|
||||||
|
{product.image_url && (
|
||||||
|
<img src={product.image_url} alt={product.name} className="product-image" />
|
||||||
|
)}
|
||||||
|
<span className="source-badge" style={{ backgroundColor: sourceColor }}>
|
||||||
|
{sourceLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="product-name">{product.name}</h1>
|
||||||
|
<p className="product-brand">{product.brand}</p>
|
||||||
|
|
||||||
|
<div className="product-details">
|
||||||
|
{isCima ? (
|
||||||
|
<>
|
||||||
|
{product.active_ingredient && (
|
||||||
|
<DetailRow label="Principio activo" value={product.active_ingredient} />
|
||||||
|
)}
|
||||||
|
{product.dosage && (
|
||||||
|
<DetailRow label="Dosis" value={product.dosage} />
|
||||||
|
)}
|
||||||
|
{product.form && (
|
||||||
|
<DetailRow label="Forma farmacéutica" value={product.form} />
|
||||||
|
)}
|
||||||
|
{product.prescription && (
|
||||||
|
<DetailRow label="Prescripción" value={product.prescription} />
|
||||||
|
)}
|
||||||
|
{product.commercialized !== undefined && (
|
||||||
|
<DetailRow label="Comercializado" value={product.commercialized ? 'Sí' : 'No'} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{product.nutriscore && product.nutriscore !== 'not-applicable' && product.nutriscore !== 'unknown' && (
|
||||||
|
<DetailRow label="Nutri-Score" value={product.nutriscore.toUpperCase()} />
|
||||||
|
)}
|
||||||
|
{product.nova_group && (
|
||||||
|
<DetailRow label="NOVA" value={`Grupo ${product.nova_group}`} />
|
||||||
|
)}
|
||||||
|
{product.eco_score && product.eco_score !== 'unknown' && (
|
||||||
|
<DetailRow label="Eco-Score" value={product.eco_score.toUpperCase()} />
|
||||||
|
)}
|
||||||
|
{product.ingredients && (
|
||||||
|
<DetailRow label="Ingredientes" value={product.ingredients} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailRow({ label, value }) {
|
||||||
|
return (
|
||||||
|
<div className="detail-row">
|
||||||
|
<span className="detail-label">{label}</span>
|
||||||
|
<span className="detail-value">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ const suggestions = [
|
|||||||
{ name: 'Omeprazol', icon: 'emergency_home', color: 'neutral-4' },
|
{ name: 'Omeprazol', icon: 'emergency_home', color: 'neutral-4' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function SearchView({ currentUser, onLoginRequest, initialQuery = '' }) {
|
function SearchView({ currentUser, onLoginRequest, initialQuery = '', onNavigateToProduct }) {
|
||||||
const [searchQuery, setSearchQuery] = useState(initialQuery);
|
const [searchQuery, setSearchQuery] = useState(initialQuery);
|
||||||
const [medicines, setMedicines] = useState([]);
|
const [medicines, setMedicines] = useState([]);
|
||||||
const [products, setProducts] = useState([]);
|
const [products, setProducts] = useState([]);
|
||||||
@@ -331,7 +331,11 @@ function SearchView({ currentUser, onLoginRequest, initialQuery = '' }) {
|
|||||||
<ProductResults
|
<ProductResults
|
||||||
products={products}
|
products={products}
|
||||||
onSelect={(p) => {
|
onSelect={(p) => {
|
||||||
|
if (onNavigateToProduct) {
|
||||||
|
onNavigateToProduct(p.source, p.id);
|
||||||
|
} else {
|
||||||
window.location.href = `/product/${p.source}/${p.id}`;
|
window.location.href = `/product/${p.source}/${p.id}`;
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user