feat(frontend): add product detail view and navigation
Run Tests on Branches / Detect Changes (push) Successful in 12s
Run Tests on Branches / Backend Tests (push) Has been cancelled
Run Tests on Branches / Frontend Tests (push) Has been cancelled
Run Tests on Branches / Frontend Mobile Tests (push) Has been cancelled

- Create ProductView component with product details display
- Add ProductView.css with responsive styling
- Add product screen to App.jsx screen system
- Pass onNavigateToProduct callback from App to SearchView
- Update ProductResults onSelect to use callback instead of window.location
This commit is contained in:
Antoni Nuñez Romeu
2026-07-13 17:36:52 +02:00
parent 31c1a14343
commit 731a6c98ae
4 changed files with 246 additions and 2 deletions
+126
View File
@@ -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>
);
}