Initialize frontend project with Next.js, Tailwind CSS, and essential configurations

This commit is contained in:
pacnpal
2025-02-23 16:01:56 -05:00
parent 8404e4d3d5
commit 5bcbcae2cd
31 changed files with 8882 additions and 56 deletions

View File

@@ -0,0 +1,146 @@
'use client';
import { useEffect, useState } from 'react';
import type { Park, ParkStatus } from '@/types/api';
export default function ParksPage() {
const [parks, setParks] = useState<Park[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchParks() {
try {
const response = await fetch('/api/parks');
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Failed to fetch parks');
}
setParks(data.data || []);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
}
fetchParks();
}, []);
const getStatusColor = (status: ParkStatus): string => {
const colors = {
OPERATING: 'bg-green-100 text-green-800',
CLOSED_TEMP: 'bg-yellow-100 text-yellow-800',
CLOSED_PERM: 'bg-red-100 text-red-800',
UNDER_CONSTRUCTION: 'bg-blue-100 text-blue-800',
DEMOLISHED: 'bg-gray-100 text-gray-800',
RELOCATED: 'bg-purple-100 text-purple-800'
};
return colors[status] || 'bg-gray-100 text-gray-500';
};
const formatRating = (rating: number | null | undefined): string => {
if (typeof rating !== 'number') return 'No ratings';
return `${rating.toFixed(1)}/5`;
};
if (loading) {
return (
<div className="min-h-screen p-8">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]" />
<p className="mt-4 text-gray-600">Loading parks...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen p-8">
<div className="rounded-lg bg-red-50 p-4 border border-red-200">
<h2 className="text-red-800 font-semibold">Error</h2>
<p className="text-red-600 mt-2">{error}</p>
</div>
</div>
);
}
return (
<div className="min-h-screen p-8">
<div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-bold mb-8">Theme Parks</h1>
{parks.length === 0 ? (
<p className="text-gray-600">No parks found</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{parks.map((park) => (
<div
key={park.id}
className="rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow"
>
<div className="flex justify-between items-start mb-4">
<h2 className="text-xl font-semibold">
<a
href={`/parks/${park.slug}`}
className="hover:text-indigo-600 transition-colors"
>
{park.name}
</a>
</h2>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(park.status)}`}>
{park.status.replace('_', ' ')}
</span>
</div>
{park.description && (
<p className="text-gray-600 mb-4">
{park.description.length > 150
? `${park.description.slice(0, 150)}...`
: park.description}
</p>
)}
<div className="space-y-2 text-sm text-gray-500">
{park.location && (
<p>
📍 {park.location.latitude}, {park.location.longitude}
</p>
)}
{park.opening_date && (
<p>🗓 Opened: {new Date(park.opening_date).toLocaleDateString()}</p>
)}
{park.operating_season && (
<p> Season: {park.operating_season}</p>
)}
{park.website && (
<p>
<a
href={park.website}
target="_blank"
rel="noopener noreferrer"
className="text-indigo-600 hover:text-indigo-800"
>
Visit Website
</a>
</p>
)}
</div>
<div className="mt-4 pt-4 border-t border-gray-100">
<div className="flex justify-between text-sm text-gray-500">
<span>🎢 {park.ride_count || 0} rides</span>
<span> {formatRating(park.average_rating)}</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}