+ 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