Files
my2/app/pages/products/[id].vue
Bastian Masanek 04f2e29403 Refactor Layout and Styling in UserMenu, AppHeader, and Product Pages
- Adjusted button margin in UserMenu.vue for better alignment.
- Updated container classes in AppHeader.vue to use max-width for improved responsiveness.
- Modified max-width settings in product detail page to enhance layout consistency across different states (loading, error, product display).
2025-11-25 08:53:39 +01:00

257 lines
8.9 KiB
Vue
Raw Permalink 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, ArrowLeftIcon, CheckCircle, ChevronLeftIcon, Loader2, ShoppingCartIcon } 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}`, {
key: `product-${productId}`, // Explicit key for role-based refresh
})
// Auto-redirect to products list if 404 (e.g., after role switch)
watch(error, (newError) => {
if (newError?.statusCode === 404) {
// Show notification
toast.error('Produkt nicht verfügbar', {
description: 'Dieses Produkt ist für deine aktuelle Rolle nicht sichtbar.',
})
// Redirect after short delay
setTimeout(() => {
navigateTo('/products')
}, 1500)
}
})
// 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 px-4 py-12 md:px-6 lg:px-8">
<!-- Back Button -->
<div class="mx-auto max-w-container-wide">
<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-4xl 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-4xl">
<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 mt-12 max-w-container-wide">
<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">
<ArrowLeftIcon class="w-5 h-5 mr-2 inline-block" />
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>
<ShoppingCartIcon class="mr-3 inline-flex" />
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>