Add cart button to desktop header with price display
Implemented desktop cart button in navigation header that displays: - Shopping cart icon with item count badge (red, top-right) - Total cart price in German locale (EUR) - Click opens CartSidebar via useCartUI() composable - Responsive: visible only on lg breakpoint and above - Hidden on mobile (FAB is used instead) Uses useCart() composable for itemCount and total, with proper Intl.NumberFormat formatting for EUR display. Also standardized CartFAB price formatting to use Intl.NumberFormat for consistency with rest of codebase. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
75
app/components/Cart/CartFAB.vue
Normal file
75
app/components/Cart/CartFAB.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { ShoppingCart } from 'lucide-vue-next'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
// Get cart data
|
||||
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
|
||||
})
|
||||
|
||||
// Format price as EUR in German locale
|
||||
const formattedTotal = computed(() => {
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
}).format(total.value)
|
||||
})
|
||||
|
||||
// Handle FAB click
|
||||
function handleClick() {
|
||||
open()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
>
|
||||
<div v-if="isVisible" class="fixed bottom-20 right-4 z-40">
|
||||
<!-- FAB Button -->
|
||||
<Button
|
||||
@click="handleClick"
|
||||
class="h-14 w-14 rounded-full shadow-lg transition-all duration-200 hover:scale-110 active:scale-95"
|
||||
variant="default"
|
||||
size="icon"
|
||||
aria-label="Warenkorb öffnen"
|
||||
>
|
||||
<!-- Cart icon with relative positioning for badge -->
|
||||
<div class="relative">
|
||||
<ShoppingCart class="h-6 w-6" />
|
||||
|
||||
<!-- Item count badge -->
|
||||
<Badge
|
||||
class="absolute -top-2 -right-2 h-5 min-w-[20px] px-0.5 flex items-center justify-center bg-experimenta-accent text-white text-xs font-bold border border-white dark:border-zinc-950"
|
||||
>
|
||||
{{ itemCount > 99 ? '99+' : itemCount }}
|
||||
</Badge>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<!-- Price text below button -->
|
||||
<div class="mt-2 text-center">
|
||||
<p class="text-xs font-semibold text-foreground whitespace-nowrap">
|
||||
{{ formattedTotal }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
@@ -2,42 +2,65 @@
|
||||
import { ShoppingCart } from 'lucide-vue-next'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
// TODO: This will come from cart store/composable in Phase 5
|
||||
const cartItemCount = ref(0)
|
||||
const { itemCount, total } = useCart()
|
||||
const { open } = useCartUI()
|
||||
|
||||
const hasItems = computed(() => cartItemCount.value > 0)
|
||||
const hasItems = computed(() => itemCount.value > 0)
|
||||
|
||||
// Format total price in German locale (EUR)
|
||||
const formattedTotal = computed(() => {
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
}).format(total.value)
|
||||
})
|
||||
|
||||
// Handle button click to open cart sidebar
|
||||
function handleClick(e: Event) {
|
||||
e.preventDefault()
|
||||
open()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink
|
||||
to="/cart"
|
||||
class="relative inline-flex items-center justify-center rounded-full p-3 transition-all hover:bg-experimenta-accent/10 focus:outline-none focus:ring-2 focus:ring-experimenta-accent focus:ring-offset-2"
|
||||
aria-label="Warenkorb"
|
||||
<!-- Desktop cart button (visible only on lg and up) -->
|
||||
<button
|
||||
@click="handleClick"
|
||||
class="hidden lg:flex items-center gap-2 rounded-lg px-4 py-2 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"
|
||||
>
|
||||
<ShoppingCart class="h-6 w-6 text-foreground" />
|
||||
<!-- Cart icon with item count badge -->
|
||||
<div class="relative inline-flex">
|
||||
<ShoppingCart class="h-6 w-6 text-white" />
|
||||
|
||||
<!-- 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 min-w-[20px] px-1 flex items-center justify-center bg-experimenta-accent text-white text-xs font-bold border-2 border-white dark:border-zinc-950"
|
||||
<!-- 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"
|
||||
>
|
||||
{{ cartItemCount > 99 ? '99+' : cartItemCount }}
|
||||
</Badge>
|
||||
</Transition>
|
||||
<Badge
|
||||
v-if="hasItems"
|
||||
class="absolute -top-2 -right-2 h-5 min-w-[20px] px-1 flex items-center justify-center bg-red-500 text-white text-xs font-bold border-2 border-white"
|
||||
>
|
||||
{{ itemCount > 99 ? '99+' : itemCount }}
|
||||
</Badge>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<!-- Pulse animation when items are added (optional) -->
|
||||
<!-- Total price (desktop only) -->
|
||||
<span class="text-sm font-semibold text-white">
|
||||
{{ formattedTotal }}
|
||||
</span>
|
||||
|
||||
<!-- Pulse animation when items are added -->
|
||||
<span
|
||||
v-if="hasItems"
|
||||
class="absolute inset-0 rounded-full bg-experimenta-accent/20 animate-ping opacity-75"
|
||||
class="absolute inset-0 rounded-lg bg-experimenta-accent/20 animate-pulse opacity-50"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NuxtLink>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user