feat(map): implement navigation phases and premium ETACard design

This commit is contained in:
2026-03-02 15:38:52 -05:00
parent f0aabf9879
commit ae55f0acbe
2 changed files with 26 additions and 18 deletions

View File

@ -2,7 +2,7 @@
<Transition name="sheet-ui"> <Transition name="sheet-ui">
<div v-if="isOpen" class="fixed inset-x-0 bottom-0 z-[9999] sm:max-w-md sm:mx-auto"> <div v-if="isOpen" class="fixed inset-x-0 bottom-0 z-[9999] sm:max-w-md sm:mx-auto">
<!-- Overlay transparente oscuro en fondo --> <!-- Overlay transparente oscuro en fondo -->
<div class="fixed inset-0 bg-black/40 transition-opacity" @click="closeCard"></div> <div class="fixed inset-0 bg-black/40 transition-opacity" @click="handleDismiss"></div>
<!-- Bottom Sheet container --> <!-- Bottom Sheet container -->
<div <div
@ -156,7 +156,7 @@ defineProps<{
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'close'): void; (e: 'close'): void; // drag hacia abajo → pasar a fase navigating
(e: 'refresh'): void; (e: 'refresh'): void;
}>(); }>();
@ -167,6 +167,8 @@ const isDragging = ref(false);
const startY = ref(0); const startY = ref(0);
const DISMISS_THRESHOLD = 0.30; // 30% de la altura = cerrar const DISMISS_THRESHOLD = 0.30; // 30% de la altura = cerrar
let intervalId: number | undefined;
function onTouchStart(e: TouchEvent) { function onTouchStart(e: TouchEvent) {
startY.value = e.touches[0]?.clientY ?? 0; startY.value = e.touches[0]?.clientY ?? 0;
isDragging.value = true; isDragging.value = true;
@ -191,18 +193,13 @@ function onTouchEnd() {
const draggedRatio = dragY.value / sheetHeight; const draggedRatio = dragY.value / sheetHeight;
if (draggedRatio >= DISMISS_THRESHOLD) { if (draggedRatio >= DISMISS_THRESHOLD) {
// Arrastró suficiente → cerrar handleDismiss();
emit('close');
} }
// Siempre resetear posición (snap back o después de cerrar)
dragY.value = 0; dragY.value = 0;
} }
// ── AUTO REFRESH ───────────────────────────────────── function handleDismiss() {
let intervalId: number; emit('close'); // 'close' en ETACard = drag dismiss = pasar a fase 'navigating'
function closeCard() {
emit('close');
} }
onMounted(() => { onMounted(() => {

View File

@ -41,6 +41,7 @@ const { procesarSeleccionDeRuta } = useFlujoPrincipal();
const { limpiarMapa: limpiarTodoCentralizado } = useMapState(); const { limpiarMapa: limpiarTodoCentralizado } = useMapState();
const showETACard = ref(false); const showETACard = ref(false);
const routePhase = ref<'idle' | 'eta' | 'navigating'>('idle');
// PERFORMANCE FIX: Use shallowRef for heavy object arrays and Map objects // PERFORMANCE FIX: Use shallowRef for heavy object arrays and Map objects
const promoMarkers = shallowRef<any[]>([]); const promoMarkers = shallowRef<any[]>([]);
@ -104,6 +105,7 @@ async function animateAndReload() {
clearMapMarkers(); clearMapMarkers();
limpiarCaminata(); limpiarCaminata();
showETACard.value = false; showETACard.value = false;
routePhase.value = 'idle';
// Recentrar en el usuario si está disponible (soft-reset) // Recentrar en el usuario si está disponible (soft-reset)
if (userCoords.value) { if (userCoords.value) {
@ -412,25 +414,35 @@ watch([etaCargando, () => busesActivos.value.length], ([loading, count]) => {
}); });
*/ */
// Cuando el usuario hace drag-down en el ETACard → pasar a fase 'navigating'
// Esto muestra el ArrivalBanner arriba y las paradas quedan en el mapa
function handleETACardDismiss() {
showETACard.value = false;
routePhase.value = 'navigating';
}
function handleBannerClick() { function handleBannerClick() {
// Solo abrir el ETACard, NO borrar paradas ni ruta // Al tocar el banner superior, volver a mostrar el ETACard
showETACard.value = true; showETACard.value = true;
routePhase.value = 'eta';
} }
// Watch for route selection changes // Watch for route selection changes
watch(() => routeStore.selectedRouteId, (routeId) => { watch(() => routeStore.selectedRouteId, (routeId) => {
if (routeId) { if (routeId) {
if (routeStore.wasSelectedFromMap) { if (routeStore.wasSelectedFromMap) {
// Iniciar dibujo y búsqueda de disponibilidad en paralelo // Al seleccionar ruta: dibujar mapa + mostrar ETACard (fase 'eta')
// ETACard se abrirá cuando paradaCercana sea calculada (ver watcher abajo)
showETACard.value = false;
updateMapMarkers(false); updateMapMarkers(false);
updateActiveUnits(); updateActiveUnits();
showETACard.value = true;
routePhase.value = 'eta';
} else { } else {
clearMapMarkers(); clearMapMarkers();
} }
} else { } else {
clearMapMarkers(); clearMapMarkers();
showETACard.value = false;
routePhase.value = 'idle';
} }
}); });
@ -439,7 +451,6 @@ watch(() => routeStore.selectedRouteId, (routeId) => {
watch(paradaCercana, (newStop) => { watch(paradaCercana, (newStop) => {
if (newStop && routeStore.selectedRouteId) { if (newStop && routeStore.selectedRouteId) {
updateActiveUnits(); updateActiveUnits();
showETACard.value = true; // Abrir ahora que sí tenemos datos reales
} }
}); });
@ -514,7 +525,7 @@ watch([() => authStore.userProfile?.auto_location, isLoaded], ([canLocate, loade
> >
<template #extra-triggers> <template #extra-triggers>
<ArrivalBanner <ArrivalBanner
:is-visible="!!(paradaCercana && routeStore.selectedRouteId && !isBannerClosing && routeStore.wasSelectedFromMap)" :is-visible="routePhase === 'navigating' && !!(paradaCercana && routeStore.selectedRouteId && !isBannerClosing)"
:stop-name="paradaCercana?.name || ''" :stop-name="paradaCercana?.name || ''"
:is-loading="etaCargando" :is-loading="etaCargando"
:has-active-buses="busesActivos.length > 0" :has-active-buses="busesActivos.length > 0"
@ -570,7 +581,7 @@ watch([() => authStore.userProfile?.auto_location, isLoaded], ([canLocate, loade
:walk-duration="duracionCaminata" :walk-duration="duracionCaminata"
:buses="busesActivos" :buses="busesActivos"
:is-loading="etaCargando" :is-loading="etaCargando"
@close="animateAndReload" @close="handleETACardDismiss"
@refresh="paradaCercana && routeStore.selectedRouteId ? calcularETA(routeStore.selectedRouteId, paradaCercana) : null" @refresh="paradaCercana && routeStore.selectedRouteId ? calcularETA(routeStore.selectedRouteId, paradaCercana) : null"
/> />
</div> </div>