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 { itemCount, total } = useCart()
|
||||||
const { open } = useCartUI()
|
const { open } = useCartUI()
|
||||||
|
|
||||||
// Get current route
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
// Determine if FAB should be visible
|
// Determine if FAB should be visible
|
||||||
const isVisible = computed(() => {
|
const isVisible = computed(() => {
|
||||||
// Only show on /products and /products/[id] routes
|
// Show when cart has items (on all pages)
|
||||||
const isProductPage = route.path === '/products' || route.path.startsWith('/products/')
|
return itemCount.value > 0
|
||||||
|
|
||||||
// Only show when cart has items
|
|
||||||
return isProductPage && itemCount.value > 0
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Format price as EUR in German locale
|
// Format price as EUR in German locale
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ 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="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">
|
aria-label="Anmelden oder Registrieren">
|
||||||
<User class="h-5 w-5" />
|
<User class="h-5 w-5" />
|
||||||
<span class="font-medium hidden sm:inline">Anmelden</span>
|
<span class="font-medium hidden sm:inline">Anmelden</span>
|
||||||
@@ -64,7 +64,7 @@ async function handleLogout() {
|
|||||||
<DropdownMenu v-else>
|
<DropdownMenu v-else>
|
||||||
<DropdownMenuTrigger as-child>
|
<DropdownMenuTrigger as-child>
|
||||||
<button
|
<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">
|
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">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const { loggedIn } = useAuth()
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: Cart + User Menu -->
|
<!-- 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) -->
|
<!-- Cart Button (only visible when logged in) -->
|
||||||
<CartButton v-if="loggedIn" />
|
<CartButton v-if="loggedIn" />
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const areas: ProductArea[] = [
|
|||||||
id: 'educator',
|
id: 'educator',
|
||||||
label: 'Bildung',
|
label: 'Bildung',
|
||||||
icon: GraduationCap,
|
icon: GraduationCap,
|
||||||
enabled: false,
|
enabled: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
badge: 'Demnächst',
|
badge: 'Demnächst',
|
||||||
route: '/educator',
|
route: '/educator',
|
||||||
@@ -97,13 +97,12 @@ 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"
|
<Badge v-if="area.badge" :class="[
|
||||||
:class="[
|
'ml-1 text-[10px] px-1.5 py-0.5 pointer-events-none rounded-[35px]',
|
||||||
'ml-1 text-[10px] px-1.5 py-0.5 transition-colors',
|
currentArea === area.id
|
||||||
currentArea === area.id
|
? 'bg-white/90 text-purple-950 border-white/50'
|
||||||
? 'bg-white/90 text-purple-950 border-white/50'
|
: 'bg-experimenta-accent/20 text-experimenta-accent border-experimenta-accent/30'
|
||||||
: 'bg-experimenta-accent/20 text-experimenta-accent border-experimenta-accent/30 hover:bg-experimenta-accent/30'
|
]">
|
||||||
]">
|
|
||||||
{{ area.badge }}
|
{{ area.badge }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -122,13 +121,12 @@ 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"
|
<Badge v-if="area.badge" :class="[
|
||||||
:class="[
|
'ml-1 text-[10px] px-1.5 py-0.5 pointer-events-none rounded-[35px]',
|
||||||
'ml-1 text-[10px] px-1.5 py-0.5 transition-colors',
|
currentArea === area.id
|
||||||
currentArea === area.id
|
? 'bg-white/90 text-purple-950 border-white/50'
|
||||||
? 'bg-white/90 text-purple-950 border-white/50'
|
: 'bg-experimenta-accent/20 text-experimenta-accent border-experimenta-accent/30'
|
||||||
: 'bg-experimenta-accent/20 text-experimenta-accent border-experimenta-accent/30 hover:bg-experimenta-accent/30'
|
]">
|
||||||
]">
|
|
||||||
{{ area.badge }}
|
{{ area.badge }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { Badge } from '@/components/ui/badge'
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { loggedIn } = useAuth()
|
const { loggedIn } = useAuth()
|
||||||
|
const { itemCount } = useCart()
|
||||||
|
const { open: openCart, isOpen: isCartOpen } = useCartUI()
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
icon: any
|
icon: any
|
||||||
route: string
|
route: string
|
||||||
badge?: number
|
|
||||||
requireAuth?: boolean
|
requireAuth?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,9 +32,8 @@ const navItems: NavItem[] = [
|
|||||||
id: 'cart',
|
id: 'cart',
|
||||||
label: 'Warenkorb',
|
label: 'Warenkorb',
|
||||||
icon: ShoppingCart,
|
icon: ShoppingCart,
|
||||||
route: '/cart',
|
route: '/cart', // Not used for navigation, but kept for consistency
|
||||||
badge: 0, // TODO: Get from cart store
|
requireAuth: false, // Cart should be accessible without auth
|
||||||
requireAuth: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'profile',
|
id: 'profile',
|
||||||
@@ -43,14 +43,26 @@ const navItems: NavItem[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const isActive = (itemRoute: string) => {
|
const isActive = (item: NavItem) => {
|
||||||
if (itemRoute === '/') {
|
// 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 === '/'
|
||||||
}
|
}
|
||||||
return route.path.startsWith(itemRoute)
|
return route.path.startsWith(item.route)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNavClick(item: NavItem) {
|
function handleNavClick(item: NavItem) {
|
||||||
|
// Special handling for cart: open cart instead of navigating
|
||||||
|
if (item.id === 'cart') {
|
||||||
|
openCart()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (item.requireAuth && !loggedIn.value) {
|
if (item.requireAuth && !loggedIn.value) {
|
||||||
// Redirect to auth page
|
// Redirect to auth page
|
||||||
navigateTo('/auth')
|
navigateTo('/auth')
|
||||||
@@ -65,66 +77,46 @@ function handleNavClick(item: NavItem) {
|
|||||||
<!-- Mobile Bottom Navigation - only visible on small screens -->
|
<!-- Mobile Bottom Navigation - only visible on small screens -->
|
||||||
<nav
|
<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"
|
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"
|
role="navigation" aria-label="Mobile Navigation">
|
||||||
aria-label="Mobile Navigation"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-around h-16 px-2">
|
<div class="flex items-center justify-around h-16 px-2">
|
||||||
<button
|
<button v-for="item in navItems" :key="item.id" :class="[
|
||||||
v-for="item in navItems"
|
'flex flex-col items-center justify-center flex-1 gap-1 py-2 px-1 rounded-lg transition-all relative',
|
||||||
:key="item.id"
|
isActive(item)
|
||||||
:class="[
|
? 'text-experimenta-accent bg-experimenta-accent/10'
|
||||||
'flex flex-col items-center justify-center flex-1 gap-1 py-2 px-1 rounded-lg transition-all relative',
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted',
|
||||||
isActive(item.route)
|
item.requireAuth && !loggedIn && 'opacity-75',
|
||||||
? 'text-experimenta-accent bg-experimenta-accent/10'
|
]" @click="handleNavClick(item)" :aria-label="item.label" :aria-current="isActive(item) ? 'page' : undefined">
|
||||||
: '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"
|
|
||||||
>
|
|
||||||
<!-- Icon with badge -->
|
<!-- Icon with badge -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<component
|
<component :is="item.icon" :class="[
|
||||||
:is="item.icon"
|
'h-5 w-5 transition-transform',
|
||||||
:class="[
|
isActive(item) && 'scale-110',
|
||||||
'h-5 w-5 transition-transform',
|
]" />
|
||||||
isActive(item.route) && 'scale-110',
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Badge for cart -->
|
<!-- Badge for cart -->
|
||||||
<Badge
|
<Badge v-if="item.id === 'cart' && itemCount > 0"
|
||||||
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">
|
||||||
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 }}
|
||||||
>
|
|
||||||
{{ item.badge > 99 ? '99+' : item.badge }}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<!-- Login indicator dot for profile when not logged in -->
|
<!-- Login indicator dot for profile when not logged in -->
|
||||||
<span
|
<span v-if="item.id === 'profile' && !loggedIn"
|
||||||
v-if="item.id === 'profile' && !loggedIn"
|
|
||||||
class="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-yellow-500 animate-pulse"
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- Label -->
|
<!-- Label -->
|
||||||
<span
|
<span :class="[
|
||||||
:class="[
|
'text-[10px] font-medium transition-all',
|
||||||
'text-[10px] font-medium transition-all',
|
isActive(item) && 'font-bold',
|
||||||
isActive(item.route) && 'font-bold',
|
]">
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Active indicator bar -->
|
<!-- Active indicator bar -->
|
||||||
<span
|
<span v-if="isActive(item)"
|
||||||
v-if="isActive(item.route)"
|
|
||||||
class="absolute bottom-0 left-1/2 -translate-x-1/2 w-12 h-0.5 bg-experimenta-accent rounded-full"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -24,28 +24,20 @@ function handleClick(e: Event) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Desktop cart button (visible only on lg and up) -->
|
<!-- Desktop cart button (visible only on lg and up) -->
|
||||||
<button
|
<button @click="handleClick" :class="[
|
||||||
@click="handleClick"
|
'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',
|
||||||
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"
|
hasItems ? 'gap-2' : 'gap-1'
|
||||||
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 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" />
|
<ShoppingCart class="h-6 w-6 text-white" strokeWidth="2" />
|
||||||
|
|
||||||
<!-- Item count badge -->
|
<!-- Item count badge -->
|
||||||
<Transition
|
<Transition enter-active-class="transition-all duration-200 ease-out" enter-from-class="scale-0 opacity-0"
|
||||||
enter-active-class="transition-all duration-200 ease-out"
|
enter-to-class="scale-100 opacity-100" leave-active-class="transition-all duration-150 ease-in"
|
||||||
enter-from-class="scale-0 opacity-0"
|
leave-from-class="scale-100 opacity-100" leave-to-class="scale-0 opacity-0">
|
||||||
enter-to-class="scale-100 opacity-100"
|
<Badge v-if="hasItems"
|
||||||
leave-active-class="transition-all duration-150 ease-in"
|
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">
|
||||||
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"
|
|
||||||
>
|
|
||||||
{{ itemCount > 99 ? '99+' : itemCount }}
|
{{ itemCount > 99 ? '99+' : itemCount }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Transition>
|
</Transition>
|
||||||
@@ -55,11 +47,5 @@ function handleClick(e: Event) {
|
|||||||
<span class="text-base font-bold text-white tabular-nums">
|
<span class="text-base font-bold text-white tabular-nums">
|
||||||
{{ formattedTotal }}
|
{{ formattedTotal }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Static background -->
|
|
||||||
<span
|
|
||||||
class="absolute inset-0 rounded-[35px] bg-white/10"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const getDiscount = (category: string): number | undefined => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLayout name="default">
|
<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 -->
|
<!-- Page Header -->
|
||||||
<div class="mx-auto mb-12 max-w-container-wide text-center">
|
<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">
|
<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 -->
|
<!-- Products Grid -->
|
||||||
<div v-else-if="products && products.length > 0" class="mx-auto max-w-container-wide">
|
<div v-else-if="products && products.length > 0" class="mx-auto max-w-container-wide">
|
||||||
<ProductGrid :columns="3">
|
<ProductGrid :columns="3">
|
||||||
<ProductCard
|
<ProductCard v-for="product in products" :key="product.id" image="/img/makerspace-jk-2025.jpg"
|
||||||
v-for="product in products"
|
:title="product.name" :description="product.description" :price="Number(product.price)"
|
||||||
:key="product.id"
|
:badge="getBadge(product.category)" :discount-percentage="getDiscount(product.category)"
|
||||||
image="/img/makerspace-jk-2025.jpg"
|
:product-id="product.id" />
|
||||||
:title="product.name"
|
|
||||||
:description="product.description"
|
|
||||||
:price="Number(product.price)"
|
|
||||||
:badge="getBadge(product.category)"
|
|
||||||
:discount-percentage="getDiscount(product.category)"
|
|
||||||
:product-id="product.id"
|
|
||||||
/>
|
|
||||||
</ProductGrid>
|
</ProductGrid>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const getDiscount = (category: string): number | undefined => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLayout name="default">
|
<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 -->
|
<!-- Page Header -->
|
||||||
<div class="mx-auto mb-12 max-w-container-wide text-center">
|
<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">
|
<h1 class="mb-4 text-4xl font-bold text-white md:text-5xl">
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ const handleAddToCart = async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLayout name="default">
|
<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 -->
|
<!-- Back Button -->
|
||||||
<div class="mx-auto mb-8 max-w-container-narrow">
|
<div class="mx-auto mb-8 max-w-container-narrow">
|
||||||
<NuxtLink to="/products"
|
<NuxtLink to="/products"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const getDiscount = (category: string): number | undefined => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLayout name="default">
|
<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 -->
|
<!-- Page Header -->
|
||||||
<div class="mx-auto mb-12 max-w-container-wide text-center">
|
<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">
|
<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 -->
|
<!-- Products Grid -->
|
||||||
<div v-else-if="products && products.length > 0" class="mx-auto max-w-container-wide">
|
<div v-else-if="products && products.length > 0" class="mx-auto max-w-container-wide">
|
||||||
<ProductGrid :columns="3">
|
<ProductGrid :columns="3">
|
||||||
<ProductCard
|
<ProductCard v-for="product in products" :key="product.id" image="/img/makerspace-jk-2025.jpg"
|
||||||
v-for="product in products"
|
:title="product.name" :description="product.description" :price="Number(product.price)"
|
||||||
:key="product.id"
|
:badge="getBadge(product.category)" :discount-percentage="getDiscount(product.category)"
|
||||||
image="/img/makerspace-jk-2025.jpg"
|
:product-id="product.id" />
|
||||||
:title="product.name"
|
|
||||||
:description="product.description"
|
|
||||||
:price="Number(product.price)"
|
|
||||||
:badge="getBadge(product.category)"
|
|
||||||
:discount-percentage="getDiscount(product.category)"
|
|
||||||
:product-id="product.id"
|
|
||||||
/>
|
|
||||||
</ProductGrid>
|
</ProductGrid>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user