- Added Checkout and Payment pages to handle user authentication and order processing. - Introduced MockPayPalButton for simulating payment during development. - Updated CartSheet and CartSidebar components to navigate to the new checkout page. - Enhanced Cart UI with responsive design for mobile and desktop views. - Implemented order confirmation and success pages to provide feedback after payment completion. These changes complete the checkout and payment functionality, improving the overall user experience and ensuring a seamless transition from cart to order confirmation.
236 lines
8.3 KiB
Vue
236 lines
8.3 KiB
Vue
<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">
|
||
<NuxtLink to="/products" class="btn-secondary flex-1 text-center">
|
||
Weitere Produkte ansehen
|
||
</NuxtLink>
|
||
<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>
|
||
|
||
</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>
|