Refactor UserMenu and navigation components for improved user experience
- Enhanced UserMenu.vue by implementing an extended user type for better user data handling and updated user initials logic. - Adjusted AppHeader.vue and AreaTabs.vue for improved layout and spacing, ensuring a more cohesive design. - Updated CartButton.vue and Tabs components for consistent styling and better responsiveness. These changes aim to enhance the overall usability and visual appeal of the application, providing a smoother user experience.
This commit is contained in:
@@ -16,17 +16,27 @@ import {
|
|||||||
|
|
||||||
const { user, loggedIn, logout } = useAuth()
|
const { user, loggedIn, logout } = useAuth()
|
||||||
|
|
||||||
|
// Extended user type with all profile fields
|
||||||
|
type ExtendedUser = typeof user.value & {
|
||||||
|
id?: string
|
||||||
|
firstName?: string
|
||||||
|
lastName?: string
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const extendedUser = computed(() => user.value as ExtendedUser)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user initials for Avatar fallback
|
* Get user initials for Avatar fallback
|
||||||
* Example: "Bastian Masanek" → "BM"
|
* Example: "Bastian Masanek" → "BM"
|
||||||
*/
|
*/
|
||||||
const userInitials = computed(() => {
|
const userInitials = computed(() => {
|
||||||
if (!user.value) return '?'
|
if (!extendedUser.value) return '?'
|
||||||
|
|
||||||
const first = user.value.firstName?.charAt(0)?.toUpperCase() || ''
|
const first = extendedUser.value.firstName?.charAt(0)?.toUpperCase() || ''
|
||||||
const last = user.value.lastName?.charAt(0)?.toUpperCase() || ''
|
const last = extendedUser.value.lastName?.charAt(0)?.toUpperCase() || ''
|
||||||
|
|
||||||
return first + last || user.value.email?.charAt(0)?.toUpperCase() || '?'
|
return first + last || extendedUser.value.email?.charAt(0)?.toUpperCase() || '?'
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,13 +54,9 @@ async function handleLogout() {
|
|||||||
<template>
|
<template>
|
||||||
<!-- Not logged in: Show login prompt -->
|
<!-- Not logged in: Show login prompt -->
|
||||||
<NuxtLink v-if="!loggedIn" to="/auth"
|
<NuxtLink v-if="!loggedIn" to="/auth"
|
||||||
class="flex items-center gap-2 px-4 py-2 rounded-[35px] border-2 border-dashed border-muted-foreground/30 hover:border-experimenta-accent hover:bg-experimenta-accent/10 transition-all"
|
class="btn-secondary flex items-center gap-2 px-4 py-2.5"
|
||||||
aria-label="Anmelden oder Registrieren">
|
aria-label="Anmelden oder Registrieren">
|
||||||
<Avatar class="h-10 w-10 border-2 border-muted-foreground/30 rounded-2xl">
|
<User class="h-5 w-5" />
|
||||||
<AvatarFallback class="bg-muted text-muted-foreground rounded-2xl">
|
|
||||||
<User class="h-5 w-5" />
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<span class="font-medium hidden sm:inline">Anmelden</span>
|
<span class="font-medium hidden sm:inline">Anmelden</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
@@ -62,13 +68,13 @@ async function handleLogout() {
|
|||||||
aria-label="Benutzermenü öffnen">
|
aria-label="Benutzermenü öffnen">
|
||||||
<!-- Greeting text (Desktop only) -->
|
<!-- Greeting text (Desktop only) -->
|
||||||
<span class="hidden md:inline font-medium text-white pl-2">
|
<span class="hidden md:inline font-medium text-white pl-2">
|
||||||
Hallo, {{ user?.firstName }}
|
Hallo, {{ extendedUser?.firstName }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Avatar with status ring (logged in indicator) -->
|
<!-- Avatar with status ring (logged in indicator) -->
|
||||||
<Avatar
|
<Avatar
|
||||||
class="h-10 w-10 md:h-12 md:w-12 border-3 border-experimenta-purple shadow-lg bg-experimenta-accent ring-2 ring-experimenta-accent ring-offset-2 ring-offset-white dark:ring-offset-zinc-950">
|
class="h-10 w-10 md:h-12 md:w-12 border-3 border-experimenta-purple shadow-lg bg-experimenta-accent ring-2 ring-experimenta-accent ring-offset-2 ring-offset-white dark:ring-offset-zinc-950">
|
||||||
<AvatarImage :src="undefined" :alt="user?.firstName" />
|
<AvatarImage :src="undefined" :alt="extendedUser?.firstName || ''" />
|
||||||
<AvatarFallback
|
<AvatarFallback
|
||||||
class="bg-experimenta-accent text-white font-bold text-sm md:text-base flex items-center justify-center w-full h-full">
|
class="bg-experimenta-accent text-white font-bold text-sm md:text-base flex items-center justify-center w-full h-full">
|
||||||
{{ userInitials }}
|
{{ userInitials }}
|
||||||
@@ -82,10 +88,10 @@ async function handleLogout() {
|
|||||||
<DropdownMenuLabel class="font-normal py-3">
|
<DropdownMenuLabel class="font-normal py-3">
|
||||||
<div class="flex flex-col space-y-1.5">
|
<div class="flex flex-col space-y-1.5">
|
||||||
<p class="text-base font-semibold leading-none">
|
<p class="text-base font-semibold leading-none">
|
||||||
{{ user?.firstName }} {{ user?.lastName }}
|
{{ extendedUser?.firstName }} {{ extendedUser?.lastName }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm leading-none text-muted-foreground">
|
<p class="text-sm leading-none text-muted-foreground">
|
||||||
{{ user?.email }}
|
{{ extendedUser?.email }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
|||||||
@@ -10,12 +10,11 @@ const { loggedIn } = useAuth()
|
|||||||
<template>
|
<template>
|
||||||
<header class="sticky top-0 z-50 w-full">
|
<header class="sticky top-0 z-50 w-full">
|
||||||
<!-- Main header bar -->
|
<!-- Main header bar -->
|
||||||
<div class="container flex h-24 items-center justify-between px-4 md:px-6 lg:px-8">
|
<div class="container flex items-end justify-between px-4 md:px-6 lg:px-8 py-6 md:py-7">
|
||||||
<!-- Left: Logo + Role Switcher -->
|
<!-- Left: Logo + Role Switcher -->
|
||||||
<div class="flex items-center gap-6 md:gap-8">
|
<div class="flex items-end gap-6 md:gap-8">
|
||||||
<NuxtLink to="/" class="flex-shrink-0 transition-transform hover:scale-105">
|
<NuxtLink to="/" class="flex-shrink-0 transition-transform hover:scale-105">
|
||||||
<img src="/img/experimenta-logo-white.svg" alt="experimenta Logo"
|
<img src="/img/experimenta-logo-white.svg" alt="experimenta Logo" class="h-16 w-auto md:h-20 lg:h-22" />
|
||||||
class="h-14 w-auto md:h-16 drop-shadow-lg" />
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<!-- Role Switcher (Desktop only) -->
|
<!-- Role Switcher (Desktop only) -->
|
||||||
@@ -25,7 +24,7 @@ const { loggedIn } = useAuth()
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: Cart + User Menu -->
|
<!-- Right: Cart + User Menu -->
|
||||||
<div class="flex items-center gap-3 md:gap-6">
|
<div class="flex items-end gap-3 md:gap-6">
|
||||||
<!-- Cart Button (only visible when logged in) -->
|
<!-- Cart Button (only visible when logged in) -->
|
||||||
<CartButton v-if="loggedIn" />
|
<CartButton v-if="loggedIn" />
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const areas: ProductArea[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'makerspace',
|
id: 'makerspace',
|
||||||
label: 'Makerspace Jahreskarte',
|
label: 'Makerspace',
|
||||||
icon: Wrench,
|
icon: Wrench,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -32,7 +32,7 @@ const areas: ProductArea[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'educator',
|
id: 'educator',
|
||||||
label: 'Pädagogen Jahreskarte',
|
label: 'Bildung',
|
||||||
icon: GraduationCap,
|
icon: GraduationCap,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -41,7 +41,7 @@ const areas: ProductArea[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'experimenta',
|
id: 'experimenta',
|
||||||
label: 'experimenta Jahreskarte',
|
label: 'experimenta',
|
||||||
icon: Sparkles,
|
icon: Sparkles,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -89,15 +89,21 @@ function navigateToArea(area: ProductArea) {
|
|||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<!-- Desktop: Tabs -->
|
<!-- Desktop: Tabs -->
|
||||||
<Tabs :model-value="currentArea" class="hidden md:block">
|
<Tabs :model-value="currentArea" class="hidden md:block">
|
||||||
<TabsList class="h-auto p-1.5 bg-white/5">
|
<TabsList class="h-auto p-2 bg-white/5">
|
||||||
<TabsTrigger v-for="area in areas.filter(area => area.visible)" :key="area.id" :value="area.id"
|
<TabsTrigger v-for="area in areas.filter(area => area.visible)" :key="area.id" :value="area.id"
|
||||||
:disabled="!area.enabled" :class="[
|
:disabled="!area.enabled" :class="[
|
||||||
'gap-2 data-[state=active]:bg-accent data-[state=active]:text-white data-[state=active]:shadow-md',
|
'gap-2 py-3 md:py-4 data-[state=active]:bg-accent data-[state=active]:text-white data-[state=active]:shadow-md',
|
||||||
!area.enabled && 'opacity-50 cursor-not-allowed',
|
!area.enabled && 'opacity-50 cursor-not-allowed',
|
||||||
]" @click="navigateToArea(area)">
|
]" @click="navigateToArea(area)">
|
||||||
<component :is="area.icon" class="h-4 w-4" />
|
<component :is="area.icon" class="h-4 w-4" />
|
||||||
<span>{{ area.label }}</span>
|
<span>{{ area.label }}</span>
|
||||||
<Badge v-if="area.badge" variant="secondary" class="ml-1 text-[10px] px-1.5 py-0">
|
<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'
|
||||||
|
]">
|
||||||
{{ area.badge }}
|
{{ area.badge }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -106,9 +112,9 @@ function navigateToArea(area: ProductArea) {
|
|||||||
|
|
||||||
<!-- Mobile: Horizontal scroll with cards (matching desktop styling) -->
|
<!-- Mobile: Horizontal scroll with cards (matching desktop styling) -->
|
||||||
<div class="md:hidden overflow-x-auto scrollbar-hide">
|
<div class="md:hidden overflow-x-auto scrollbar-hide">
|
||||||
<div class="inline-flex h-auto items-center justify-center rounded-[35px] bg-white/5 p-1.5 min-w-max">
|
<div class="inline-flex h-auto items-center justify-center rounded-[35px] bg-white/5 p-2 min-w-max">
|
||||||
<button v-for="area in areas.filter(area => area.visible)" :key="area.id" :disabled="!area.enabled" :class="[
|
<button v-for="area in areas.filter(area => area.visible)" :key="area.id" :disabled="!area.enabled" :class="[
|
||||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[25px] px-4 py-[10px] text-lg font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-0',
|
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[25px] px-4 py-3 text-lg font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-0',
|
||||||
currentArea === area.id
|
currentArea === area.id
|
||||||
? 'bg-accent text-white shadow-md'
|
? 'bg-accent text-white shadow-md'
|
||||||
: 'text-white/70 hover:text-white',
|
: 'text-white/70 hover:text-white',
|
||||||
@@ -116,7 +122,13 @@ function navigateToArea(area: ProductArea) {
|
|||||||
]" @click="navigateToArea(area)">
|
]" @click="navigateToArea(area)">
|
||||||
<component :is="area.icon" class="h-4 w-4" />
|
<component :is="area.icon" class="h-4 w-4" />
|
||||||
<span>{{ area.label }}</span>
|
<span>{{ area.label }}</span>
|
||||||
<Badge v-if="area.badge" variant="secondary" class="ml-1 text-[10px] px-1.5 py-0">
|
<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'
|
||||||
|
]">
|
||||||
{{ area.badge }}
|
{{ area.badge }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ function handleClick(e: Event) {
|
|||||||
<!-- Desktop cart button (visible only on lg and up) -->
|
<!-- Desktop cart button (visible only on lg and up) -->
|
||||||
<button
|
<button
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
class="relative hidden lg:flex items-center gap-5 rounded-[25px] px-[30px] py-[10px] transition-all hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-experimenta-accent focus:ring-offset-2 focus:ring-offset-transparent"
|
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"
|
aria-label="Warenkorb öffnen"
|
||||||
>
|
>
|
||||||
<!-- Cart icon with item count badge -->
|
<!-- Cart icon with item count badge -->
|
||||||
<div class="relative inline-flex items-center justify-center">
|
<div class="relative inline-flex items-center justify-center h-12 w-12">
|
||||||
<ShoppingCart class="h-6 w-6 text-white" strokeWidth="2" />
|
<ShoppingCart class="h-6 w-6 text-white" strokeWidth="2" />
|
||||||
|
|
||||||
<!-- Item count badge -->
|
<!-- Item count badge -->
|
||||||
@@ -44,7 +44,7 @@ function handleClick(e: Event) {
|
|||||||
>
|
>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="hasItems"
|
v-if="hasItems"
|
||||||
class="absolute -top-2.5 -right-3.5 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"
|
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"
|
||||||
>
|
>
|
||||||
{{ itemCount > 99 ? '99+' : itemCount }}
|
{{ itemCount > 99 ? '99+' : itemCount }}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -58,7 +58,7 @@ function handleClick(e: Event) {
|
|||||||
|
|
||||||
<!-- Static background -->
|
<!-- Static background -->
|
||||||
<span
|
<span
|
||||||
class="absolute inset-0 rounded-[25px] bg-white/10"
|
class="absolute inset-0 rounded-[35px] bg-white/10"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const forwarded = useForwardProps(delegatedProps)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TabsList v-bind="forwarded" :class="cn(
|
<TabsList v-bind="forwarded" :class="cn(
|
||||||
'inline-flex h-auto items-center justify-center rounded-[35px] bg-white/5 p-1.5 text-white/70',
|
'inline-flex h-auto items-center justify-center rounded-[45px] bg-white/5 p-1.5 text-white/70',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const forwarded = useForwardProps(delegatedProps)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TabsTrigger v-bind="forwarded" :class="cn(
|
<TabsTrigger v-bind="forwarded" :class="cn(
|
||||||
'inline-flex items-center justify-center whitespace-nowrap rounded-[25px] px-4 py-[10px] text-lg font-medium ring-offset-transparent transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50 text-white/70 hover:text-white data-[state=active]:bg-accent data-[state=active]:text-white data-[state=active]:shadow-md leading-[1.7em]',
|
'inline-flex items-center justify-center whitespace-nowrap rounded-[45px] px-4 py-[10px] text-lg font-medium ring-offset-transparent transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50 text-white/70 hover:text-white data-[state=active]:bg-accent data-[state=active]:text-white data-[state=active]:shadow-md leading-[1.7em]',
|
||||||
props.class
|
props.class
|
||||||
)">
|
)">
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
Reference in New Issue
Block a user