feat(map): implement navigation phases and premium ETACard design
This commit is contained in:
@ -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(() => {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user