diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 00000000..1c1e03c9 --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,126 @@ +import { useState } from 'react'; +import { Search, Menu, Zap, MapPin, Star } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; +import { Badge } from '@/components/ui/badge'; + +export function Header() { + const [isSearchFocused, setIsSearchFocused] = useState(false); + + return ( +
+
+ {/* Logo and Brand */} +
+
+
+ +
+
+
+

+ ThrillWiki +

+ Theme Park Database +
+
+ + {/* Desktop Navigation */} + +
+ + {/* Search Bar */} +
+
+ + setIsSearchFocused(true)} + onBlur={() => setIsSearchFocused(false)} + /> + {isSearchFocused && ( +
+
Popular searches
+
+ + Roller Coasters + + + Disney World + + + Cedar Point + +
+
+ )} +
+
+ + {/* User Actions */} +
+ + + + {/* Mobile Menu */} + + + + + +
+ + + + +
+ + +
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/parks/ParkCard.tsx b/src/components/parks/ParkCard.tsx new file mode 100644 index 00000000..4a4cde36 --- /dev/null +++ b/src/components/parks/ParkCard.tsx @@ -0,0 +1,132 @@ +import { MapPin, Star, Users, Clock } from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Park } from '@/types/database'; + +interface ParkCardProps { + park: Park; + onClick?: () => void; +} + +export function ParkCard({ park, onClick }: ParkCardProps) { + const getStatusColor = (status: string) => { + switch (status) { + case 'operating': return 'bg-green-500/20 text-green-400 border-green-500/30'; + case 'seasonal': return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'; + case 'under_construction': return 'bg-blue-500/20 text-blue-400 border-blue-500/30'; + default: return 'bg-red-500/20 text-red-400 border-red-500/30'; + } + }; + + const getParkTypeIcon = (type: string) => { + switch (type) { + case 'theme_park': return '🏰'; + case 'amusement_park': return '🎢'; + case 'water_park': return '🏊'; + case 'family_entertainment': return '🎪'; + default: return '🎡'; + } + }; + + const formatParkType = (type: string) => { + return type.split('_').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(' '); + }; + + return ( + +
+ {/* Image Placeholder with Gradient */} +
+ {park.card_image_url ? ( + {park.name} + ) : ( +
+ {getParkTypeIcon(park.park_type)} +
+ )} + + {/* Gradient Overlay */} +
+ + {/* Status Badge */} + + {park.status.replace('_', ' ').toUpperCase()} + +
+ + + {/* Header */} +
+
+

+ {park.name} +

+ {getParkTypeIcon(park.park_type)} +
+ + {park.location && ( +
+ + {park.location.city && `${park.location.city}, `}{park.location.country} +
+ )} +
+ + {/* Description */} + {park.description && ( +

+ {park.description} +

+ )} + + {/* Park Type */} + + {formatParkType(park.park_type)} + + + {/* Stats */} +
+
+
+ {park.ride_count} + rides +
+
+ {park.coaster_count} + 🎢 +
+
+ + {park.average_rating > 0 && ( +
+ + {park.average_rating.toFixed(1)} + ({park.review_count}) +
+ )} +
+ + {/* Action Button */} + +
+
+ + ); +} \ No newline at end of file diff --git a/src/components/parks/ParkGrid.tsx b/src/components/parks/ParkGrid.tsx new file mode 100644 index 00000000..e37eda2d --- /dev/null +++ b/src/components/parks/ParkGrid.tsx @@ -0,0 +1,268 @@ +import { useState, useEffect } from 'react'; +import { Search, Filter, MapPin, SlidersHorizontal } from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Sheet, SheetContent, SheetTrigger, SheetHeader, SheetTitle } from '@/components/ui/sheet'; +import { ParkCard } from './ParkCard'; +import { Park } from '@/types/database'; +import { supabase } from '@/integrations/supabase/client'; + +export function ParkGrid() { + const [parks, setParks] = useState([]); + const [filteredParks, setFilteredParks] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [sortBy, setSortBy] = useState('name'); + const [filterStatus, setFilterStatus] = useState('all'); + const [filterType, setFilterType] = useState('all'); + + useEffect(() => { + fetchParks(); + }, []); + + useEffect(() => { + filterAndSortParks(); + }, [parks, searchTerm, sortBy, filterStatus, filterType]); + + const fetchParks = async () => { + try { + const { data, error } = await supabase + .from('parks') + .select(` + *, + location:locations(*), + operator:companies!parks_operator_id_fkey(*), + property_owner:companies!parks_property_owner_id_fkey(*) + `) + .order('name'); + + if (error) throw error; + setParks(data || []); + } catch (error) { + console.error('Error fetching parks:', error); + } finally { + setLoading(false); + } + }; + + const filterAndSortParks = () => { + let filtered = parks.filter(park => { + const matchesSearch = park.name.toLowerCase().includes(searchTerm.toLowerCase()) || + park.description?.toLowerCase().includes(searchTerm.toLowerCase()) || + park.location?.city?.toLowerCase().includes(searchTerm.toLowerCase()) || + park.location?.country?.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesStatus = filterStatus === 'all' || park.status === filterStatus; + const matchesType = filterType === 'all' || park.park_type === filterType; + + return matchesSearch && matchesStatus && matchesType; + }); + + // Sort parks + filtered.sort((a, b) => { + switch (sortBy) { + case 'name': + return a.name.localeCompare(b.name); + case 'rating': + return (b.average_rating || 0) - (a.average_rating || 0); + case 'rides': + return (b.ride_count || 0) - (a.ride_count || 0); + case 'recent': + return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); + default: + return 0; + } + }); + + setFilteredParks(filtered); + }; + + const clearFilters = () => { + setSearchTerm(''); + setSortBy('name'); + setFilterStatus('all'); + setFilterType('all'); + }; + + if (loading) { + return ( +
+
+ {[...Array(8)].map((_, i) => ( +
+
+
+ ))} +
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

+ Discover Theme Parks +

+

+ Explore {parks.length} amazing theme parks around the world +

+
+ +
+ + {/* Search and Filters */} +
+ {/* Search */} +
+ + setSearchTerm(e.target.value)} + className="pl-10 bg-muted/50 border-border/50 focus:border-primary/50" + /> +
+ + {/* Quick Sort */} + + + {/* Advanced Filters */} + + + + + + + Filter Parks + +
+ {/* Status Filter */} +
+ + +
+ + {/* Type Filter */} +
+ + +
+ + +
+
+
+
+ + {/* Active Filters */} + {(filterStatus !== 'all' || filterType !== 'all' || searchTerm) && ( +
+ Active filters: + {searchTerm && ( + setSearchTerm('')} className="cursor-pointer"> + Search: {searchTerm} ✕ + + )} + {filterStatus !== 'all' && ( + setFilterStatus('all')} className="cursor-pointer"> + Status: {filterStatus} ✕ + + )} + {filterType !== 'all' && ( + setFilterType('all')} className="cursor-pointer"> + Type: {filterType.replace('_', ' ')} ✕ + + )} + +
+ )} + + {/* Results Count */} +
+ Showing {filteredParks.length} of {parks.length} parks +
+ + {/* Parks Grid */} + {filteredParks.length === 0 ? ( +
+
🎢
+

No parks found

+

+ Try adjusting your search terms or filters +

+ +
+ ) : ( +
+ {filteredParks.map((park) => ( + { + // Navigate to park detail page + console.log('Navigate to park:', park.slug); + }} + /> + ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index 4844bbda..68a8c3e6 100644 --- a/src/index.css +++ b/src/index.css @@ -8,33 +8,51 @@ All colors MUST be HSL. @layer base { :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; + /* ThrillWiki vibrant theme park inspired design system */ + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; + /* Vibrant thrill-inspired primary colors */ + --primary: 271 91% 65%; /* Electric purple for excitement */ + --primary-foreground: 0 0% 98%; + --primary-glow: 271 100% 75%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; + --secondary: 199 89% 48%; /* Bright cyan for energy */ + --secondary-foreground: 0 0% 98%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; + --muted: 240 5% 64%; + --muted-foreground: 240 5% 64%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --accent: 38 92% 50%; /* Vibrant orange for action */ + --accent-foreground: 0 0% 98%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; + --destructive: 0 84% 60%; + --destructive-foreground: 0 0% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; + --border: 240 6% 10%; + --input: 240 6% 10%; + --ring: 271 91% 65%; + + /* Theme park gradients */ + --gradient-primary: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-glow))); + --gradient-secondary: linear-gradient(135deg, hsl(var(--secondary)), hsl(var(--accent))); + --gradient-hero: linear-gradient(135deg, hsl(271 91% 65%), hsl(199 89% 48%), hsl(38 92% 50%)); + --gradient-card: linear-gradient(145deg, hsl(240 10% 5%), hsl(240 8% 7%)); + + /* Thrill shadows and effects */ + --shadow-glow: 0 0 40px hsl(var(--primary) / 0.4); + --shadow-card: 0 10px 30px -10px hsl(var(--primary) / 0.3); + --shadow-intense: 0 25px 50px -12px hsl(var(--primary) / 0.5); + + /* Animation variables */ + --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + --transition-bounce: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); --radius: 0.5rem; @@ -56,33 +74,35 @@ All colors MUST be HSL. } .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; + /* Dark mode uses the same vibrant theme */ + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; + --card: 240 8% 7%; + --card-foreground: 0 0% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; + --popover: 240 8% 7%; + --popover-foreground: 0 0% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; + --primary: 271 91% 65%; + --primary-foreground: 0 0% 98%; + --primary-glow: 271 100% 75%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; + --secondary: 199 89% 48%; + --secondary-foreground: 0 0% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; + --muted: 240 5% 20%; + --muted-foreground: 240 5% 64%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; + --accent: 38 92% 50%; + --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; + --destructive: 0 84% 60%; + --destructive-foreground: 0 0% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; + --border: 240 6% 15%; + --input: 240 6% 15%; + --ring: 271 91% 65%; --sidebar-background: 240 5.9% 10%; --sidebar-foreground: 240 4.8% 95.9%; --sidebar-primary: 224.3 76.3% 48%; diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 59972747..250c2133 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -14,7 +14,650 @@ export type Database = { } public: { Tables: { - [_ in never]: never + companies: { + Row: { + company_type: string + created_at: string + description: string | null + founded_year: number | null + headquarters_location: string | null + id: string + logo_url: string | null + name: string + slug: string + updated_at: string + website_url: string | null + } + Insert: { + company_type: string + created_at?: string + description?: string | null + founded_year?: number | null + headquarters_location?: string | null + id?: string + logo_url?: string | null + name: string + slug: string + updated_at?: string + website_url?: string | null + } + Update: { + company_type?: string + created_at?: string + description?: string | null + founded_year?: number | null + headquarters_location?: string | null + id?: string + logo_url?: string | null + name?: string + slug?: string + updated_at?: string + website_url?: string | null + } + Relationships: [] + } + content_submissions: { + Row: { + content: Json + created_at: string + id: string + reviewed_at: string | null + reviewer_id: string | null + reviewer_notes: string | null + status: string + submission_type: string + updated_at: string + user_id: string + } + Insert: { + content: Json + created_at?: string + id?: string + reviewed_at?: string | null + reviewer_id?: string | null + reviewer_notes?: string | null + status?: string + submission_type: string + updated_at?: string + user_id: string + } + Update: { + content?: Json + created_at?: string + id?: string + reviewed_at?: string | null + reviewer_id?: string | null + reviewer_notes?: string | null + status?: string + submission_type?: string + updated_at?: string + user_id?: string + } + Relationships: [] + } + locations: { + Row: { + city: string | null + country: string + created_at: string + id: string + latitude: number | null + longitude: number | null + name: string + postal_code: string | null + state_province: string | null + timezone: string | null + } + Insert: { + city?: string | null + country: string + created_at?: string + id?: string + latitude?: number | null + longitude?: number | null + name: string + postal_code?: string | null + state_province?: string | null + timezone?: string | null + } + Update: { + city?: string | null + country?: string + created_at?: string + id?: string + latitude?: number | null + longitude?: number | null + name?: string + postal_code?: string | null + state_province?: string | null + timezone?: string | null + } + Relationships: [] + } + park_operating_hours: { + Row: { + closing_time: string | null + created_at: string + day_of_week: number + id: string + is_closed: boolean | null + opening_time: string | null + park_id: string + season_end: string | null + season_start: string | null + } + Insert: { + closing_time?: string | null + created_at?: string + day_of_week: number + id?: string + is_closed?: boolean | null + opening_time?: string | null + park_id: string + season_end?: string | null + season_start?: string | null + } + Update: { + closing_time?: string | null + created_at?: string + day_of_week?: number + id?: string + is_closed?: boolean | null + opening_time?: string | null + park_id?: string + season_end?: string | null + season_start?: string | null + } + Relationships: [ + { + foreignKeyName: "park_operating_hours_park_id_fkey" + columns: ["park_id"] + isOneToOne: false + referencedRelation: "parks" + referencedColumns: ["id"] + }, + ] + } + parks: { + Row: { + average_rating: number | null + banner_image_url: string | null + card_image_url: string | null + closing_date: string | null + coaster_count: number | null + created_at: string + description: string | null + email: string | null + id: string + location_id: string | null + name: string + opening_date: string | null + operator_id: string | null + park_type: string + phone: string | null + property_owner_id: string | null + review_count: number | null + ride_count: number | null + slug: string + status: string + updated_at: string + website_url: string | null + } + Insert: { + average_rating?: number | null + banner_image_url?: string | null + card_image_url?: string | null + closing_date?: string | null + coaster_count?: number | null + created_at?: string + description?: string | null + email?: string | null + id?: string + location_id?: string | null + name: string + opening_date?: string | null + operator_id?: string | null + park_type: string + phone?: string | null + property_owner_id?: string | null + review_count?: number | null + ride_count?: number | null + slug: string + status?: string + updated_at?: string + website_url?: string | null + } + Update: { + average_rating?: number | null + banner_image_url?: string | null + card_image_url?: string | null + closing_date?: string | null + coaster_count?: number | null + created_at?: string + description?: string | null + email?: string | null + id?: string + location_id?: string | null + name?: string + opening_date?: string | null + operator_id?: string | null + park_type?: string + phone?: string | null + property_owner_id?: string | null + review_count?: number | null + ride_count?: number | null + slug?: string + status?: string + updated_at?: string + website_url?: string | null + } + Relationships: [ + { + foreignKeyName: "parks_location_id_fkey" + columns: ["location_id"] + isOneToOne: false + referencedRelation: "locations" + referencedColumns: ["id"] + }, + { + foreignKeyName: "parks_operator_id_fkey" + columns: ["operator_id"] + isOneToOne: false + referencedRelation: "companies" + referencedColumns: ["id"] + }, + { + foreignKeyName: "parks_property_owner_id_fkey" + columns: ["property_owner_id"] + isOneToOne: false + referencedRelation: "companies" + referencedColumns: ["id"] + }, + ] + } + profiles: { + Row: { + avatar_url: string | null + bio: string | null + coaster_count: number | null + created_at: string + date_of_birth: string | null + display_name: string | null + id: string + location_id: string | null + park_count: number | null + privacy_level: string + reputation_score: number | null + review_count: number | null + ride_count: number | null + theme_preference: string + updated_at: string + user_id: string + username: string + } + Insert: { + avatar_url?: string | null + bio?: string | null + coaster_count?: number | null + created_at?: string + date_of_birth?: string | null + display_name?: string | null + id?: string + location_id?: string | null + park_count?: number | null + privacy_level?: string + reputation_score?: number | null + review_count?: number | null + ride_count?: number | null + theme_preference?: string + updated_at?: string + user_id: string + username: string + } + Update: { + avatar_url?: string | null + bio?: string | null + coaster_count?: number | null + created_at?: string + date_of_birth?: string | null + display_name?: string | null + id?: string + location_id?: string | null + park_count?: number | null + privacy_level?: string + reputation_score?: number | null + review_count?: number | null + ride_count?: number | null + theme_preference?: string + updated_at?: string + user_id?: string + username?: string + } + Relationships: [ + { + foreignKeyName: "profiles_location_id_fkey" + columns: ["location_id"] + isOneToOne: false + referencedRelation: "locations" + referencedColumns: ["id"] + }, + ] + } + reviews: { + Row: { + content: string | null + created_at: string + helpful_votes: number | null + id: string + moderated_at: string | null + moderated_by: string | null + moderation_status: string + park_id: string | null + photos: Json | null + rating: number + ride_id: string | null + title: string | null + total_votes: number | null + updated_at: string + user_id: string + visit_date: string | null + wait_time_minutes: number | null + } + Insert: { + content?: string | null + created_at?: string + helpful_votes?: number | null + id?: string + moderated_at?: string | null + moderated_by?: string | null + moderation_status?: string + park_id?: string | null + photos?: Json | null + rating: number + ride_id?: string | null + title?: string | null + total_votes?: number | null + updated_at?: string + user_id: string + visit_date?: string | null + wait_time_minutes?: number | null + } + Update: { + content?: string | null + created_at?: string + helpful_votes?: number | null + id?: string + moderated_at?: string | null + moderated_by?: string | null + moderation_status?: string + park_id?: string | null + photos?: Json | null + rating?: number + ride_id?: string | null + title?: string | null + total_votes?: number | null + updated_at?: string + user_id?: string + visit_date?: string | null + wait_time_minutes?: number | null + } + Relationships: [ + { + foreignKeyName: "reviews_park_id_fkey" + columns: ["park_id"] + isOneToOne: false + referencedRelation: "parks" + referencedColumns: ["id"] + }, + { + foreignKeyName: "reviews_ride_id_fkey" + columns: ["ride_id"] + isOneToOne: false + referencedRelation: "rides" + referencedColumns: ["id"] + }, + ] + } + ride_models: { + Row: { + category: string + created_at: string + description: string | null + id: string + manufacturer_id: string + name: string + ride_type: string + slug: string + technical_specs: Json | null + updated_at: string + } + Insert: { + category: string + created_at?: string + description?: string | null + id?: string + manufacturer_id: string + name: string + ride_type: string + slug: string + technical_specs?: Json | null + updated_at?: string + } + Update: { + category?: string + created_at?: string + description?: string | null + id?: string + manufacturer_id?: string + name?: string + ride_type?: string + slug?: string + technical_specs?: Json | null + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "ride_models_manufacturer_id_fkey" + columns: ["manufacturer_id"] + isOneToOne: false + referencedRelation: "companies" + referencedColumns: ["id"] + }, + ] + } + rides: { + Row: { + age_requirement: number | null + average_rating: number | null + capacity_per_hour: number | null + category: string + closing_date: string | null + coaster_stats: Json | null + created_at: string + description: string | null + designer_id: string | null + duration_seconds: number | null + height_requirement: number | null + id: string + image_url: string | null + inversions: number | null + length_meters: number | null + manufacturer_id: string | null + max_height_meters: number | null + max_speed_kmh: number | null + name: string + opening_date: string | null + park_id: string + review_count: number | null + ride_model_id: string | null + slug: string + status: string + technical_specs: Json | null + updated_at: string + } + Insert: { + age_requirement?: number | null + average_rating?: number | null + capacity_per_hour?: number | null + category: string + closing_date?: string | null + coaster_stats?: Json | null + created_at?: string + description?: string | null + designer_id?: string | null + duration_seconds?: number | null + height_requirement?: number | null + id?: string + image_url?: string | null + inversions?: number | null + length_meters?: number | null + manufacturer_id?: string | null + max_height_meters?: number | null + max_speed_kmh?: number | null + name: string + opening_date?: string | null + park_id: string + review_count?: number | null + ride_model_id?: string | null + slug: string + status?: string + technical_specs?: Json | null + updated_at?: string + } + Update: { + age_requirement?: number | null + average_rating?: number | null + capacity_per_hour?: number | null + category?: string + closing_date?: string | null + coaster_stats?: Json | null + created_at?: string + description?: string | null + designer_id?: string | null + duration_seconds?: number | null + height_requirement?: number | null + id?: string + image_url?: string | null + inversions?: number | null + length_meters?: number | null + manufacturer_id?: string | null + max_height_meters?: number | null + max_speed_kmh?: number | null + name?: string + opening_date?: string | null + park_id?: string + review_count?: number | null + ride_model_id?: string | null + slug?: string + status?: string + technical_specs?: Json | null + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "rides_designer_id_fkey" + columns: ["designer_id"] + isOneToOne: false + referencedRelation: "companies" + referencedColumns: ["id"] + }, + { + foreignKeyName: "rides_manufacturer_id_fkey" + columns: ["manufacturer_id"] + isOneToOne: false + referencedRelation: "companies" + referencedColumns: ["id"] + }, + { + foreignKeyName: "rides_park_id_fkey" + columns: ["park_id"] + isOneToOne: false + referencedRelation: "parks" + referencedColumns: ["id"] + }, + { + foreignKeyName: "rides_ride_model_id_fkey" + columns: ["ride_model_id"] + isOneToOne: false + referencedRelation: "ride_models" + referencedColumns: ["id"] + }, + ] + } + user_ride_credits: { + Row: { + created_at: string + first_ride_date: string | null + id: string + ride_count: number | null + ride_id: string + updated_at: string + user_id: string + } + Insert: { + created_at?: string + first_ride_date?: string | null + id?: string + ride_count?: number | null + ride_id: string + updated_at?: string + user_id: string + } + Update: { + created_at?: string + first_ride_date?: string | null + id?: string + ride_count?: number | null + ride_id?: string + updated_at?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "user_ride_credits_ride_id_fkey" + columns: ["ride_id"] + isOneToOne: false + referencedRelation: "rides" + referencedColumns: ["id"] + }, + ] + } + user_top_lists: { + Row: { + created_at: string + description: string | null + id: string + is_public: boolean | null + items: Json + list_type: string + title: string + updated_at: string + user_id: string + } + Insert: { + created_at?: string + description?: string | null + id?: string + is_public?: boolean | null + items: Json + list_type: string + title: string + updated_at?: string + user_id: string + } + Update: { + created_at?: string + description?: string | null + id?: string + is_public?: boolean | null + items?: Json + list_type?: string + title?: string + updated_at?: string + user_id?: string + } + Relationships: [] + } } Views: { [_ in never]: never diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 7130b547..dfea5382 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,12 +1,159 @@ -// Update this page (the content is just a fallback if you fail to update the page) +import { Header } from '@/components/layout/Header'; +import { ParkGrid } from '@/components/parks/ParkGrid'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Zap, MapPin, Star, TrendingUp, Users } from 'lucide-react'; const Index = () => { return ( -
-
-

Welcome to Your Blank App

-

Start building your amazing project here!

-
+
+
+ + {/* Hero Section */} +
+
+
+
+
+ + 🚀 Beta Version + + + Community Driven + +
+ +

+ The Ultimate +
+ + Theme Park Database + +

+ +

+ Discover amazing theme parks, track your ride credits, share reviews, and connect with fellow thrill seekers worldwide. +

+ +
+ + +
+
+
+ + {/* Floating Elements */} +
+
+
+
+
+
+
+
+
+
+ + {/* Stats Section */} +
+
+
+
+
2,500+
+
Theme Parks
+
+
+
15,000+
+
Rides Tracked
+
+
+
50,000+
+
User Reviews
+
+
+
180+
+
Countries
+
+
+
+
+ + {/* Features Section */} +
+
+
+

+ Everything You Need for Your + Thrill Journey +

+

+ From park discovery to ride tracking, ThrillWiki has all the tools enthusiasts need. +

+
+ +
+
+
+ +
+

Discover Parks

+

+ Explore thousands of theme parks worldwide with detailed information, photos, and reviews. +

+
+ +
+
+ +
+

Track Credits

+

+ Keep track of every ride you've experienced and build your personal coaster portfolio. +

+
+ +
+
+ +
+

Join Community

+

+ Share reviews, create top lists, and connect with fellow theme park enthusiasts. +

+
+
+
+
+ + {/* Parks Section */} +
+ +
+ + {/* CTA Section */} +
+
+
+

+ Ready to Start Your + Thrill Adventure? +

+

+ Join thousands of theme park enthusiasts and start tracking your rides today! +

+ +
+
+
); }; diff --git a/src/types/database.ts b/src/types/database.ts new file mode 100644 index 00000000..b6478741 --- /dev/null +++ b/src/types/database.ts @@ -0,0 +1,123 @@ +export interface Location { + id: string; + name: string; + country: string; + state_province?: string; + city?: string; + postal_code?: string; + latitude?: number; + longitude?: number; + timezone?: string; +} + +export interface Company { + id: string; + name: string; + slug: string; + description?: string; + company_type: string; // Allow any string from database + website_url?: string; + founded_year?: number; + headquarters_location?: string; + logo_url?: string; +} + +export interface Park { + id: string; + name: string; + slug: string; + description?: string; + status: string; // Allow any string from database + park_type: string; // Allow any string from database + opening_date?: string; + closing_date?: string; + website_url?: string; + phone?: string; + email?: string; + location?: Location; + operator?: Company; + property_owner?: Company; + banner_image_url?: string; + card_image_url?: string; + average_rating: number; + review_count: number; + ride_count: number; + coaster_count: number; + created_at: string; + updated_at: string; +} + +export interface RideModel { + id: string; + name: string; + slug: string; + manufacturer?: Company; + category: 'roller_coaster' | 'flat_ride' | 'water_ride' | 'dark_ride' | 'kiddie_ride' | 'transportation'; + ride_type: string; + description?: string; + technical_specs?: any; +} + +export interface Ride { + id: string; + name: string; + slug: string; + description?: string; + park?: Park; + ride_model?: RideModel; + manufacturer?: Company; + designer?: Company; + category: 'roller_coaster' | 'flat_ride' | 'water_ride' | 'dark_ride' | 'kiddie_ride' | 'transportation'; + status: 'operating' | 'closed' | 'under_construction' | 'maintenance' | 'sbno'; + opening_date?: string; + closing_date?: string; + height_requirement?: number; + age_requirement?: number; + capacity_per_hour?: number; + duration_seconds?: number; + max_speed_kmh?: number; + max_height_meters?: number; + length_meters?: number; + inversions?: number; + coaster_stats?: any; + technical_specs?: any; + average_rating: number; + review_count: number; + image_url?: string; +} + +export interface Profile { + id: string; + user_id: string; + username: string; + display_name?: string; + bio?: string; + avatar_url?: string; + location?: Location; + date_of_birth?: string; + privacy_level: 'public' | 'friends' | 'private'; + theme_preference: 'light' | 'dark' | 'system'; + ride_count: number; + coaster_count: number; + park_count: number; + review_count: number; + reputation_score: number; +} + +export interface Review { + id: string; + user_id: string; + park?: Park; + ride?: Ride; + rating: number; + title?: string; + content?: string; + visit_date?: string; + wait_time_minutes?: number; + photos?: any; + helpful_votes: number; + total_votes: number; + moderation_status: 'pending' | 'approved' | 'rejected' | 'flagged'; + created_at: string; + updated_at: string; +} \ No newline at end of file diff --git a/supabase/migrations/20250920001531_786b1ae2-d202-49df-a45a-fbe232c9d7b2.sql b/supabase/migrations/20250920001531_786b1ae2-d202-49df-a45a-fbe232c9d7b2.sql new file mode 100644 index 00000000..f61d1ec8 --- /dev/null +++ b/supabase/migrations/20250920001531_786b1ae2-d202-49df-a45a-fbe232c9d7b2.sql @@ -0,0 +1,297 @@ +-- Create core tables for ThrillWiki platform + +-- Companies table (manufacturers, operators, designers, property owners) +CREATE TABLE public.companies ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + name TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE, + description TEXT, + company_type TEXT NOT NULL CHECK (company_type IN ('manufacturer', 'operator', 'designer', 'property_owner')), + website_url TEXT, + founded_year INTEGER, + headquarters_location TEXT, + logo_url TEXT, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +-- Locations table for geographic data +CREATE TABLE public.locations ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + name TEXT NOT NULL, + country TEXT NOT NULL, + state_province TEXT, + city TEXT, + postal_code TEXT, + latitude DECIMAL(10, 8), + longitude DECIMAL(11, 8), + timezone TEXT, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +-- Parks table +CREATE TABLE public.parks ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + name TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE, + description TEXT, + status TEXT NOT NULL DEFAULT 'operating' CHECK (status IN ('operating', 'closed', 'under_construction', 'seasonal')), + park_type TEXT NOT NULL CHECK (park_type IN ('theme_park', 'amusement_park', 'water_park', 'family_entertainment')), + opening_date DATE, + closing_date DATE, + website_url TEXT, + phone TEXT, + email TEXT, + location_id UUID REFERENCES public.locations(id), + operator_id UUID REFERENCES public.companies(id), + property_owner_id UUID REFERENCES public.companies(id), + banner_image_url TEXT, + card_image_url TEXT, + average_rating DECIMAL(3, 2) DEFAULT 0, + review_count INTEGER DEFAULT 0, + ride_count INTEGER DEFAULT 0, + coaster_count INTEGER DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +-- Operating hours for parks +CREATE TABLE public.park_operating_hours ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + park_id UUID NOT NULL REFERENCES public.parks(id) ON DELETE CASCADE, + day_of_week INTEGER NOT NULL CHECK (day_of_week >= 0 AND day_of_week <= 6), -- 0 = Sunday + opening_time TIME, + closing_time TIME, + is_closed BOOLEAN DEFAULT false, + season_start DATE, + season_end DATE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +-- Ride models (templates created by manufacturers) +CREATE TABLE public.ride_models ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + name TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE, + manufacturer_id UUID NOT NULL REFERENCES public.companies(id), + category TEXT NOT NULL CHECK (category IN ('roller_coaster', 'flat_ride', 'water_ride', 'dark_ride', 'kiddie_ride', 'transportation')), + ride_type TEXT NOT NULL, + description TEXT, + technical_specs JSONB, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +-- Rides table +CREATE TABLE public.rides ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + name TEXT NOT NULL, + slug TEXT NOT NULL, + description TEXT, + park_id UUID NOT NULL REFERENCES public.parks(id) ON DELETE CASCADE, + ride_model_id UUID REFERENCES public.ride_models(id), + manufacturer_id UUID REFERENCES public.companies(id), + designer_id UUID REFERENCES public.companies(id), + category TEXT NOT NULL CHECK (category IN ('roller_coaster', 'flat_ride', 'water_ride', 'dark_ride', 'kiddie_ride', 'transportation')), + status TEXT NOT NULL DEFAULT 'operating' CHECK (status IN ('operating', 'closed', 'under_construction', 'maintenance', 'sbno')), + opening_date DATE, + closing_date DATE, + height_requirement INTEGER, -- in centimeters + age_requirement INTEGER, + capacity_per_hour INTEGER, + duration_seconds INTEGER, + max_speed_kmh DECIMAL(5, 2), + max_height_meters DECIMAL(6, 2), + length_meters DECIMAL(8, 2), + inversions INTEGER DEFAULT 0, + coaster_stats JSONB, -- Additional roller coaster specific stats + technical_specs JSONB, + average_rating DECIMAL(3, 2) DEFAULT 0, + review_count INTEGER DEFAULT 0, + image_url TEXT, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + UNIQUE(park_id, slug) +); + +-- User profiles +CREATE TABLE public.profiles ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID NOT NULL UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE, + username TEXT NOT NULL UNIQUE, + display_name TEXT, + bio TEXT, + avatar_url TEXT, + location_id UUID REFERENCES public.locations(id), + date_of_birth DATE, + privacy_level TEXT NOT NULL DEFAULT 'public' CHECK (privacy_level IN ('public', 'friends', 'private')), + theme_preference TEXT NOT NULL DEFAULT 'system' CHECK (theme_preference IN ('light', 'dark', 'system')), + ride_count INTEGER DEFAULT 0, + coaster_count INTEGER DEFAULT 0, + park_count INTEGER DEFAULT 0, + review_count INTEGER DEFAULT 0, + reputation_score INTEGER DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +-- Reviews table +CREATE TABLE public.reviews ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + park_id UUID REFERENCES public.parks(id) ON DELETE CASCADE, + ride_id UUID REFERENCES public.rides(id) ON DELETE CASCADE, + rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5), + title TEXT, + content TEXT, + visit_date DATE, + wait_time_minutes INTEGER, + photos JSONB, -- Array of photo URLs and metadata + helpful_votes INTEGER DEFAULT 0, + total_votes INTEGER DEFAULT 0, + moderation_status TEXT NOT NULL DEFAULT 'pending' CHECK (moderation_status IN ('pending', 'approved', 'rejected', 'flagged')), + moderated_at TIMESTAMP WITH TIME ZONE, + moderated_by UUID REFERENCES auth.users(id), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + CHECK ((park_id IS NOT NULL AND ride_id IS NULL) OR (park_id IS NULL AND ride_id IS NOT NULL)) +); + +-- User ride credits (tracking which rides users have been on) +CREATE TABLE public.user_ride_credits ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + ride_id UUID NOT NULL REFERENCES public.rides(id) ON DELETE CASCADE, + first_ride_date DATE, + ride_count INTEGER DEFAULT 1, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + UNIQUE(user_id, ride_id) +); + +-- User top lists +CREATE TABLE public.user_top_lists ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + title TEXT NOT NULL, + description TEXT, + list_type TEXT NOT NULL CHECK (list_type IN ('parks', 'rides', 'coasters')), + items JSONB NOT NULL, -- Array of {id, position, notes} + is_public BOOLEAN DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +-- Content submissions for moderation +CREATE TABLE public.content_submissions ( + id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + submission_type TEXT NOT NULL CHECK (submission_type IN ('park', 'ride', 'review', 'photo')), + content JSONB NOT NULL, + status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected', 'needs_revision')), + reviewer_id UUID REFERENCES auth.users(id), + reviewer_notes TEXT, + reviewed_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +-- Enable Row Level Security on all tables +ALTER TABLE public.companies ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.locations ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.parks ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.park_operating_hours ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.ride_models ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.rides ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.reviews ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.user_ride_credits ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.user_top_lists ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.content_submissions ENABLE ROW LEVEL SECURITY; + +-- RLS Policies for public read access to core content +CREATE POLICY "Public read access to companies" ON public.companies FOR SELECT USING (true); +CREATE POLICY "Public read access to locations" ON public.locations FOR SELECT USING (true); +CREATE POLICY "Public read access to parks" ON public.parks FOR SELECT USING (true); +CREATE POLICY "Public read access to operating hours" ON public.park_operating_hours FOR SELECT USING (true); +CREATE POLICY "Public read access to ride models" ON public.ride_models FOR SELECT USING (true); +CREATE POLICY "Public read access to rides" ON public.rides FOR SELECT USING (true); +CREATE POLICY "Public read access to approved reviews" ON public.reviews FOR SELECT USING (moderation_status = 'approved'); + +-- Profiles policies +CREATE POLICY "Public read access to public profiles" ON public.profiles FOR SELECT USING (privacy_level = 'public'); +CREATE POLICY "Users can update their own profile" ON public.profiles FOR UPDATE USING (auth.uid() = user_id); +CREATE POLICY "Users can insert their own profile" ON public.profiles FOR INSERT WITH CHECK (auth.uid() = user_id); + +-- User content policies +CREATE POLICY "Users can view their own ride credits" ON public.user_ride_credits FOR SELECT USING (auth.uid() = user_id); +CREATE POLICY "Users can manage their own ride credits" ON public.user_ride_credits FOR ALL USING (auth.uid() = user_id); + +CREATE POLICY "Users can view public top lists" ON public.user_top_lists FOR SELECT USING (is_public = true OR auth.uid() = user_id); +CREATE POLICY "Users can manage their own top lists" ON public.user_top_lists FOR ALL USING (auth.uid() = user_id); + +CREATE POLICY "Users can view their own reviews" ON public.reviews FOR SELECT USING (auth.uid() = user_id); +CREATE POLICY "Users can create reviews" ON public.reviews FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY "Users can update their own reviews" ON public.reviews FOR UPDATE USING (auth.uid() = user_id); + +CREATE POLICY "Users can view their own submissions" ON public.content_submissions FOR SELECT USING (auth.uid() = user_id); +CREATE POLICY "Users can create submissions" ON public.content_submissions FOR INSERT WITH CHECK (auth.uid() = user_id); + +-- Function to update timestamps +CREATE OR REPLACE FUNCTION public.update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql SET search_path = public; + +-- Triggers for automatic timestamp updates +CREATE TRIGGER update_companies_updated_at BEFORE UPDATE ON public.companies FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); +CREATE TRIGGER update_parks_updated_at BEFORE UPDATE ON public.parks FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); +CREATE TRIGGER update_ride_models_updated_at BEFORE UPDATE ON public.ride_models FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); +CREATE TRIGGER update_rides_updated_at BEFORE UPDATE ON public.rides FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); +CREATE TRIGGER update_profiles_updated_at BEFORE UPDATE ON public.profiles FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); +CREATE TRIGGER update_reviews_updated_at BEFORE UPDATE ON public.reviews FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); +CREATE TRIGGER update_user_ride_credits_updated_at BEFORE UPDATE ON public.user_ride_credits FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); +CREATE TRIGGER update_user_top_lists_updated_at BEFORE UPDATE ON public.user_top_lists FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); +CREATE TRIGGER update_content_submissions_updated_at BEFORE UPDATE ON public.content_submissions FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +-- Function to handle new user profile creation +CREATE OR REPLACE FUNCTION public.handle_new_user() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO public.profiles (user_id, username, display_name) + VALUES ( + NEW.id, + COALESCE(NEW.raw_user_meta_data ->> 'username', 'user_' || substring(NEW.id::text, 1, 8)), + COALESCE(NEW.raw_user_meta_data ->> 'display_name', NEW.raw_user_meta_data ->> 'name') + ); + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public; + +-- Trigger for automatic profile creation +CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.handle_new_user(); + +-- Indexes for better performance +CREATE INDEX idx_parks_slug ON public.parks(slug); +CREATE INDEX idx_parks_location ON public.parks(location_id); +CREATE INDEX idx_parks_status ON public.parks(status); +CREATE INDEX idx_parks_park_type ON public.parks(park_type); + +CREATE INDEX idx_rides_park ON public.rides(park_id); +CREATE INDEX idx_rides_slug ON public.rides(park_id, slug); +CREATE INDEX idx_rides_category ON public.rides(category); +CREATE INDEX idx_rides_status ON public.rides(status); + +CREATE INDEX idx_reviews_park ON public.reviews(park_id) WHERE park_id IS NOT NULL; +CREATE INDEX idx_reviews_ride ON public.reviews(ride_id) WHERE ride_id IS NOT NULL; +CREATE INDEX idx_reviews_user ON public.reviews(user_id); +CREATE INDEX idx_reviews_moderation_status ON public.reviews(moderation_status); + +CREATE INDEX idx_profiles_username ON public.profiles(username); +CREATE INDEX idx_user_ride_credits_user ON public.user_ride_credits(user_id); +CREATE INDEX idx_user_top_lists_user ON public.user_top_lists(user_id); \ No newline at end of file