Refactor navigation components for improved layout and functionality
- Updated UserMenu.vue to enhance button styling and spacing for a more modern look. - Simplified CartFAB.vue to always show the cart button when items are present, regardless of the route. - Adjusted AppHeader.vue for better alignment of elements. - Enhanced AreaTabs.vue to enable the educator tab and improve badge styling. - Refined BottomNav.vue to handle cart visibility and navigation more effectively. These changes aim to enhance user navigation and overall experience within the application.
This commit is contained in:
@@ -7,16 +7,10 @@ import { Button } from '@/components/ui/button'
|
||||
const { itemCount, total } = useCart()
|
||||
const { open } = useCartUI()
|
||||
|
||||
// Get current route
|
||||
const route = useRoute()
|
||||
|
||||
// Determine if FAB should be visible
|
||||
const isVisible = computed(() => {
|
||||
// Only show on /products and /products/[id] routes
|
||||
const isProductPage = route.path === '/products' || route.path.startsWith('/products/')
|
||||
|
||||
// Only show when cart has items
|
||||
return isProductPage && itemCount.value > 0
|
||||
// Show when cart has items (on all pages)
|
||||
return itemCount.value > 0
|
||||
})
|
||||
|
||||
// Format price as EUR in German locale
|
||||
|
||||
@@ -54,7 +54,7 @@ async function handleLogout() {
|
||||
<template>
|
||||
<!-- Not logged in: Show login prompt -->
|
||||
<NuxtLink v-if="!loggedIn" to="/auth"
|
||||
class="btn-secondary flex items-center gap-2 px-4 py-2.5"
|
||||
class="flex items-center gap-2 px-4 py-3.5 rounded-[35px] border-2 border-dashed border-white/30 hover:border-experimenta-accent hover:bg-experimenta-accent/10 transition-all"
|
||||
aria-label="Anmelden oder Registrieren">
|
||||
<User class="h-5 w-5" />
|
||||
<span class="font-medium hidden sm:inline">Anmelden</span>
|
||||
@@ -64,7 +64,7 @@ async function handleLogout() {
|
||||
<DropdownMenu v-else>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<button
|
||||
class="flex items-center gap-3 rounded-[35px] px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-experimenta-accent focus:ring-offset-2 transition-all hover:bg-white/10"
|
||||
class="flex items-center gap-3 rounded-[35px] px-2 py-[10px] focus:outline-none focus:ring-2 focus:ring-experimenta-accent focus:ring-offset-2 transition-all hover:bg-white/10"
|
||||
aria-label="Benutzermenü öffnen">
|
||||
<!-- Greeting text (Desktop only) -->
|
||||
<span class="hidden md:inline font-medium text-white pl-2">
|
||||
|
||||
@@ -24,7 +24,7 @@ const { loggedIn } = useAuth()
|
||||
</div>
|
||||
|
||||
<!-- Right: Cart + User Menu -->
|
||||
<div class="flex items-end gap-3 md:gap-6">
|
||||
<div class="flex items-end md:gap-6">
|
||||
<!-- Cart Button (only visible when logged in) -->
|
||||
<CartButton v-if="loggedIn" />
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const areas: ProductArea[] = [
|
||||
id: 'educator',
|
||||
label: 'Bildung',
|
||||
icon: GraduationCap,
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
visible: true,
|
||||
badge: 'Demnächst',
|
||||
route: '/educator',
|
||||
@@ -97,13 +97,12 @@ function navigateToArea(area: ProductArea) {
|
||||
]" @click="navigateToArea(area)">
|
||||
<component :is="area.icon" class="h-4 w-4" />
|
||||
<span>{{ area.label }}</span>
|
||||
<Badge v-if="area.badge"
|
||||
:class="[
|
||||
'ml-1 text-[10px] px-1.5 py-0.5 transition-colors',
|
||||
currentArea === area.id
|
||||
? 'bg-white/90 text-purple-950 border-white/50'
|
||||
: 'bg-experimenta-accent/20 text-experimenta-accent border-experimenta-accent/30 hover:bg-experimenta-accent/30'
|
||||
]">
|
||||
<Badge v-if="area.badge" :class="[
|
||||
'ml-1 text-[10px] px-1.5 py-0.5 pointer-events-none rounded-[35px]',
|
||||
currentArea === area.id
|
||||
? 'bg-white/90 text-purple-950 border-white/50'
|
||||
: 'bg-experimenta-accent/20 text-experimenta-accent border-experimenta-accent/30'
|
||||
]">
|
||||
{{ area.badge }}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
@@ -122,13 +121,12 @@ function navigateToArea(area: ProductArea) {
|
||||
]" @click="navigateToArea(area)">
|
||||
<component :is="area.icon" class="h-4 w-4" />
|
||||
<span>{{ area.label }}</span>
|
||||
<Badge v-if="area.badge"
|
||||
:class="[
|
||||
'ml-1 text-[10px] px-1.5 py-0.5 transition-colors',
|
||||
currentArea === area.id
|
||||
? 'bg-white/90 text-purple-950 border-white/50'
|
||||
: 'bg-experimenta-accent/20 text-experimenta-accent border-experimenta-accent/30 hover:bg-experimenta-accent/30'
|
||||
]">
|
||||
<Badge v-if="area.badge" :class="[
|
||||
'ml-1 text-[10px] px-1.5 py-0.5 pointer-events-none rounded-[35px]',
|
||||
currentArea === area.id
|
||||
? 'bg-white/90 text-purple-950 border-white/50'
|
||||
: 'bg-experimenta-accent/20 text-experimenta-accent border-experimenta-accent/30'
|
||||
]">
|
||||
{{ area.badge }}
|
||||
</Badge>
|
||||
</button>
|
||||
|
||||
@@ -4,13 +4,14 @@ import { Badge } from '@/components/ui/badge'
|
||||
|
||||
const route = useRoute()
|
||||
const { loggedIn } = useAuth()
|
||||
const { itemCount } = useCart()
|
||||
const { open: openCart, isOpen: isCartOpen } = useCartUI()
|
||||
|
||||
interface NavItem {
|
||||
id: string
|
||||
label: string
|
||||
icon: any
|
||||
route: string
|
||||
badge?: number
|
||||
requireAuth?: boolean
|
||||
}
|
||||
|
||||
@@ -31,9 +32,8 @@ const navItems: NavItem[] = [
|
||||
id: 'cart',
|
||||
label: 'Warenkorb',
|
||||
icon: ShoppingCart,
|
||||
route: '/cart',
|
||||
badge: 0, // TODO: Get from cart store
|
||||
requireAuth: true,
|
||||
route: '/cart', // Not used for navigation, but kept for consistency
|
||||
requireAuth: false, // Cart should be accessible without auth
|
||||
},
|
||||
{
|
||||
id: 'profile',
|
||||
@@ -43,14 +43,26 @@ const navItems: NavItem[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const isActive = (itemRoute: string) => {
|
||||
if (itemRoute === '/') {
|
||||
const isActive = (item: NavItem) => {
|
||||
// Special handling for cart: check if cart is open
|
||||
if (item.id === 'cart') {
|
||||
return isCartOpen.value
|
||||
}
|
||||
|
||||
// For other items, check route
|
||||
if (item.route === '/') {
|
||||
return route.path === '/'
|
||||
}
|
||||
return route.path.startsWith(itemRoute)
|
||||
return route.path.startsWith(item.route)
|
||||
}
|
||||
|
||||
function handleNavClick(item: NavItem) {
|
||||
// Special handling for cart: open cart instead of navigating
|
||||
if (item.id === 'cart') {
|
||||
openCart()
|
||||
return
|
||||
}
|
||||
|
||||
if (item.requireAuth && !loggedIn.value) {
|
||||
// Redirect to auth page
|
||||
navigateTo('/auth')
|
||||
@@ -65,66 +77,46 @@ function handleNavClick(item: NavItem) {
|
||||
<!-- 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"
|
||||
>
|
||||
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"
|
||||
>
|
||||
<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)
|
||||
? '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) ? 'page' : undefined">
|
||||
<!-- Icon with badge -->
|
||||
<div class="relative">
|
||||
<component
|
||||
:is="item.icon"
|
||||
:class="[
|
||||
'h-5 w-5 transition-transform',
|
||||
isActive(item.route) && 'scale-110',
|
||||
]"
|
||||
/>
|
||||
<component :is="item.icon" :class="[
|
||||
'h-5 w-5 transition-transform',
|
||||
isActive(item) && '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 v-if="item.id === 'cart' && itemCount > 0"
|
||||
class="absolute -top-2 -right-2 h-4 min-w-[16px] px-1 flex items-center justify-center text-[10px] bg-experimenta-accent">
|
||||
{{ itemCount > 99 ? '99+' : itemCount }}
|
||||
</Badge>
|
||||
|
||||
<!-- Login indicator dot for profile when not logged in -->
|
||||
<span
|
||||
v-if="item.id === 'profile' && !loggedIn"
|
||||
<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"
|
||||
/>
|
||||
aria-label="Nicht angemeldet" />
|
||||
</div>
|
||||
|
||||
<!-- Label -->
|
||||
<span
|
||||
:class="[
|
||||
'text-[10px] font-medium transition-all',
|
||||
isActive(item.route) && 'font-bold',
|
||||
]"
|
||||
>
|
||||
<span :class="[
|
||||
'text-[10px] font-medium transition-all',
|
||||
isActive(item) && 'font-bold',
|
||||
]">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
|
||||
<!-- Active indicator bar -->
|
||||
<span
|
||||
v-if="isActive(item.route)"
|
||||
<span v-if="isActive(item)"
|
||||
class="absolute bottom-0 left-1/2 -translate-x-1/2 w-12 h-0.5 bg-experimenta-accent rounded-full"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,28 +24,20 @@ function handleClick(e: Event) {
|
||||
|
||||
<template>
|
||||
<!-- Desktop cart button (visible only on lg and up) -->
|
||||
<button
|
||||
@click="handleClick"
|
||||
class="relative hidden lg:flex items-center gap-4 rounded-[35px] px-6 py-1.5 transition-all hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-experimenta-accent focus:ring-offset-2 focus:ring-offset-transparent"
|
||||
aria-label="Warenkorb öffnen"
|
||||
>
|
||||
<button @click="handleClick" :class="[
|
||||
'relative hidden lg:flex items-center rounded-[35px] px-6 py-1.5 transition-all hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-experimenta-accent focus:ring-offset-2 focus:ring-offset-transparent',
|
||||
hasItems ? 'gap-2' : 'gap-1'
|
||||
]" aria-label="Warenkorb öffnen">
|
||||
<!-- Cart icon with item count badge -->
|
||||
<div class="relative inline-flex items-center justify-center h-12 w-12">
|
||||
<div class="relative inline-flex items-center justify-center h-14 w-12">
|
||||
<ShoppingCart class="h-6 w-6 text-white" strokeWidth="2" />
|
||||
|
||||
<!-- Item count badge -->
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-200 ease-out"
|
||||
enter-from-class="scale-0 opacity-0"
|
||||
enter-to-class="scale-100 opacity-100"
|
||||
leave-active-class="transition-all duration-150 ease-in"
|
||||
leave-from-class="scale-100 opacity-100"
|
||||
leave-to-class="scale-0 opacity-0"
|
||||
>
|
||||
<Badge
|
||||
v-if="hasItems"
|
||||
class="absolute -top-1 -right-1 h-5.5 min-w-[22px] px-1.5 flex items-center justify-center bg-experimenta-accent text-white text-xs font-bold border-2 border-purple-darkest shadow-lg"
|
||||
>
|
||||
<Transition enter-active-class="transition-all duration-200 ease-out" enter-from-class="scale-0 opacity-0"
|
||||
enter-to-class="scale-100 opacity-100" leave-active-class="transition-all duration-150 ease-in"
|
||||
leave-from-class="scale-100 opacity-100" leave-to-class="scale-0 opacity-0">
|
||||
<Badge v-if="hasItems"
|
||||
class="absolute -top-1 -right-1 h-5.5 min-w-[22px] px-1.5 flex items-center justify-center bg-experimenta-accent text-white text-xs font-bold border-2 border-purple-darkest shadow-lg pointer-events-none">
|
||||
{{ itemCount > 99 ? '99+' : itemCount }}
|
||||
</Badge>
|
||||
</Transition>
|
||||
@@ -55,11 +47,5 @@ function handleClick(e: Event) {
|
||||
<span class="text-base font-bold text-white tabular-nums">
|
||||
{{ formattedTotal }}
|
||||
</span>
|
||||
|
||||
<!-- Static background -->
|
||||
<span
|
||||
class="absolute inset-0 rounded-[35px] bg-white/10"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -51,7 +51,7 @@ const getDiscount = (category: string): number | undefined => {
|
||||
|
||||
<template>
|
||||
<NuxtLayout name="default">
|
||||
<div class="min-h-screen bg-gradient-primary px-4 py-12 md:px-6 lg:px-8">
|
||||
<div class="min-h-screen px-4 py-12 md:px-6 lg:px-8">
|
||||
<!-- Page Header -->
|
||||
<div class="mx-auto mb-12 max-w-container-wide text-center">
|
||||
<h1 class="mb-4 text-4xl font-bold text-white md:text-5xl">
|
||||
@@ -85,17 +85,10 @@ const getDiscount = (category: string): number | undefined => {
|
||||
<!-- Products Grid -->
|
||||
<div v-else-if="products && products.length > 0" class="mx-auto max-w-container-wide">
|
||||
<ProductGrid :columns="3">
|
||||
<ProductCard
|
||||
v-for="product in products"
|
||||
:key="product.id"
|
||||
image="/img/makerspace-jk-2025.jpg"
|
||||
:title="product.name"
|
||||
:description="product.description"
|
||||
:price="Number(product.price)"
|
||||
:badge="getBadge(product.category)"
|
||||
:discount-percentage="getDiscount(product.category)"
|
||||
:product-id="product.id"
|
||||
/>
|
||||
<ProductCard v-for="product in products" :key="product.id" image="/img/makerspace-jk-2025.jpg"
|
||||
:title="product.name" :description="product.description" :price="Number(product.price)"
|
||||
:badge="getBadge(product.category)" :discount-percentage="getDiscount(product.category)"
|
||||
:product-id="product.id" />
|
||||
</ProductGrid>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const getDiscount = (category: string): number | undefined => {
|
||||
|
||||
<template>
|
||||
<NuxtLayout name="default">
|
||||
<div class="min-h-screen bg-gradient-primary px-4 py-12 md:px-6 lg:px-8">
|
||||
<div class="min-h-screen px-4 py-12 md:px-6 lg:px-8">
|
||||
<!-- Page Header -->
|
||||
<div class="mx-auto mb-12 max-w-container-wide text-center">
|
||||
<h1 class="mb-4 text-4xl font-bold text-white md:text-5xl">
|
||||
|
||||
@@ -97,7 +97,7 @@ const handleAddToCart = async () => {
|
||||
|
||||
<template>
|
||||
<NuxtLayout name="default">
|
||||
<div class="min-h-screen bg-gradient-primary px-4 py-12 md:px-6 lg:px-8">
|
||||
<div class="min-h-screen px-4 py-12 md:px-6 lg:px-8">
|
||||
<!-- Back Button -->
|
||||
<div class="mx-auto mb-8 max-w-container-narrow">
|
||||
<NuxtLink to="/products"
|
||||
|
||||
@@ -52,7 +52,7 @@ const getDiscount = (category: string): number | undefined => {
|
||||
|
||||
<template>
|
||||
<NuxtLayout name="default">
|
||||
<div class="min-h-screen bg-gradient-primary px-4 py-12 md:px-6 lg:px-8">
|
||||
<div class="min-h-screen px-4 py-12 md:px-6 lg:px-8">
|
||||
<!-- Page Header -->
|
||||
<div class="mx-auto mb-12 max-w-container-wide text-center">
|
||||
<h1 class="mb-4 text-4xl font-bold text-white md:text-5xl">
|
||||
@@ -86,17 +86,10 @@ const getDiscount = (category: string): number | undefined => {
|
||||
<!-- Products Grid -->
|
||||
<div v-else-if="products && products.length > 0" class="mx-auto max-w-container-wide">
|
||||
<ProductGrid :columns="3">
|
||||
<ProductCard
|
||||
v-for="product in products"
|
||||
:key="product.id"
|
||||
image="/img/makerspace-jk-2025.jpg"
|
||||
:title="product.name"
|
||||
:description="product.description"
|
||||
:price="Number(product.price)"
|
||||
:badge="getBadge(product.category)"
|
||||
:discount-percentage="getDiscount(product.category)"
|
||||
:product-id="product.id"
|
||||
/>
|
||||
<ProductCard v-for="product in products" :key="product.id" image="/img/makerspace-jk-2025.jpg"
|
||||
:title="product.name" :description="product.description" :price="Number(product.price)"
|
||||
:badge="getBadge(product.category)" :discount-percentage="getDiscount(product.category)"
|
||||
:product-id="product.id" />
|
||||
</ProductGrid>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user