Files
my2/app/pages/products/[id].vue
Bastian Masanek b372e2cf78 Implement shopping cart functionality with UI components and API integration
- Added CartItem, CartSummary, CartEmpty, CartSidebar, and CartSheet components for managing cart display and interactions.
- Integrated useCart and useCartUI composables for cart state management and UI control.
- Implemented API endpoints for cart operations, including fetching, adding, updating, and removing items.
- Enhanced user experience with loading states and notifications using vue-sonner for cart actions.
- Configured session management for guest and authenticated users, ensuring cart persistence across sessions.

This commit completes the shopping cart feature, enabling users to add items, view their cart, and proceed to checkout.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-11-03 12:43:13 +01:00

258 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* Product Detail Page
*
* Displays full details for a single product with large image and description.
* Includes functional "Add to Cart" functionality with notifications and cart UI integration.
*/
import { ArrowLeft, CheckCircle, Loader2 } from 'lucide-vue-next'
import { toast } from 'vue-sonner'
// Page metadata
definePageMeta({
layout: 'default',
})
// Get product ID from route
const route = useRoute()
const productId = route.params.id as string
// Type definition for product
interface Product {
id: string
navProductId: string
name: string
description: string
price: string
stockQuantity: number
category: string
active: boolean
createdAt: Date
updatedAt: Date
}
// Fetch product from API
const { data: product, error, pending } = await useFetch<Product>(`/api/products/${productId}`)
// Format price in EUR
const formattedPrice = computed(() => {
if (!product.value) return ''
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
}).format(Number(product.value.price))
})
// Cart composables
const { addItem, loading: cartLoading, items } = useCart()
const { open: openCart } = useCartUI()
// Local loading state for this specific button
const isAddingToCart = ref(false)
// Check if product is already in cart
const isInCart = computed(() => {
if (!product.value) return false
return items.value.some((item) => item.productId === product.value.id)
})
// Handle "Add to Cart" action
const handleAddToCart = async () => {
if (!product.value) return
// Prevent adding if out of stock
if (product.value.stockQuantity === 0) {
toast.error('Nicht verfügbar', {
description: 'Dieses Produkt ist derzeit nicht auf Lager.',
})
return
}
isAddingToCart.value = true
try {
// Add item to cart (quantity: 1)
await addItem(product.value.id, 1)
// Show success notification
toast.success('In den Warenkorb gelegt', {
description: `${product.value.name} wurde zu deinem Warenkorb hinzugefügt.`,
})
// Open cart sidebar/sheet to show added item
openCart()
} catch (error) {
console.error('Failed to add item to cart:', error)
// Show error notification
toast.error('Fehler beim Hinzufügen', {
description: 'Das Produkt konnte nicht in den Warenkorb gelegt werden. Bitte versuche es erneut.',
})
} finally {
isAddingToCart.value = false
}
}
</script>
<template>
<NuxtLayout name="default">
<div class="min-h-screen bg-gradient-primary px-4 py-12 md:px-6 lg:px-8">
<!-- Back Button -->
<div class="mx-auto mb-8 max-w-container-narrow">
<NuxtLink
to="/products"
class="inline-flex items-center gap-2 text-white/80 transition-colors hover:text-white"
>
<ArrowLeft :size="20" />
<span>Zurück zur Übersicht</span>
</NuxtLink>
</div>
<!-- Loading State -->
<div v-if="pending" class="mx-auto max-w-container-narrow text-center">
<div class="card-glass inline-block">
<p class="text-white/80">Produkt wird geladen...</p>
</div>
</div>
<!-- Error State (404 or other errors) -->
<div v-else-if="error" class="mx-auto max-w-container-narrow">
<div class="card-glass border-red/50 bg-red/10">
<h2 class="mb-2 text-2xl font-semibold text-red">
{{ error.statusCode === 404 ? 'Produkt nicht gefunden' : 'Fehler beim Laden' }}
</h2>
<p class="mb-6 text-white/80">
{{
error.statusCode === 404
? 'Das angeforderte Produkt existiert nicht oder ist nicht verfügbar.'
: error.message || 'Ein unbekannter Fehler ist aufgetreten.'
}}
</p>
<NuxtLink to="/products">
<Button variant="experimenta" size="experimenta">
Zur Produktübersicht
</Button>
</NuxtLink>
</div>
</div>
<!-- Product Detail -->
<div v-else-if="product" class="mx-auto max-w-container-narrow">
<div class="overflow-hidden rounded-2xl border border-white/20 bg-white/10 shadow-glass backdrop-blur-lg">
<!-- Product Image (no padding, flush with top) -->
<div class="relative aspect-[16/9] w-full overflow-hidden bg-purple-dark">
<img
src="/img/makerspace-jk-2025.jpg"
:alt="product.name"
class="h-full w-full object-cover"
/>
<!-- Gradient overlay -->
<div
class="absolute inset-0 bg-gradient-to-t from-purple-darkest/80 via-transparent to-transparent"
/>
</div>
<!-- Product Content -->
<div class="space-y-6 p-8">
<!-- Title -->
<h1 class="text-3xl font-bold text-white md:text-4xl">
{{ product.name }}
</h1>
<!-- Description -->
<p class="text-lg leading-relaxed text-white/90">
{{ product.description }}
</p>
<!-- Product Details -->
<div class="grid gap-4 sm:grid-cols-2">
<!-- Price Card -->
<div class="rounded-xl bg-white/5 p-4 backdrop-blur-sm">
<span class="mb-1 block text-xs uppercase tracking-wide text-white/60">Preis</span>
<span class="text-3xl font-bold text-experimenta-accent">
{{ formattedPrice }}
</span>
</div>
<!-- Availability Card -->
<div class="rounded-xl bg-white/5 p-4 backdrop-blur-sm">
<span class="mb-1 block text-xs uppercase tracking-wide text-white/60">Verfügbarkeit</span>
<div
:class="[
'flex items-center gap-2 text-xl font-semibold',
product.stockQuantity > 0 ? 'text-green' : 'text-red',
]"
>
<CheckCircle v-if="product.stockQuantity > 0" :size="24" />
<span>{{ product.stockQuantity > 0 ? 'Sofort' : 'Nicht verfügbar' }}</span>
</div>
</div>
</div>
<!-- Features / Benefits -->
<div class="rounded-xl border border-white/20 bg-white/5 p-6 backdrop-blur-sm">
<h2 class="mb-4 text-xl font-semibold text-white">
Was du mit dieser Karte bekommst:
</h2>
<ul class="space-y-3 text-white/90">
<li class="flex items-start gap-2">
<span class="mt-1 text-experimenta-accent"></span>
<span>365 Tage unbegrenzter Zugang</span>
</li>
<li class="flex items-start gap-2">
<span class="mt-1 text-experimenta-accent"></span>
<span>Keine versteckten Kosten</span>
</li>
<li class="flex items-start gap-2">
<span class="mt-1 text-experimenta-accent"></span>
<span>Sofort einsatzbereit nach Kauf</span>
</li>
<li class="flex items-start gap-2">
<span class="mt-1 text-experimenta-accent"></span>
<span>Flexible Nutzung komme so oft du möchtest</span>
</li>
</ul>
</div>
<!-- Action Buttons -->
<div class="flex flex-col gap-4 sm:flex-row">
<Button
variant="experimenta"
size="experimenta"
class="flex-1 relative"
:disabled="product.stockQuantity === 0 || isAddingToCart"
@click="handleAddToCart"
>
<!-- Loading spinner -->
<Loader2
v-if="isAddingToCart"
:size="20"
class="mr-2 animate-spin"
/>
<!-- Button text -->
<span v-if="isAddingToCart">Wird hinzugefügt...</span>
<span v-else-if="product.stockQuantity === 0">Nicht verfügbar</span>
<span v-else>In den Warenkorb</span>
</Button>
<NuxtLink to="/products" class="btn-secondary flex-1 text-center">
Weitere Produkte ansehen
</NuxtLink>
</div>
<!-- Already in cart hint -->
<div
v-if="isInCart && product.stockQuantity > 0"
class="mt-2 text-center text-sm text-white/70"
>
Dieses Produkt befindet sich bereits in deinem Warenkorb.
</div>
</div>
</div>
</div>
</div>
</NuxtLayout>
</template>