You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
147 lines
3.9 KiB
147 lines
3.9 KiB
<script setup lang="ts">
|
|
import { Home, Wrench, ShoppingCart, User } from 'lucide-vue-next'
|
|
import { Badge } from '@/components/ui/badge'
|
|
|
|
const route = useRoute()
|
|
const { loggedIn } = useAuth()
|
|
|
|
interface NavItem {
|
|
id: string
|
|
label: string
|
|
icon: any
|
|
route: string
|
|
badge?: number
|
|
requireAuth?: boolean
|
|
}
|
|
|
|
const navItems: NavItem[] = [
|
|
{
|
|
id: 'home',
|
|
label: 'Start',
|
|
icon: Home,
|
|
route: '/',
|
|
},
|
|
{
|
|
id: 'products',
|
|
label: 'Makerspace',
|
|
icon: Wrench,
|
|
route: '/products',
|
|
},
|
|
{
|
|
id: 'cart',
|
|
label: 'Warenkorb',
|
|
icon: ShoppingCart,
|
|
route: '/cart',
|
|
badge: 0, // TODO: Get from cart store
|
|
requireAuth: true,
|
|
},
|
|
{
|
|
id: 'profile',
|
|
label: 'Profil',
|
|
icon: User,
|
|
route: '/auth',
|
|
},
|
|
]
|
|
|
|
const isActive = (itemRoute: string) => {
|
|
if (itemRoute === '/') {
|
|
return route.path === '/'
|
|
}
|
|
return route.path.startsWith(itemRoute)
|
|
}
|
|
|
|
function handleNavClick(item: NavItem) {
|
|
if (item.requireAuth && !loggedIn.value) {
|
|
// Redirect to auth page
|
|
navigateTo('/auth')
|
|
return
|
|
}
|
|
|
|
navigateTo(item.route)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Mobile Bottom Navigation - only visible on small screens -->
|
|
<nav
|
|
class="fixed bottom-0 left-0 right-0 z-50 md:hidden border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 safe-area-inset-bottom"
|
|
role="navigation"
|
|
aria-label="Mobile Navigation"
|
|
>
|
|
<div class="flex items-center justify-around h-16 px-2">
|
|
<button
|
|
v-for="item in navItems"
|
|
:key="item.id"
|
|
:class="[
|
|
'flex flex-col items-center justify-center flex-1 gap-1 py-2 px-1 rounded-lg transition-all relative',
|
|
isActive(item.route)
|
|
? 'text-experimenta-accent bg-experimenta-accent/10'
|
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted',
|
|
item.requireAuth && !loggedIn && 'opacity-75',
|
|
]"
|
|
@click="handleNavClick(item)"
|
|
:aria-label="item.label"
|
|
:aria-current="isActive(item.route) ? 'page' : undefined"
|
|
>
|
|
<!-- Icon with badge -->
|
|
<div class="relative">
|
|
<component
|
|
:is="item.icon"
|
|
:class="[
|
|
'h-5 w-5 transition-transform',
|
|
isActive(item.route) && 'scale-110',
|
|
]"
|
|
/>
|
|
|
|
<!-- Badge for cart -->
|
|
<Badge
|
|
v-if="item.badge && item.badge > 0"
|
|
class="absolute -top-2 -right-2 h-4 min-w-[16px] px-1 flex items-center justify-center text-[10px] bg-experimenta-accent"
|
|
>
|
|
{{ item.badge > 99 ? '99+' : item.badge }}
|
|
</Badge>
|
|
|
|
<!-- Login indicator dot for profile when not logged in -->
|
|
<span
|
|
v-if="item.id === 'profile' && !loggedIn"
|
|
class="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-yellow-500 animate-pulse"
|
|
aria-label="Nicht angemeldet"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Label -->
|
|
<span
|
|
:class="[
|
|
'text-[10px] font-medium transition-all',
|
|
isActive(item.route) && 'font-bold',
|
|
]"
|
|
>
|
|
{{ item.label }}
|
|
</span>
|
|
|
|
<!-- Active indicator bar -->
|
|
<span
|
|
v-if="isActive(item.route)"
|
|
class="absolute bottom-0 left-1/2 -translate-x-1/2 w-12 h-0.5 bg-experimenta-accent rounded-full"
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Safe area spacer for devices with notches/home indicators -->
|
|
<div class="h-[env(safe-area-inset-bottom)] bg-background" />
|
|
</nav>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Support for safe area insets (iPhone X+) */
|
|
.safe-area-inset-bottom {
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
}
|
|
|
|
/* Ensure bottom nav doesn't overlay content */
|
|
@media (max-width: 768px) {
|
|
/* Add bottom padding to body/main to account for fixed bottom nav */
|
|
/* This will be handled in the layout component */
|
|
}
|
|
</style>
|
|
|