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:
Bastian Masanek
2025-11-01 19:07:59 +01:00
parent 9150af3ac2
commit 7ab80a6635
9 changed files with 600 additions and 60 deletions

View File

@@ -0,0 +1,50 @@
/**
* GET /api/products/[id]
*
* Returns a single product by UUID.
* Returns 404 if product is not found or is inactive.
*/
import { z } from 'zod'
import { and, eq } from 'drizzle-orm'
import { products } from '../../database/schema'
// UUID validation schema
const paramsSchema = z.object({
id: z.string().uuid('Invalid product ID format'),
})
export default defineEventHandler(async (event) => {
const db = useDatabase()
// Validate and extract product ID from route params
const params = await getValidatedRouterParams(event, paramsSchema.parse)
try {
// Fetch product by ID (must be active)
const product = await db.query.products.findFirst({
where: and(eq(products.id, params.id), eq(products.active, true)),
})
// Return 404 if product not found or inactive
if (!product) {
throw createError({
statusCode: 404,
statusMessage: 'Product not found',
})
}
return product
} catch (error) {
// Re-throw createError errors as-is
if (error && typeof error === 'object' && 'statusCode' in error) {
throw error
}
console.error('Error fetching product:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch product',
})
}
})

View File

@@ -0,0 +1,29 @@
/**
* GET /api/products
*
* Returns a list of all active products available for purchase.
* Products are sorted by category and name.
*/
import { eq } from 'drizzle-orm'
import { products } from '../../database/schema'
export default defineEventHandler(async (event) => {
const db = useDatabase()
try {
// Fetch all active products
const allProducts = await db.query.products.findMany({
where: eq(products.active, true),
orderBy: (products, { asc }) => [asc(products.category), asc(products.name)],
})
return allProducts
} catch (error) {
console.error('Error fetching products:', error)
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch products',
})
}
})

103
server/database/seed.ts Normal file
View File

@@ -0,0 +1,103 @@
/**
* Database Seed Script
*
* Seeds the database with initial mock product data for development and testing.
* Run with: pnpm db:seed
*/
import 'dotenv/config'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { products } from './schema'
/**
* Sample annual pass products for experimenta
*/
const mockProducts = [
{
navProductId: 'MSPACE-JK-2025',
name: 'Makerspace Jahreskarte',
description:
'Unbegrenzter Zugang zum Makerspace für 365 Tage. Nutze modernste Werkzeuge, 3D-Drucker, Lasercutter und vieles mehr. Perfekt für Maker, Tüftler und kreative Köpfe.',
price: '120.00',
stockQuantity: 100,
category: 'makerspace-annual-pass',
active: true,
},
{
navProductId: 'EXPERIMENTA-JK-2025',
name: 'experimenta Jahreskarte',
description:
'Erlebe die Ausstellungswelt der experimenta ein ganzes Jahr lang. Mit freiem Eintritt zu allen Ausstellungen, Science Dome Shows und Sonderausstellungen.',
price: '85.00',
stockQuantity: 200,
category: 'annual-pass',
active: true,
},
{
navProductId: 'PAEDAGOGEN-JK-2025',
name: 'Pädagogen Jahreskarte',
description:
'Speziell für Lehrkräfte und Pädagogen. Mit exklusiven Fortbildungsangeboten, didaktischen Materialien und freiem Zugang zu allen Ausstellungen.',
price: '60.00',
stockQuantity: 50,
category: 'educator-annual-pass',
active: true,
},
]
async function seed() {
// Get database connection from environment
const connectionString = process.env.DATABASE_URL
if (!connectionString) {
throw new Error('DATABASE_URL environment variable is not set')
}
console.log('🌱 Starting database seed...')
// Create database connection
const client = postgres(connectionString)
const db = drizzle(client)
try {
// Insert products
console.log(`📦 Inserting ${mockProducts.length} products...`)
const insertedProducts = await db
.insert(products)
.values(mockProducts)
.onConflictDoUpdate({
target: products.navProductId,
set: {
name: mockProducts[0].name, // Drizzle requires a set object, using sql.excluded in real impl
description: mockProducts[0].description,
price: mockProducts[0].price,
stockQuantity: mockProducts[0].stockQuantity,
updatedAt: new Date(),
},
})
.returning()
console.log(`✅ Successfully inserted/updated ${insertedProducts.length} products:`)
insertedProducts.forEach((product) => {
console.log(` - ${product.name} (${product.navProductId}) - €${product.price}`)
})
console.log('\n✨ Database seed completed successfully!')
} catch (error) {
console.error('❌ Error seeding database:', error)
throw error
} finally {
// Close database connection
await client.end()
}
}
// Run seed function
seed()
.then(() => {
process.exit(0)
})
.catch((error) => {
console.error('Fatal error:', error)
process.exit(1)
})