Fix build error: Moví ETACard.vue a src/components/map/ para que coincida con la ruta de importación
This commit is contained in:
247
frontend/src/components/map/ETACard.vue
Normal file
247
frontend/src/components/map/ETACard.vue
Normal file
@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<Transition name="sheet-ui">
|
||||
<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 -->
|
||||
<div class="fixed inset-0 bg-black/40 transition-opacity" @click="closeCard"></div>
|
||||
|
||||
<!-- Bottom Sheet container -->
|
||||
<div
|
||||
ref="sheetRef"
|
||||
class="relative bg-white dark:bg-gray-900 rounded-t-3xl shadow-2xl p-5 transform flex flex-col gap-4 max-h-[85vh] overflow-y-auto"
|
||||
:style="{
|
||||
transform: `translateY(${dragY}px)`,
|
||||
transition: isDragging ? 'none' : 'transform 0.3s ease-out'
|
||||
}"
|
||||
@touchstart="onTouchStart"
|
||||
@touchmove="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
>
|
||||
<!-- Pestaña de arrastre (visual + funcional) -->
|
||||
<div
|
||||
class="absolute top-3 left-1/2 -translate-x-1/2 w-12 h-1.5 bg-gray-300 dark:bg-gray-600 rounded-full cursor-grab active:cursor-grabbing"
|
||||
@touchstart="onTouchStart"
|
||||
></div>
|
||||
|
||||
<!-- Indicador visual de que se puede arrastrar -->
|
||||
<p class="text-center text-[10px] text-gray-400 mt-1 mb-0 pointer-events-none">
|
||||
Desliza hacia abajo para cerrar
|
||||
</p>
|
||||
|
||||
<!-- Cabecera de la parada -->
|
||||
<div v-if="stopName" class="mt-4 flex items-start gap-4 pb-4 border-b border-gray-100 dark:border-gray-800">
|
||||
<div class="bg-blue-100 dark:bg-blue-900/40 p-3 rounded-2xl flex-shrink-0">
|
||||
<span class="material-icons text-blue-600 dark:text-blue-400 text-3xl">place</span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-1">Tu parada asignada</h3>
|
||||
<h2 class="text-xl font-black text-gray-900 dark:text-white leading-tight">
|
||||
{{ stopName }}
|
||||
</h2>
|
||||
<div class="flex items-center gap-2 mt-2 text-sm text-gray-600 dark:text-gray-300 font-medium">
|
||||
<span class="material-icons text-sm text-gray-400">directions_walk</span>
|
||||
A {{ Math.round(walkDuration / 60) }} min caminando
|
||||
<span class="text-gray-300 dark:text-gray-600">•</span>
|
||||
{{ Math.round(walkDistance) }} metros
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenido de buses -->
|
||||
<div class="flex flex-col gap-3 py-2">
|
||||
<!-- Estado de carga -->
|
||||
<div v-if="isLoading" class="flex flex-col items-center justify-center py-8">
|
||||
<div class="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
<span class="mt-4 text-gray-500 dark:text-gray-400 font-medium animate-pulse">Calculando satélites...</span>
|
||||
</div>
|
||||
|
||||
<!-- Sin servicio -->
|
||||
<div v-else-if="buses.length === 0" class="p-6 text-center">
|
||||
<span class="material-icons text-4xl text-gray-300 mb-2">event_busy</span>
|
||||
<p class="text-gray-500 dark:text-gray-400 font-medium italic">
|
||||
{{ t('schedules.noSchedules') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Lista de llegadas (Max 2) -->
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(bus, index) in buses.slice(0, 2)"
|
||||
:key="bus.horario_id"
|
||||
class="group bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-2xl p-4 flex items-center justify-between"
|
||||
:class="{ 'ring-2 ring-green-500/50 dark:ring-green-400/50 bg-green-50/30 dark:bg-green-900/10': bus.estado === 'en_camino' }"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Icono dinámico según estado -->
|
||||
<div
|
||||
class="w-12 h-12 rounded-full flex items-center justify-center text-white shrink-0"
|
||||
:class="{
|
||||
'bg-green-500': bus.estado === 'en_camino',
|
||||
'bg-blue-500': bus.estado === 'próximo',
|
||||
'bg-gray-400 dark:bg-gray-600': bus.estado === 'pasó'
|
||||
}"
|
||||
>
|
||||
<span class="material-icons">{{ bus.estado === 'pasó' ? 'history' : 'directions_bus' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-bold text-gray-900 dark:text-white line-clamp-1">
|
||||
{{ index === 0 ? 'Bus más cercano' : 'Siguiente bus' }}
|
||||
</span>
|
||||
<div class="flex flex-col mt-0.5 gap-0.5">
|
||||
<span class="text-xs font-semibold text-gray-500 dark:text-gray-400 flex items-center gap-1">
|
||||
<span class="material-icons" style="font-size: 14px">schedule</span>
|
||||
{{ bus.estado === 'pasó' ? 'Salió a las' : 'Sale a las' }} {{ bus.hora_salida }}
|
||||
</span>
|
||||
<span class="text-[11px] font-medium text-gray-500 dark:text-gray-400 pl-4">
|
||||
<span v-if="bus.estado === 'pasó'">Pasó por tu parada a las</span>
|
||||
<span v-else>Llega a tu parada ~</span>
|
||||
{{ bus.horaLlegadaParada }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end text-right shrink-0 ml-4">
|
||||
<!-- ETA gigante -->
|
||||
<div v-if="bus.estado !== 'pasó'" class="text-2xl font-black text-gray-900 dark:text-white flex items-baseline gap-1" :class="{ 'text-green-600 dark:text-green-400': bus.estado === 'en_camino' }">
|
||||
<span>~{{ bus.etaMinutos }}</span>
|
||||
<span class="text-sm font-bold text-gray-500 dark:text-gray-400">min</span>
|
||||
</div>
|
||||
|
||||
<!-- Badges de estado -->
|
||||
<div class="mt-1">
|
||||
<span v-if="bus.estado === 'en_camino'" class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-400 text-[10px] font-black uppercase tracking-wider">
|
||||
<span class="w-1.5 h-1.5 bg-green-500 rounded-full animate-ping"></span>
|
||||
En Vía
|
||||
</span>
|
||||
<span v-else-if="bus.estado === 'próximo'" class="inline-flex px-2.5 py-1 rounded-lg bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-400 text-[10px] font-black uppercase tracking-wider">
|
||||
Programado
|
||||
</span>
|
||||
<span v-else class="inline-flex px-2.5 py-1 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 text-[10px] font-black uppercase tracking-wider line-through">
|
||||
Ya pasó
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Legal Disclaimer Intocable -->
|
||||
<div v-if="stopName" class="mt-2 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-xl flex items-start gap-3 border border-yellow-100 dark:border-yellow-900/50">
|
||||
<span class="material-icons text-yellow-600 dark:text-yellow-500 text-lg mt-0.5 shrink-0">info</span>
|
||||
<p class="text-[11px] leading-snug text-yellow-800 dark:text-yellow-600/90 font-medium">
|
||||
<strong>Aviso:</strong> Este es un tiempo estimado basado en la velocidad promedio de las unidades en la ciudad. No existe rastreo GPS en tiempo real.
|
||||
El tráfico y paradas intermedias pueden alterar el tiempo de llegada dramáticamente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { BusETA } from '@/composables/useETA';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps<{
|
||||
isOpen: boolean;
|
||||
stopName: string;
|
||||
walkDistance: number;
|
||||
walkDuration: number; // en segundos
|
||||
buses: BusETA[];
|
||||
isLoading: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'refresh'): void;
|
||||
}>();
|
||||
|
||||
// ── DRAG TO DISMISS ──────────────────────────────────
|
||||
const sheetRef = ref<HTMLElement | null>(null);
|
||||
const dragY = ref(0); // desplazamiento actual del drag
|
||||
const isDragging = ref(false);
|
||||
const startY = ref(0);
|
||||
const DISMISS_THRESHOLD = 0.30; // 30% de la altura = cerrar
|
||||
|
||||
function onTouchStart(e: TouchEvent) {
|
||||
startY.value = e.touches[0]?.clientY ?? 0;
|
||||
isDragging.value = true;
|
||||
dragY.value = 0;
|
||||
}
|
||||
|
||||
function onTouchMove(e: TouchEvent) {
|
||||
if (!isDragging.value) return;
|
||||
const delta = (e.touches[0]?.clientY ?? 0) - startY.value;
|
||||
// Solo permitir arrastrar hacia ABAJO (delta positivo)
|
||||
if (delta > 0) {
|
||||
dragY.value = delta;
|
||||
e.preventDefault(); // evitar scroll del contenido mientras arrastra
|
||||
}
|
||||
}
|
||||
|
||||
function onTouchEnd() {
|
||||
if (!isDragging.value) return;
|
||||
isDragging.value = false;
|
||||
|
||||
const sheetHeight = sheetRef.value?.offsetHeight ?? 400;
|
||||
const draggedRatio = dragY.value / sheetHeight;
|
||||
|
||||
if (draggedRatio >= DISMISS_THRESHOLD) {
|
||||
// Arrastró suficiente → cerrar
|
||||
emit('close');
|
||||
}
|
||||
// Siempre resetear posición (snap back o después de cerrar)
|
||||
dragY.value = 0;
|
||||
}
|
||||
|
||||
// ── AUTO REFRESH ─────────────────────────────────────
|
||||
let intervalId: number;
|
||||
|
||||
function closeCard() {
|
||||
emit('close');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Refresca el panel de ETAs cada 60 segundos
|
||||
intervalId = window.setInterval(() => {
|
||||
emit('refresh');
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Transición de entrada/salida (Slide up de abajo hacia arriba per "Fluid Interfaces") */
|
||||
.sheet-ui-enter-active {
|
||||
transition: all 0.5s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
.sheet-ui-leave-active {
|
||||
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.sheet-ui-enter-from,
|
||||
.sheet-ui-leave-to {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sheet-ui-enter-to,
|
||||
.sheet-ui-leave-from {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Transición extra para el overlay */
|
||||
.fixed.inset-0 {
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user