mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-29 01:47:00 -05:00
Add park detail API and detail page implementation with loading states and error handling
This commit is contained in:
@@ -1,217 +1,110 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
import { ParkListResponse, Park } from '@/types/api';
|
||||
import type { ParkStatus } from '@/types/api';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest
|
||||
): Promise<NextResponse<ParkListResponse>> {
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
// Get query parameters
|
||||
const { searchParams } = new URL(request.url);
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
const limit = parseInt(searchParams.get('limit') || '10');
|
||||
const search = searchParams.get('search') || '';
|
||||
const status = searchParams.get('status') || undefined;
|
||||
const search = searchParams.get('search')?.trim();
|
||||
const status = searchParams.get('status') as ParkStatus;
|
||||
const ownerId = searchParams.get('ownerId');
|
||||
const hasOwner = searchParams.get('hasOwner');
|
||||
const minRides = parseInt(searchParams.get('minRides') || '');
|
||||
const minCoasters = parseInt(searchParams.get('minCoasters') || '');
|
||||
const minSize = parseInt(searchParams.get('minSize') || '');
|
||||
const openingDateStart = searchParams.get('openingDateStart');
|
||||
const openingDateEnd = searchParams.get('openingDateEnd');
|
||||
|
||||
// Calculate pagination
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build where clause
|
||||
const where = {
|
||||
AND: [
|
||||
search ? {
|
||||
OR: [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } },
|
||||
],
|
||||
} : {},
|
||||
status ? { status } : {},
|
||||
],
|
||||
AND: [] as any[]
|
||||
};
|
||||
|
||||
// Ensure database connection is initialized
|
||||
if (!prisma) {
|
||||
throw new Error('Database connection not initialized');
|
||||
// Search filter
|
||||
if (search) {
|
||||
where.AND.push({
|
||||
OR: [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } },
|
||||
{ owner: { name: { contains: search, mode: 'insensitive' } } }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch parks with relationships
|
||||
const [parks, total] = await Promise.all([
|
||||
prisma.park.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
include: {
|
||||
creator: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
owner: true,
|
||||
areas: true,
|
||||
reviews: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
photos: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.park.count({ where }),
|
||||
]);
|
||||
// Status filter
|
||||
if (status) {
|
||||
where.AND.push({ status });
|
||||
}
|
||||
|
||||
// Owner filters
|
||||
if (ownerId) {
|
||||
where.AND.push({ ownerId });
|
||||
}
|
||||
if (hasOwner !== null) {
|
||||
where.AND.push({ owner: hasOwner === 'true' ? { not: null } : null });
|
||||
}
|
||||
|
||||
// Numeric filters
|
||||
if (!isNaN(minRides)) {
|
||||
where.AND.push({ ride_count: { gte: minRides } });
|
||||
}
|
||||
if (!isNaN(minCoasters)) {
|
||||
where.AND.push({ coaster_count: { gte: minCoasters } });
|
||||
}
|
||||
if (!isNaN(minSize)) {
|
||||
where.AND.push({ size_acres: { gte: minSize } });
|
||||
}
|
||||
|
||||
// Date range filter
|
||||
if (openingDateStart || openingDateEnd) {
|
||||
const dateFilter: any = {};
|
||||
if (openingDateStart) {
|
||||
dateFilter.gte = new Date(openingDateStart);
|
||||
}
|
||||
if (openingDateEnd) {
|
||||
dateFilter.lte = new Date(openingDateEnd);
|
||||
}
|
||||
where.AND.push({ opening_date: dateFilter });
|
||||
}
|
||||
|
||||
const parks = await prisma.park.findMany({
|
||||
where: where.AND.length > 0 ? where : undefined,
|
||||
include: {
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true
|
||||
}
|
||||
},
|
||||
location: true,
|
||||
_count: {
|
||||
select: {
|
||||
rides: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{ status: 'asc' },
|
||||
{ name: 'asc' }
|
||||
]
|
||||
});
|
||||
|
||||
// Transform dates and format response
|
||||
const formattedParks = parks.map(park => ({
|
||||
...park,
|
||||
opening_date: park.opening_date?.toISOString().split('T')[0],
|
||||
closing_date: park.closing_date?.toISOString().split('T')[0],
|
||||
created_at: park.created_at.toISOString(),
|
||||
updated_at: park.updated_at.toISOString(),
|
||||
// Format nested dates
|
||||
areas: park.areas.map(area => ({
|
||||
...area,
|
||||
opening_date: area.opening_date?.toISOString().split('T')[0],
|
||||
closing_date: area.closing_date?.toISOString().split('T')[0],
|
||||
created_at: area.created_at.toISOString(),
|
||||
updated_at: area.updated_at.toISOString(),
|
||||
})),
|
||||
reviews: park.reviews.map(review => ({
|
||||
...review,
|
||||
created_at: review.created_at.toISOString(),
|
||||
updated_at: review.updated_at.toISOString(),
|
||||
})),
|
||||
photos: park.photos.map(photo => ({
|
||||
...photo,
|
||||
created_at: photo.created_at.toISOString(),
|
||||
updated_at: photo.updated_at.toISOString(),
|
||||
})),
|
||||
ride_count: park._count.rides,
|
||||
_count: undefined
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: formattedParks,
|
||||
metadata: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
},
|
||||
data: formattedParks
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching parks:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch parks',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest
|
||||
): Promise<NextResponse<ParkListResponse>> {
|
||||
try {
|
||||
// Ensure user is authenticated
|
||||
const userToken = request.headers.get('x-user-token');
|
||||
if (!userToken) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
|
||||
// Validate required fields
|
||||
if (!data.name) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Name is required',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Generate slug from name
|
||||
const slug = data.name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
|
||||
// Ensure database connection is initialized
|
||||
if (!prisma) {
|
||||
throw new Error('Database connection not initialized');
|
||||
}
|
||||
|
||||
// Create new park
|
||||
const park = await prisma.park.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
slug,
|
||||
description: data.description,
|
||||
status: data.status || 'OPERATING',
|
||||
location: data.location,
|
||||
opening_date: data.opening_date ? new Date(data.opening_date) : null,
|
||||
closing_date: data.closing_date ? new Date(data.closing_date) : null,
|
||||
operating_season: data.operating_season,
|
||||
size_acres: data.size_acres,
|
||||
website: data.website,
|
||||
creatorId: parseInt(data.creatorId),
|
||||
ownerId: data.ownerId ? parseInt(data.ownerId) : null,
|
||||
},
|
||||
include: {
|
||||
creator: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
owner: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.error('Error in /api/parks:', error);
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
...park,
|
||||
opening_date: park.opening_date?.toISOString().split('T')[0],
|
||||
closing_date: park.closing_date?.toISOString().split('T')[0],
|
||||
created_at: park.created_at.toISOString(),
|
||||
updated_at: park.updated_at.toISOString(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating park:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create park',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
success: false,
|
||||
error: 'Failed to fetch parks'
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user