Add product detail and listing pages with API integration
- Created a new product detail page to display individual product information, including images, descriptions, and pricing. - Implemented a product listing page to showcase all available products using the ProductCard and ProductGrid components. - Added API endpoints for fetching product data, ensuring only active products are returned. - Introduced a database seed script to populate the database with initial mock product data for development and testing. - Updated settings to include new database seeding command and adjusted routing for product links.
This commit is contained in:
195
app/pages/products/[id].vue
Normal file
195
app/pages/products/[id].vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Product Detail Page
|
||||
*
|
||||
* Displays full details for a single product with large image and description.
|
||||
* Includes placeholder "Add to Cart" functionality for future implementation.
|
||||
*/
|
||||
|
||||
import { ArrowLeft, CheckCircle } from 'lucide-vue-next'
|
||||
|
||||
// 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))
|
||||
})
|
||||
|
||||
// Handle "Add to Cart" action (placeholder for future implementation)
|
||||
const handleAddToCart = () => {
|
||||
// TODO: Implement cart functionality in future phase
|
||||
alert('Add to Cart funktioniert noch nicht. Diese Funktion wird in einer späteren Phase implementiert.')
|
||||
}
|
||||
</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"
|
||||
:disabled="product.stockQuantity === 0"
|
||||
@click="handleAddToCart"
|
||||
>
|
||||
{{ product.stockQuantity > 0 ? 'In den Warenkorb' : 'Nicht verfügbar' }}
|
||||
</Button>
|
||||
<NuxtLink to="/products" class="flex-1">
|
||||
<Button variant="outline" size="experimenta" class="w-full">
|
||||
Weitere Produkte ansehen
|
||||
</Button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
Reference in New Issue
Block a user