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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
)"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user