Implement experimenta product listing page and enhance navigation components

- Added a new page for displaying experimenta annual passes, integrating ProductCard and ProductGrid components for product presentation.
- Updated API to filter products by category, allowing for specific product queries.
- Enhanced navigation components, including AreaTabs and RoleSwitcher, to improve user experience and accessibility.
- Refactored existing components for better styling consistency and responsiveness.
- Improved dropdown menu components with updated styles and hover effects for better usability.
This commit is contained in:
Bastian Masanek
2025-11-01 20:08:28 +01:00
parent 81495d5e17
commit a826ea1344
9 changed files with 207 additions and 64 deletions

View File

@@ -43,12 +43,9 @@ async function handleLogout() {
<template>
<!-- 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-2xl border-2 border-dashed border-muted-foreground/30 hover:border-experimenta-accent hover:bg-experimenta-accent/10 transition-all"
aria-label="Anmelden oder Registrieren"
>
aria-label="Anmelden oder Registrieren">
<Avatar class="h-10 w-10 border-2 border-muted-foreground/30 rounded-2xl">
<AvatarFallback class="bg-muted text-muted-foreground rounded-2xl">
<User class="h-5 w-5" />
@@ -62,17 +59,18 @@ async function handleLogout() {
<DropdownMenuTrigger as-child>
<button
class="flex items-center gap-3 rounded-2xl 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"
aria-label="Benutzermenü öffnen"
>
aria-label="Benutzermenü öffnen">
<!-- Greeting text (Desktop only) -->
<span class="hidden md:inline text-sm font-medium text-white">
<span class="hidden md:inline font-medium text-white">
Hallo, {{ user?.firstName }}
</span>
<!-- Avatar with status ring (logged in indicator) -->
<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">
<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">
<AvatarImage :src="undefined" :alt="user?.firstName" />
<AvatarFallback class="bg-experimenta-accent text-white font-bold text-sm md:text-base flex items-center justify-center w-full h-full">
<AvatarFallback
class="bg-experimenta-accent text-white font-bold text-sm md:text-base flex items-center justify-center w-full h-full">
{{ userInitials }}
</AvatarFallback>
</Avatar>

View File

@@ -8,16 +8,26 @@ interface ProductArea {
label: string
icon: any
enabled: boolean
visible: boolean
badge?: string
route: string
}
const areas: ProductArea[] = [
{
id: 'makerspace',
label: 'Makerspace',
id: 'experimenta-annual-passes',
label: 'experimenta Jahreskarten',
icon: Sparkles,
enabled: true,
visible: false,
route: '/experimenta',
},
{
id: 'makerspace-annual-passes',
label: 'Makerspace Jahreskarten',
icon: Wrench,
enabled: true,
visible: true,
route: '/products',
},
{
@@ -25,17 +35,10 @@ const areas: ProductArea[] = [
label: 'Labore',
icon: FlaskConical,
enabled: false,
visible: false,
badge: 'Demnächst',
route: '/labs',
},
{
id: 'experimenta',
label: 'experimenta',
icon: Sparkles,
enabled: false,
badge: 'Demnächst',
route: '/experimenta',
},
]
const route = useRoute()
@@ -64,17 +67,11 @@ function navigateToArea(area: ProductArea) {
<!-- Desktop: Tabs -->
<Tabs :model-value="currentArea" class="hidden md:block">
<TabsList class="h-auto p-1 bg-muted/50">
<TabsTrigger
v-for="area in areas"
:key="area.id"
:value="area.id"
:disabled="!area.enabled"
:class="[
<TabsTrigger v-for="area in areas.filter(area => area.visible)" :key="area.id" :value="area.id"
:disabled="!area.enabled" :class="[
'gap-2 data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-900',
!area.enabled && 'opacity-60 cursor-not-allowed',
]"
@click="navigateToArea(area)"
>
]" @click="navigateToArea(area)">
<component :is="area.icon" class="h-4 w-4" />
<span>{{ area.label }}</span>
<Badge v-if="area.badge" variant="secondary" class="ml-1 text-[10px] px-1.5 py-0">
@@ -87,29 +84,17 @@ function navigateToArea(area: ProductArea) {
<!-- Mobile: Horizontal scroll with cards -->
<div class="md:hidden overflow-x-auto scrollbar-hide">
<div class="flex gap-2 p-1 min-w-max">
<button
v-for="area in areas"
:key="area.id"
:disabled="!area.enabled"
:class="[
'flex items-center gap-2 px-4 py-2 rounded-lg border-2 transition-all whitespace-nowrap',
currentArea === area.id
? 'bg-purple-600 text-white border-purple-600'
: 'bg-white dark:bg-zinc-900 border-border hover:border-purple-300',
!area.enabled && 'opacity-60 cursor-not-allowed',
]"
@click="navigateToArea(area)"
>
<component
:is="area.icon"
:class="['h-4 w-4', currentArea === area.id ? 'text-white' : '']"
/>
<button v-for="area in areas.filter(area => area.visible)" :key="area.id" :disabled="!area.enabled" :class="[
'flex items-center gap-2 px-4 py-2 rounded-lg border-2 transition-all whitespace-nowrap',
currentArea === area.id
? 'bg-purple-600 text-white border-purple-600'
: 'bg-white dark:bg-zinc-900 border-border hover:border-purple-300',
!area.enabled && 'opacity-60 cursor-not-allowed',
]" @click="navigateToArea(area)">
<component :is="area.icon" :class="['h-4 w-4', currentArea === area.id ? 'text-white' : '']" />
<span class="font-medium">{{ area.label }}</span>
<Badge
v-if="area.badge"
:variant="currentArea === area.id ? 'secondary' : 'outline'"
class="text-[10px] px-1.5 py-0"
>
<Badge v-if="area.badge" :variant="currentArea === area.id ? 'secondary' : 'outline'"
class="text-[10px] px-1.5 py-0">
{{ area.badge }}
</Badge>
</button>

View File

@@ -107,7 +107,7 @@ function switchRole(role: UserRole) {
:key="role.value"
:disabled="!role.enabled"
:class="[
'gap-3 py-3 cursor-pointer',
'gap-3 py-3 cursor-pointer group',
!role.enabled && 'opacity-60 cursor-not-allowed',
currentRole === role.value && 'bg-purple-50 dark:bg-purple-950',
]"
@@ -117,7 +117,7 @@ function switchRole(role: UserRole) {
<div class="flex-1 flex flex-col gap-1">
<span class="text-sm font-semibold">{{ role.label }}</span>
<span class="text-sm text-muted-foreground">{{ role.description }}</span>
<span class="text-sm text-muted-foreground group-hover:text-foreground/80 transition-colors">{{ role.description }}</span>
</div>
<Check

View File

@@ -27,7 +27,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<DropdownMenuPortal>
<DropdownMenuContent
v-bind="forwarded"
:class="cn('z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.class)"
:class="cn('z-50 min-w-32 overflow-hidden rounded-2xl border border-white/10 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-xl p-2 text-zinc-800 dark:text-zinc-100 shadow-glass data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.class)"
>
<slot />
</DropdownMenuContent>

View File

@@ -16,7 +16,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<DropdownMenuItem
v-bind="forwardedProps"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
'relative flex cursor-default select-none items-center rounded-xl gap-2 px-3 py-2.5 text-sm outline-none transition-all duration-200 hover:bg-accent focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-40 [&>svg]:size-4 [&>svg]:shrink-0',
inset && 'pl-8',
props.class,
)"

View File

@@ -15,7 +15,7 @@ const forwardedProps = useForwardProps(delegatedProps)
<template>
<DropdownMenuLabel
v-bind="forwardedProps"
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)"
:class="cn('px-3 py-2 text-sm font-semibold text-zinc-600 dark:text-zinc-400', inset && 'pl-8', props.class)"
>
<slot />
</DropdownMenuLabel>