Restructure with Turborepo
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import '../App.css';
|
||||
import SearchBar from '../components/SearchBar';
|
||||
import MedicineResults from '../components/MedicineResults';
|
||||
import PharmacyList from '../components/PharmacyList';
|
||||
import PharmacyMap from '../components/PharmacyMap';
|
||||
import HomeView from './HomeView';
|
||||
import ScannerView from './ScannerView';
|
||||
import { haversineKm, getUserPosition } from '../utils/geo';
|
||||
|
||||
function PublicView({
|
||||
currentUser,
|
||||
onLoginRequest,
|
||||
screen: screenProp,
|
||||
onScreenChange,
|
||||
}) {
|
||||
// ponytail: uncontrolled by default (own state); controlled when App.jsx passes props
|
||||
// so BottomNav can drive the inner screen directly. No-op fallback breaks tests.
|
||||
const [internalScreen, setInternalScreen] = useState('home');
|
||||
const screen = screenProp ?? internalScreen;
|
||||
const setScreen = onScreenChange ?? setInternalScreen;
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [medicines, setMedicines] = useState([]);
|
||||
const [selectedMedicine, setSelectedMedicine] = useState(null);
|
||||
const [pharmacies, setPharmacies] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [userPosition, setUserPosition] = useState(null);
|
||||
const [positionSource, setPositionSource] = useState(null); // 'profile' | 'browser'
|
||||
const [sortByDistance, setSortByDistance] = useState(false);
|
||||
const [locating, setLocating] = useState(false);
|
||||
const [locationError, setLocationError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const searchMedicines = async () => {
|
||||
if (searchQuery.trim().length < 2) {
|
||||
setMedicines([]);
|
||||
setSelectedMedicine(null);
|
||||
setPharmacies([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/medicines/search?q=${encodeURIComponent(searchQuery)}`);
|
||||
const data = await response.json();
|
||||
setMedicines(data);
|
||||
} catch (error) {
|
||||
console.error('Error searching medicines:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(searchMedicines, 300);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPharmacies = async () => {
|
||||
if (!selectedMedicine) {
|
||||
setPharmacies([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/medicines/${selectedMedicine.id}/pharmacies`);
|
||||
const data = await response.json();
|
||||
setPharmacies(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching pharmacies:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPharmacies();
|
||||
}, [selectedMedicine]);
|
||||
|
||||
const savedLat = currentUser?.latitude;
|
||||
const savedLon = currentUser?.longitude;
|
||||
const hasSavedCoords = savedLat != null && savedLon != null;
|
||||
|
||||
const handleSortByDistance = async () => {
|
||||
if (sortByDistance) {
|
||||
setSortByDistance(false);
|
||||
return;
|
||||
}
|
||||
setLocationError('');
|
||||
if (hasSavedCoords) {
|
||||
setUserPosition({ lat: savedLat, lon: savedLon });
|
||||
setPositionSource('profile');
|
||||
setSortByDistance(true);
|
||||
return;
|
||||
}
|
||||
if (userPosition) {
|
||||
setSortByDistance(true);
|
||||
return;
|
||||
}
|
||||
setLocating(true);
|
||||
try {
|
||||
const pos = await getUserPosition();
|
||||
setUserPosition(pos);
|
||||
setPositionSource('browser');
|
||||
setSortByDistance(true);
|
||||
} catch (err) {
|
||||
console.error('[location] error', err);
|
||||
let msg;
|
||||
if (err && typeof err.code === 'number') {
|
||||
if (err.code === 1) msg = 'Permiso de ubicación denegado — actívalo en la configuración del navegador';
|
||||
else if (err.code === 2) msg = 'Ubicación no disponible — comprueba los servicios de ubicación/WiFi';
|
||||
else if (err.code === 3) msg = 'La solicitud de ubicación expiró — inténtalo de nuevo';
|
||||
else msg = `Error de geolocalización (código ${err.code})`;
|
||||
} else {
|
||||
msg = err && err.message ? err.message : 'No se pudo obtener tu ubicación';
|
||||
}
|
||||
setLocationError(msg);
|
||||
} finally {
|
||||
setLocating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const displayedPharmacies = useMemo(() => {
|
||||
if (!sortByDistance || !userPosition) return pharmacies;
|
||||
return [...pharmacies].sort((a, b) => {
|
||||
if (a.latitude == null || a.longitude == null) return 1;
|
||||
if (b.latitude == null || b.longitude == null) return -1;
|
||||
return (
|
||||
haversineKm(userPosition.lat, userPosition.lon, a.latitude, a.longitude) -
|
||||
haversineKm(userPosition.lat, userPosition.lon, b.latitude, b.longitude)
|
||||
);
|
||||
});
|
||||
}, [pharmacies, sortByDistance, userPosition]);
|
||||
|
||||
/* ── Scanner → Search handoff ──────────────────────────── */
|
||||
function handleScanSelectMedicine(medicineName) {
|
||||
setSearchQuery(medicineName);
|
||||
setSelectedMedicine(null);
|
||||
setPharmacies([]);
|
||||
setScreen('search');
|
||||
}
|
||||
|
||||
/* ── Screens ─────────────────────────────────────────────── */
|
||||
if (screen === 'home') {
|
||||
return (
|
||||
<HomeView
|
||||
onScanClick={() => setScreen('scan')}
|
||||
onSearchClick={() => setScreen('search')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (screen === 'scan') {
|
||||
return (
|
||||
<ScannerView
|
||||
onClose={() => setScreen('home')}
|
||||
onSelectMedicine={handleScanSelectMedicine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// screen === 'search'
|
||||
return (
|
||||
<>
|
||||
<header className="app-header">
|
||||
<button
|
||||
className="back-to-home-btn"
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
setSelectedMedicine(null);
|
||||
setPharmacies([]);
|
||||
setScreen('home');
|
||||
}}
|
||||
aria-label="Volver al inicio"
|
||||
>
|
||||
← Inicio
|
||||
</button>
|
||||
<h1>💊 FarmaClic</h1>
|
||||
<p>Encuentra tu medicamento en farmacias cercanas</p>
|
||||
</header>
|
||||
|
||||
<main className="app-main">
|
||||
<SearchBar
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholder="Buscar un medicamento..."
|
||||
/>
|
||||
|
||||
{loading && <div className="loading">Buscando...</div>}
|
||||
|
||||
{searchQuery && !selectedMedicine && (
|
||||
<MedicineResults
|
||||
medicines={medicines}
|
||||
onSelect={setSelectedMedicine}
|
||||
query={searchQuery}
|
||||
currentUser={currentUser}
|
||||
onLoginRequest={onLoginRequest}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedMedicine && (
|
||||
<div className="selected-medicine-section">
|
||||
<div className="medicine-info">
|
||||
<h2>{selectedMedicine.name}</h2>
|
||||
<div className="medicine-details">
|
||||
<span><strong>Principio Activo:</strong> {selectedMedicine.active_ingredient}</span>
|
||||
<span><strong>Dosis:</strong> {selectedMedicine.dosage}</span>
|
||||
<span><strong>Forma:</strong> {selectedMedicine.form}</span>
|
||||
</div>
|
||||
<button
|
||||
className="back-button"
|
||||
onClick={() => {
|
||||
setSelectedMedicine(null);
|
||||
setPharmacies([]);
|
||||
}}
|
||||
>
|
||||
← Volver a búsqueda
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{pharmacies.length > 0 && (
|
||||
<div className="pharmacy-controls">
|
||||
<button
|
||||
className={`sort-distance-button ${sortByDistance ? 'active' : ''}`}
|
||||
onClick={handleSortByDistance}
|
||||
disabled={locating}
|
||||
>
|
||||
{locating
|
||||
? '📍 Localizando…'
|
||||
: sortByDistance
|
||||
? '📍 Ordenado por distancia · Reset'
|
||||
: hasSavedCoords
|
||||
? '📍 Ordenar por ubicación guardada'
|
||||
: '📍 Ordenar por distancia'}
|
||||
</button>
|
||||
{sortByDistance && positionSource === 'profile' && (
|
||||
<span className="location-source">Usando tu dirección guardada</span>
|
||||
)}
|
||||
{locationError && (
|
||||
<span className="location-error">{locationError}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PharmacyMap pharmacies={displayedPharmacies} />
|
||||
<PharmacyList
|
||||
pharmacies={displayedPharmacies}
|
||||
loading={loading}
|
||||
userPosition={sortByDistance ? userPosition : null}
|
||||
medicine={selectedMedicine}
|
||||
currentUser={currentUser}
|
||||
onLoginRequest={onLoginRequest}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublicView;
|
||||
Reference in New Issue
Block a user