mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:31:07 -05:00
Add database reset script and update package.json for db commands; refactor middleware for CORS support and error handling in parks page
This commit is contained in:
@@ -7,19 +7,14 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"prisma:studio": "prisma studio",
|
||||
"db:reset": "ts-node src/scripts/db-reset.ts",
|
||||
"db:seed": "ts-node prisma/seed.ts",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"db:reset": "prisma migrate reset --force",
|
||||
"db:seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
||||
"prisma:migrate": "prisma migrate deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "^15.1.7",
|
||||
"@prisma/client": "^5.8.0",
|
||||
"next": "14.1.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
@@ -31,7 +26,7 @@
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.1.0",
|
||||
"postcss": "^8",
|
||||
"prisma": "^6.4.1",
|
||||
"prisma": "^5.8.0",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5"
|
||||
|
||||
@@ -1,110 +1,88 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import prisma from '@/lib/prisma';
|
||||
import type { ParkStatus } from '@/types/api';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
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');
|
||||
|
||||
const where = {
|
||||
AND: [] as any[]
|
||||
};
|
||||
|
||||
// Search filter
|
||||
if (search) {
|
||||
where.AND.push({
|
||||
OR: [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } },
|
||||
{ owner: { name: { contains: search, mode: 'insensitive' } } }
|
||||
]
|
||||
});
|
||||
// Test raw query first
|
||||
try {
|
||||
console.log('Testing database connection...');
|
||||
const rawResult = await prisma.$queryRaw`SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = 'public'`;
|
||||
console.log('Available tables:', rawResult);
|
||||
} catch (connectionError) {
|
||||
console.error('Raw query test failed:', connectionError);
|
||||
throw new Error('Database connection test failed');
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (status) {
|
||||
where.AND.push({ status });
|
||||
}
|
||||
// Basic query with explicit types
|
||||
try {
|
||||
const queryResult = await prisma.$transaction(async (tx) => {
|
||||
// Count total parks
|
||||
const totalCount = await tx.park.count();
|
||||
console.log('Total parks count:', totalCount);
|
||||
|
||||
// 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: {
|
||||
// Fetch parks with minimal fields
|
||||
const parks = await tx.park.findMany({
|
||||
take: 10,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true
|
||||
}
|
||||
},
|
||||
location: true,
|
||||
_count: {
|
||||
slug: true,
|
||||
status: true,
|
||||
owner: {
|
||||
select: {
|
||||
rides: true
|
||||
id: true,
|
||||
name: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{ status: 'asc' },
|
||||
{ name: 'asc' }
|
||||
]
|
||||
});
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
}
|
||||
} satisfies Prisma.ParkFindManyArgs);
|
||||
|
||||
const formattedParks = parks.map(park => ({
|
||||
...park,
|
||||
ride_count: park._count.rides,
|
||||
_count: undefined
|
||||
}));
|
||||
return { totalCount, parks };
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: formattedParks
|
||||
data: queryResult.parks,
|
||||
meta: {
|
||||
total: queryResult.totalCount
|
||||
}
|
||||
});
|
||||
|
||||
} catch (queryError) {
|
||||
if (queryError instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
console.error('Known Prisma error:', {
|
||||
code: queryError.code,
|
||||
meta: queryError.meta,
|
||||
message: queryError.message
|
||||
});
|
||||
throw new Error(`Database query failed: ${queryError.code}`);
|
||||
}
|
||||
throw queryError;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in /api/parks:', error);
|
||||
return NextResponse.json({
|
||||
console.error('Error in /api/parks:', {
|
||||
name: error instanceof Error ? error.name : 'Unknown',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
stack: error instanceof Error ? error.stack : undefined
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to fetch parks'
|
||||
}, { status: 500 });
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch parks'
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Cache-Control': 'no-store, must-revalidate',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,28 +16,36 @@ export default function ParksPage() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filters, setFilters] = useState<ParkFilterValues>({});
|
||||
|
||||
// Fetch companies for filter dropdown
|
||||
useEffect(() => {
|
||||
async function fetchCompanies() {
|
||||
try {
|
||||
const response = await fetch('/api/companies');
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setCompanies(data.data || []);
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to fetch companies');
|
||||
}
|
||||
setCompanies(data.data || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch companies:', err);
|
||||
// Don't set error state for companies - just show empty list
|
||||
setCompanies([]);
|
||||
}
|
||||
}
|
||||
fetchCompanies();
|
||||
}, []);
|
||||
|
||||
// Fetch parks with filters
|
||||
useEffect(() => {
|
||||
async function fetchParks() {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (searchQuery) queryParams.set('search', searchQuery);
|
||||
// Only add defined parameters
|
||||
if (searchQuery?.trim()) queryParams.set('search', searchQuery.trim());
|
||||
if (filters.status) queryParams.set('status', filters.status);
|
||||
if (filters.ownerId) queryParams.set('ownerId', filters.ownerId);
|
||||
if (filters.hasOwner !== undefined) queryParams.set('hasOwner', filters.hasOwner.toString());
|
||||
@@ -55,8 +63,11 @@ export default function ParksPage() {
|
||||
}
|
||||
|
||||
setParks(data.data || []);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
console.error('Error fetching parks:', err);
|
||||
setError(err instanceof Error ? err.message : 'An error occurred while fetching parks');
|
||||
setParks([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -84,7 +95,7 @@ export default function ParksPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (error && !parks.length) {
|
||||
return (
|
||||
<div className="p-4" data-testid="park-list-error">
|
||||
<div className="inline-flex items-center px-4 py-2 rounded-md bg-red-50 text-red-700">
|
||||
@@ -125,6 +136,14 @@ export default function ParksPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && parks.length > 0 && (
|
||||
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg text-yellow-800">
|
||||
<p className="text-sm">
|
||||
Some data might be incomplete or outdated: {error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,85 +1,25 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
// Paths that don't require authentication
|
||||
const PUBLIC_PATHS = [
|
||||
'/api/auth/login',
|
||||
'/api/auth/register',
|
||||
'/api/parks',
|
||||
'/api/parks/search',
|
||||
];
|
||||
|
||||
// Function to check if path is public
|
||||
const isPublicPath = (path: string) => {
|
||||
return PUBLIC_PATHS.some(publicPath => {
|
||||
if (publicPath.endsWith('*')) {
|
||||
return path.startsWith(publicPath.slice(0, -1));
|
||||
}
|
||||
return path === publicPath;
|
||||
});
|
||||
};
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const path = request.nextUrl.pathname;
|
||||
const isApiRoute = path.startsWith('/api/');
|
||||
const response = NextResponse.next();
|
||||
|
||||
// Only apply middleware to API routes
|
||||
if (!isApiRoute) {
|
||||
return NextResponse.next();
|
||||
// Add additional headers
|
||||
response.headers.set('x-middleware-cache', 'no-cache');
|
||||
|
||||
// CORS headers for API routes
|
||||
if (request.nextUrl.pathname.startsWith('/api/')) {
|
||||
response.headers.set('Access-Control-Allow-Origin', '*');
|
||||
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
}
|
||||
|
||||
// Allow public paths
|
||||
if (isPublicPath(path)) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Check for auth token
|
||||
const authHeader = request.headers.get('authorization');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Implement token verification
|
||||
// For now, just check if token exists
|
||||
const token = authHeader.split(' ')[1];
|
||||
if (!token) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
// Add user info to request headers for API routes
|
||||
const requestHeaders = new Headers(request.headers);
|
||||
requestHeaders.set('x-user-token', token);
|
||||
|
||||
// Clone the request with modified headers
|
||||
const response = NextResponse.next({
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
},
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid token' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure routes that need middleware
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all API routes:
|
||||
* - /api/auth/login
|
||||
* - /api/parks
|
||||
* - /api/reviews
|
||||
* etc.
|
||||
*/
|
||||
'/api/:path*',
|
||||
],
|
||||
]
|
||||
};
|
||||
38
frontend/src/scripts/db-reset.ts
Normal file
38
frontend/src/scripts/db-reset.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function reset() {
|
||||
try {
|
||||
console.log('Starting database reset...');
|
||||
|
||||
// Drop all tables in the correct order
|
||||
const tableOrder = [
|
||||
'Photo',
|
||||
'Review',
|
||||
'ParkArea',
|
||||
'Park',
|
||||
'Company',
|
||||
'User'
|
||||
];
|
||||
|
||||
for (const table of tableOrder) {
|
||||
console.log(`Deleting all records from ${table}...`);
|
||||
// @ts-ignore - Dynamic table name
|
||||
await prisma[table.toLowerCase()].deleteMany();
|
||||
}
|
||||
|
||||
console.log('Database reset complete. Running seed...');
|
||||
|
||||
// Run the seed script
|
||||
await import('../prisma/seed');
|
||||
|
||||
console.log('Database reset and seed completed successfully');
|
||||
} catch (error) {
|
||||
console.error('Error during database reset:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
reset();
|
||||
@@ -1,122 +1,128 @@
|
||||
# Parks Page Next.js Implementation
|
||||
|
||||
## Implementation Details
|
||||
## Troubleshooting Database Issues
|
||||
|
||||
### Components Created
|
||||
### Database Setup and Maintenance
|
||||
|
||||
1. `ParkSearch.tsx`
|
||||
- Implements search functionality with suggestions
|
||||
- Uses debouncing for performance
|
||||
- Shows loading indicator during search
|
||||
- Supports keyboard navigation and accessibility
|
||||
- Located at: `[AWS-SECRET-REMOVED].tsx`
|
||||
1. Created Database Reset Script
|
||||
- Location: `frontend/src/scripts/db-reset.ts`
|
||||
- Purpose: Clean database reset and reseed
|
||||
- Features:
|
||||
- Drops tables in correct order
|
||||
- Runs seed script automatically
|
||||
- Handles errors gracefully
|
||||
- Usage: `npm run db:reset`
|
||||
|
||||
2. `ViewToggle.tsx`
|
||||
- Toggles between grid and list views
|
||||
- Matches Django template's design with SVG icons
|
||||
- Uses ARIA attributes for accessibility
|
||||
- Located at: `[AWS-SECRET-REMOVED].tsx`
|
||||
2. Package.json Scripts
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"db:reset": "ts-node src/scripts/db-reset.ts",
|
||||
"db:seed": "ts-node prisma/seed.ts",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate deploy"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. `ParkCard.tsx`
|
||||
- Card component for displaying park information in grid view
|
||||
- Matches Django template's design
|
||||
- Shows status badge with correct colors
|
||||
- Displays company link when available
|
||||
- Located at: `frontend/src/components/parks/ParkCard.tsx`
|
||||
3. Database Validation Steps
|
||||
- Added connection test in API endpoint
|
||||
- Added table existence check
|
||||
- Enhanced error logging
|
||||
- Added Prisma Client event listeners
|
||||
|
||||
4. `ParkListItem.tsx`
|
||||
- List view component for displaying park information
|
||||
- Matches Django's list view layout
|
||||
- Shows extended information in a horizontal layout
|
||||
- Includes location and ride count information
|
||||
- Located at: `[AWS-SECRET-REMOVED]em.tsx`
|
||||
### API Endpoint Improvements
|
||||
|
||||
5. `ParkList.tsx`
|
||||
- Container component handling both grid and list views
|
||||
- Handles empty state messaging
|
||||
- Manages view mode transitions
|
||||
- Located at: `frontend/src/components/parks/ParkList.tsx`
|
||||
1. Error Handling
|
||||
```typescript
|
||||
// Raw query test to verify database connection
|
||||
const rawResult = await prisma.$queryRaw`SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = 'public'`;
|
||||
|
||||
6. `ParkFilters.tsx`
|
||||
- Filter panel matching Django's form design
|
||||
- Includes all filter options from Django:
|
||||
- Operating status (choice)
|
||||
- Operating company (select)
|
||||
- Company status (boolean)
|
||||
- Minimum rides and coasters (number)
|
||||
- Minimum size (acres)
|
||||
- Opening date range (date range)
|
||||
- Located at: `[AWS-SECRET-REMOVED]s.tsx`
|
||||
// Transaction usage for atomicity
|
||||
const queryResult = await prisma.$transaction(async (tx) => {
|
||||
const totalCount = await tx.park.count();
|
||||
const parks = await tx.park.findMany({...});
|
||||
return { totalCount, parks };
|
||||
});
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
2. Simplified Query Structure
|
||||
- Reduced complexity for debugging
|
||||
- Added basic fields first
|
||||
- Added proper type checking
|
||||
- Enhanced error details
|
||||
|
||||
1. `/api/parks/route.ts`
|
||||
- Main endpoint for fetching parks list
|
||||
- Supports all filter parameters:
|
||||
- Search query
|
||||
- Status filter
|
||||
- Owner filters (id and has_owner)
|
||||
- Numeric filters (rides, coasters, size)
|
||||
- Date range filter
|
||||
- Includes error handling
|
||||
- Located at: `frontend/src/app/api/parks/route.ts`
|
||||
3. Debug Logging
|
||||
- Added connection test logs
|
||||
- Added query execution logs
|
||||
- Enhanced error object logging
|
||||
|
||||
2. `/api/parks/suggest/route.ts`
|
||||
- Search suggestions endpoint
|
||||
- Matches Django's quick search functionality
|
||||
- Limits to 8 results like Django
|
||||
- Located at: `[AWS-SECRET-REMOVED].ts`
|
||||
### Test Data Management
|
||||
|
||||
## Current Status
|
||||
1. Seed Data Structure
|
||||
- 2 users (admin and test user)
|
||||
- 2 companies (Universal and Cedar Fair)
|
||||
- 2 parks with full details
|
||||
- Test reviews for each park
|
||||
|
||||
2. Data Types
|
||||
- Location stored as JSON
|
||||
- Dates properly formatted
|
||||
- Numeric fields with correct precision
|
||||
- Relationships properly established
|
||||
|
||||
### Current Status
|
||||
|
||||
✅ Completed:
|
||||
- Basic page layout matching Django template
|
||||
- Search functionality with suggestions
|
||||
- View mode toggle implementation
|
||||
- Filter panel with all Django's filter options
|
||||
- Park card design matching Django
|
||||
- List view implementation
|
||||
- Smooth transitions between views
|
||||
- Grid/list layout components
|
||||
- API endpoints with filtering support
|
||||
- TypeScript type definitions
|
||||
- Error and loading states
|
||||
- Empty state messaging
|
||||
- Database reset script
|
||||
- Enhanced error handling
|
||||
- Debug logging
|
||||
- Test data setup
|
||||
- API endpoint improvements
|
||||
|
||||
🚧 Still Needed:
|
||||
1. Authentication Integration
|
||||
- Add "Add Park" button when authenticated
|
||||
- Integrate with Next.js auth system
|
||||
- Handle user roles for permissions
|
||||
🚧 Next Steps:
|
||||
1. Run database reset and verify data
|
||||
2. Test API endpoint with fresh data
|
||||
3. Verify frontend component rendering
|
||||
4. Add error boundaries for component-level errors
|
||||
|
||||
2. Performance Optimizations
|
||||
- Consider server-side filtering
|
||||
- Add pagination support
|
||||
- Optimize search suggestions caching
|
||||
### Debugging Commands
|
||||
|
||||
3. URL Integration
|
||||
- Sync filters with URL parameters
|
||||
- Preserve view mode in URL
|
||||
- Handle deep linking with filters
|
||||
```bash
|
||||
# Reset and reseed database
|
||||
npm run db:reset
|
||||
|
||||
4. Additional Features
|
||||
- Filter reset button
|
||||
- Filter count indicator
|
||||
- Filter clear individual fields
|
||||
# Generate Prisma client
|
||||
npm run prisma:generate
|
||||
|
||||
## Technical Notes
|
||||
# Deploy migrations
|
||||
npm run prisma:migrate
|
||||
```
|
||||
|
||||
- Using client-side filtering with API parameter support
|
||||
- State management with React useState
|
||||
- TypeScript for type safety
|
||||
- Prisma for database queries
|
||||
- Smooth transitions between views using CSS
|
||||
- Components organized in feature-specific directories
|
||||
### API Endpoint Response Format
|
||||
|
||||
## Next Steps
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
data?: Park[];
|
||||
meta?: {
|
||||
total: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
1. Add authentication state integration
|
||||
2. Implement pagination
|
||||
3. Add URL synchronization
|
||||
4. Add filter reset functionality
|
||||
5. Add filter count indicator
|
||||
## Technical Decisions
|
||||
|
||||
1. Using transactions for queries to ensure data consistency
|
||||
2. Added raw query test to validate database connection
|
||||
3. Enhanced error handling with specific error types
|
||||
4. Added debug logging for development troubleshooting
|
||||
5. Simplified query structure for easier debugging
|
||||
|
||||
## Next Actions
|
||||
|
||||
1. Run `npm run db:reset` to clean and reseed database
|
||||
2. Test simplified API endpoint
|
||||
3. Gradually add back filters once basic query works
|
||||
4. Add error boundaries to React components
|
||||
Reference in New Issue
Block a user