Browse Source

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.
main
Bastian Masanek 2 months ago
parent
commit
a22e4b42ca
  1. 10
      app/components/Cart/CartFAB.vue
  2. 4
      app/components/UserMenu.vue
  3. 2
      app/components/navigation/AppHeader.vue
  4. 16
      app/components/navigation/AreaTabs.vue
  5. 80
      app/components/navigation/BottomNav.vue
  6. 34
      app/components/navigation/CartButton.vue
  7. 17
      app/pages/educator/index.vue
  8. 2
      app/pages/experimenta/index.vue
  9. 2
      app/pages/products/[id].vue
  10. 17
      app/pages/products/index.vue

10
app/components/Cart/CartFAB.vue

@ -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

4
app/components/UserMenu.vue

@ -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">

2
app/components/navigation/AppHeader.vue

@ -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" />

16
app/components/navigation/AreaTabs.vue

@ -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,12 +97,11 @@ 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',
<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 hover:bg-experimenta-accent/30'
: 'bg-experimenta-accent/20 text-experimenta-accent border-experimenta-accent/30'
]">
{{ area.badge }}
</Badge>
@ -122,12 +121,11 @@ 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',
<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 hover:bg-experimenta-accent/30'
: 'bg-experimenta-accent/20 text-experimenta-accent border-experimenta-accent/30'
]">
{{ area.badge }}
</Badge>

80
app/components/navigation/BottomNav.vue

@ -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="[
<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)
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.route) ? 'page' : undefined"
>
]" @click="handleNavClick(item)" :aria-label="item.label" :aria-current="isActive(item) ? 'page' : undefined">
<!-- Icon with badge -->
<div class="relative">
<component
:is="item.icon"
:class="[
<component :is="item.icon" :class="[
'h-5 w-5 transition-transform',
isActive(item.route) && 'scale-110',
]"
/>
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="[
<span :class="[
'text-[10px] font-medium transition-all',
isActive(item.route) && 'font-bold',
]"
>
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>

34
app/components/navigation/CartButton.vue

@ -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>

17
app/pages/educator/index.vue

@ -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>

2
app/pages/experimenta/index.vue

@ -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">

2
app/pages/products/[id].vue

@ -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"

17
app/pages/products/index.vue

@ -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>

Loading…
Cancel
Save