diff --git a/apps/frontend/src/App.jsx b/apps/frontend/src/App.jsx index 5e55fe1..2f468ce 100644 --- a/apps/frontend/src/App.jsx +++ b/apps/frontend/src/App.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import './App.css'; import HomeView from './views/HomeView'; import SearchView from './views/SearchView'; +import ProductView from './views/ProductView'; import ScannerView from './views/ScannerView'; import AlertsView from './views/AlertsView'; import ProfileView from './views/ProfileView'; @@ -19,6 +20,7 @@ function App() { const [showSaved, setShowSaved] = useState(false); const [badgeCount, setBadgeCount] = useState(0); const [prescriptionSearch, setPrescriptionSearch] = useState(''); + const [productScreen, setProductScreen] = useState(null); const [screenSize, setScreenSize] = useState({ width: window.innerWidth, height: window.innerHeight @@ -180,6 +182,16 @@ function App() { currentUser={currentUser} onLoginRequest={() => setShowLogin(true)} initialQuery={prescriptionSearch} + onNavigateToProduct={(source, id) => setProductScreen({ source, id })} + /> + ); + break; + case 'product': + activeView = ( + { setProductScreen(null); setScreen('search'); }} /> ); break; diff --git a/apps/frontend/src/views/ProductView.css b/apps/frontend/src/views/ProductView.css new file mode 100644 index 0000000..8791fcb --- /dev/null +++ b/apps/frontend/src/views/ProductView.css @@ -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); +} diff --git a/apps/frontend/src/views/ProductView.jsx b/apps/frontend/src/views/ProductView.jsx new file mode 100644 index 0000000..e4edf98 --- /dev/null +++ b/apps/frontend/src/views/ProductView.jsx @@ -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 ( +
+
Cargando...
+
+ ); + } + + if (error) { + return ( +
+ +
{error}
+
+ ); + } + + if (!product) return null; + + const isCima = product.source === 'cima'; + const sourceColor = isCima ? '#2563eb' : '#16a34a'; + const sourceLabel = isCima ? 'CIMA' : 'Open Food Facts'; + + return ( +
+ + +
+ {product.image_url && ( + {product.name} + )} + + {sourceLabel} + +
+ +

{product.name}

+

{product.brand}

+ +
+ {isCima ? ( + <> + {product.active_ingredient && ( + + )} + {product.dosage && ( + + )} + {product.form && ( + + )} + {product.prescription && ( + + )} + {product.commercialized !== undefined && ( + + )} + + ) : ( + <> + {product.nutriscore && product.nutriscore !== 'not-applicable' && product.nutriscore !== 'unknown' && ( + + )} + {product.nova_group && ( + + )} + {product.eco_score && product.eco_score !== 'unknown' && ( + + )} + {product.ingredients && ( + + )} + + )} +
+
+ ); +} + +function DetailRow({ label, value }) { + return ( +
+ {label} + {value} +
+ ); +} diff --git a/apps/frontend/src/views/SearchView.jsx b/apps/frontend/src/views/SearchView.jsx index fbf72d8..5cea6c6 100644 --- a/apps/frontend/src/views/SearchView.jsx +++ b/apps/frontend/src/views/SearchView.jsx @@ -14,7 +14,7 @@ const suggestions = [ { 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 [medicines, setMedicines] = useState([]); const [products, setProducts] = useState([]); @@ -331,7 +331,11 @@ function SearchView({ currentUser, onLoginRequest, initialQuery = '' }) { { - window.location.href = `/product/${p.source}/${p.id}`; + if (onNavigateToProduct) { + onNavigateToProduct(p.source, p.id); + } else { + window.location.href = `/product/${p.source}/${p.id}`; + } }} />