Files
SIB/frontend/src/components/BottomNav.vue

166 lines
3.9 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
const navItems = [
{ name: 'map', path: '/map', icon: 'map' },
{ name: 'schedules', path: '/schedules', icon: 'schedule' },
{ name: 'discover', path: '/discover', icon: 'explore' },
{ name: 'taxi', path: '/transporte', icon: 'directions_bus' }
]
const isNavigating = ref(false)
const navigateTo = async (path: string) => {
// Prevent rapid multiple navigations (debounce guard)
if (isNavigating.value) return
if (route.path === path) return
try {
isNavigating.value = true
await router.push(path)
} catch (e: any) {
if (e?.name !== 'NavigationDuplicated') {
console.error('SIBU | Error de navegación en el menú inferior:', e)
}
} finally {
// Add a small delay to prevent rapid double-taps
setTimeout(() => { isNavigating.value = false }, 300)
}
}
const isActive = (path: string) => {
return route.path === path || route.path.startsWith(path + '/')
}
// Scroll detection logic
const isVisible = ref(true)
let lastScrollPosition = 0
const handleScroll = () => {
const currentScrollPosition = window.pageYOffset || document.documentElement.scrollTop
if (currentScrollPosition < 0) return // For iOS elastic scroll
if (Math.abs(currentScrollPosition - lastScrollPosition) < 10) return
isVisible.value = currentScrollPosition < lastScrollPosition || currentScrollPosition < 50
lastScrollPosition = currentScrollPosition
}
onMounted(() => {
window.addEventListener('scroll', handleScroll)
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
</script>
<template>
<nav class="bottom-nav" :class="{ 'nav-hidden': !isVisible }">
<div
v-for="item in navItems"
:key="item.name"
class="nav-item"
:class="{ active: isActive(item.path), 'opacity-50 pointer-events-none': isNavigating }"
@click.prevent="navigateTo(item.path)"
@touchend.prevent="navigateTo(item.path)"
>
<span class="material-icons">{{ item.icon }}</span>
<span class="nav-label">{{ t('navigation.' + item.name) }}</span>
</div>
</nav>
</template>
<style scoped>
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: calc(70px + var(--safe-area-bottom));
background: var(--header-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-around;
align-items: center;
padding-bottom: var(--safe-area-bottom);
z-index: 1000;
box-shadow: 0 -10px 30px rgba(0,0,0,0.3);
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}
.nav-hidden {
transform: translateY(100%);
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
padding: 8px 12px;
border-radius: 16px;
flex: 1;
}
.nav-item:hover {
background: var(--hover-bg);
}
.nav-item.active {
color: var(--active-color);
transform: translateY(-4px);
}
.material-icons {
font-size: 26px;
transition: transform 0.3s;
}
.nav-item.active .material-icons {
transform: scale(1.1);
}
.nav-label {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: -0.01em;
}
@media (min-width: 900px) {
.bottom-nav {
left: 50%;
right: auto;
width: 600px;
bottom: 24px;
border-radius: 24px;
border: 1px solid var(--border-color);
box-shadow: 0 20px 50px rgba(0,0,0,0.4);
height: 80px;
padding: 0 20px;
/* En desktop no la ocultamos para mantener la UX de cursor */
transform: translateX(-50%);
}
.nav-hidden {
transform: translate(-50%, 150%);
}
.nav-item {
border-radius: 12px;
}
}
</style>