Restructure with Turborepo
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import './PharmacyList.css';
|
||||
import { haversineKm, formatDistance } from '../utils/geo';
|
||||
import { getOpenStatus } from '../utils/hours';
|
||||
import {
|
||||
pushSupported,
|
||||
isSubscribedLocally,
|
||||
subscribeToPush,
|
||||
unsubscribeFromPush,
|
||||
} from '../utils/notifications.js';
|
||||
|
||||
function PharmacyList({ pharmacies, loading, userPosition, medicine, currentUser, onLoginRequest }) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="loading-pharmacies">
|
||||
<p>Cargando farmacias...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (pharmacies.length === 0) {
|
||||
return (
|
||||
<div className="no-pharmacies">
|
||||
<p>No se encontraron farmacias con este medicamento</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pharmacy-list">
|
||||
<h3 className="pharmacy-list-title">
|
||||
Disponible en {pharmacies.length} {pharmacies.length === 1 ? 'farmacia' : 'farmacias'}
|
||||
</h3>
|
||||
<div className="pharmacy-grid">
|
||||
{pharmacies.map((pharmacy) => {
|
||||
const distanceKm =
|
||||
userPosition && pharmacy.latitude != null && pharmacy.longitude != null
|
||||
? haversineKm(userPosition.lat, userPosition.lon, pharmacy.latitude, pharmacy.longitude)
|
||||
: null;
|
||||
return (
|
||||
<PharmacyCard
|
||||
key={pharmacy.id}
|
||||
pharmacy={pharmacy}
|
||||
distanceKm={distanceKm}
|
||||
medicine={medicine}
|
||||
currentUser={currentUser}
|
||||
onLoginRequest={onLoginRequest}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PharmacyCard({ pharmacy, distanceKm, medicine, currentUser, onLoginRequest }) {
|
||||
const nregistro = medicine?.nregistro || medicine?.id;
|
||||
const supported = pushSupported();
|
||||
const outOfStock = pharmacy.stock !== undefined && pharmacy.stock <= 0;
|
||||
const showBell = Boolean(medicine && nregistro && outOfStock);
|
||||
|
||||
const [subscribed, setSubscribed] = useState(() =>
|
||||
showBell ? isSubscribedLocally(nregistro, pharmacy.id) : false
|
||||
);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (showBell) setSubscribed(isSubscribedLocally(nregistro, pharmacy.id));
|
||||
}, [nregistro, pharmacy.id, showBell]);
|
||||
|
||||
async function handleBell(e) {
|
||||
e.stopPropagation();
|
||||
if (!currentUser) {
|
||||
onLoginRequest?.();
|
||||
return;
|
||||
}
|
||||
if (!supported) {
|
||||
setError('Las notificaciones requieren iOS 16.4+ y este sitio instalado como app (Compartir → Añadir a Pantalla de Inicio).');
|
||||
return;
|
||||
}
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
if (subscribed) {
|
||||
await unsubscribeFromPush(nregistro, pharmacy.id);
|
||||
setSubscribed(false);
|
||||
} else {
|
||||
await subscribeToPush(nregistro, medicine.name, pharmacy.id);
|
||||
setSubscribed(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[notify] pharmacy toggle failed:', err);
|
||||
setError(err.message || 'No se pudo actualizar la suscripción');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const openStatus = getOpenStatus(pharmacy.opening_hours);
|
||||
|
||||
return (
|
||||
<div className="pharmacy-card">
|
||||
<div className="pharmacy-header">
|
||||
<h4>🏥 {pharmacy.name}</h4>
|
||||
<div className="pharmacy-header-actions">
|
||||
{distanceKm != null && (
|
||||
<span className="pharmacy-distance">{formatDistance(distanceKm)}</span>
|
||||
)}
|
||||
{showBell && (
|
||||
<button
|
||||
type="button"
|
||||
className={`notify-bell${subscribed && currentUser ? ' notify-bell--on' : ''}${!currentUser ? ' notify-bell--locked' : ''}`}
|
||||
onClick={handleBell}
|
||||
disabled={busy}
|
||||
aria-pressed={subscribed && !!currentUser}
|
||||
aria-label={
|
||||
!currentUser
|
||||
? 'Inicia sesión para activar notificaciones'
|
||||
: subscribed
|
||||
? 'Desactivar notificaciones para esta farmacia'
|
||||
: 'Notificarme cuando llegue a esta farmacia'
|
||||
}
|
||||
title={
|
||||
!currentUser
|
||||
? 'Inicia sesión para activar notificaciones'
|
||||
: subscribed
|
||||
? 'Notificaciones activadas para esta farmacia — clic para desactivar'
|
||||
: 'Notificarme cuando llegue a esta farmacia'
|
||||
}
|
||||
>
|
||||
{subscribed && currentUser ? '🔔' : '🔕'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pharmacy-details">
|
||||
{openStatus && (
|
||||
<p className={`pharmacy-hours pharmacy-hours--${openStatus.status}`}>
|
||||
<span className="pharmacy-hours-dot" /> {openStatus.label}
|
||||
</p>
|
||||
)}
|
||||
<p className="pharmacy-address">📍 {pharmacy.address}</p>
|
||||
{pharmacy.phone && (
|
||||
<p className="pharmacy-phone">📞 {pharmacy.phone}</p>
|
||||
)}
|
||||
{error && <p className="notify-error">{error}</p>}
|
||||
<div className="pharmacy-pricing">
|
||||
{pharmacy.price && (
|
||||
<span className="price">€{parseFloat(pharmacy.price).toFixed(2)}</span>
|
||||
)}
|
||||
{pharmacy.stock !== undefined && (
|
||||
<span className={`stock ${pharmacy.stock > 20 ? 'in-stock' : pharmacy.stock > 0 ? 'low-stock' : 'out-of-stock'}`}>
|
||||
{pharmacy.stock > 20 ? '✓ En Stock' : pharmacy.stock > 0 ? `⚠ Stock Bajo (${pharmacy.stock})` : '✗ Sin Stock'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PharmacyList;
|
||||
Reference in New Issue
Block a user