From b397f1fb173c97cbeacc96188981f1e70261165f Mon Sep 17 00:00:00 2001 From: Millian Lamiaux Date: Sat, 14 Mar 2026 20:44:32 +0100 Subject: [PATCH] chore: add supabase schema - Add database schema definitions - Include workouts, trainers, and related tables --- supabase/schema.sql | 237 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 supabase/schema.sql diff --git a/supabase/schema.sql b/supabase/schema.sql new file mode 100644 index 0000000..85b494d --- /dev/null +++ b/supabase/schema.sql @@ -0,0 +1,237 @@ +-- ============================================================ +-- TabataFit Supabase Schema +-- ============================================================ +-- Run this SQL in Supabase SQL Editor to set up your database + +-- Enable UUID extension +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- ============================================================ +-- TABLES +-- ============================================================ + +-- Trainers table +CREATE TABLE IF NOT EXISTS public.trainers ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + specialty TEXT NOT NULL, + color TEXT NOT NULL DEFAULT '#FF6B35', + avatar_url TEXT, + workout_count INTEGER DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Workouts table +CREATE TABLE IF NOT EXISTS public.workouts ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + title TEXT NOT NULL, + trainer_id UUID NOT NULL REFERENCES public.trainers(id) ON DELETE CASCADE, + category TEXT NOT NULL CHECK (category IN ('full-body', 'core', 'upper-body', 'lower-body', 'cardio')), + level TEXT NOT NULL CHECK (level IN ('Beginner', 'Intermediate', 'Advanced')), + duration INTEGER NOT NULL CHECK (duration IN (4, 8, 12, 20)), + calories INTEGER NOT NULL, + rounds INTEGER NOT NULL, + prep_time INTEGER NOT NULL DEFAULT 10, + work_time INTEGER NOT NULL DEFAULT 20, + rest_time INTEGER NOT NULL DEFAULT 10, + equipment TEXT[] DEFAULT '{}', + music_vibe TEXT NOT NULL CHECK (music_vibe IN ('electronic', 'hip-hop', 'pop', 'rock', 'chill')), + exercises JSONB NOT NULL DEFAULT '[]', + thumbnail_url TEXT, + video_url TEXT, + is_featured BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Collections table +CREATE TABLE IF NOT EXISTS public.collections ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + title TEXT NOT NULL, + description TEXT NOT NULL, + icon TEXT NOT NULL, + gradient TEXT[] DEFAULT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Collection-Workouts junction table +CREATE TABLE IF NOT EXISTS public.collection_workouts ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + collection_id UUID NOT NULL REFERENCES public.collections(id) ON DELETE CASCADE, + workout_id UUID NOT NULL REFERENCES public.workouts(id) ON DELETE CASCADE, + sort_order INTEGER NOT NULL DEFAULT 0, + UNIQUE(collection_id, workout_id) +); + +-- Admin users table +CREATE TABLE IF NOT EXISTS public.admin_users ( + id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + email TEXT NOT NULL, + role TEXT NOT NULL CHECK (role IN ('admin', 'editor')) DEFAULT 'editor', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + last_login TIMESTAMP WITH TIME ZONE +); + +-- ============================================================ +-- INDEXES +-- ============================================================ + +CREATE INDEX IF NOT EXISTS idx_workouts_trainer_id ON public.workouts(trainer_id); +CREATE INDEX IF NOT EXISTS idx_workouts_category ON public.workouts(category); +CREATE INDEX IF NOT EXISTS idx_workouts_level ON public.workouts(level); +CREATE INDEX IF NOT EXISTS idx_workouts_is_featured ON public.workouts(is_featured); +CREATE INDEX IF NOT EXISTS idx_workouts_created_at ON public.workouts(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_collection_workouts_collection_id ON public.collection_workouts(collection_id); +CREATE INDEX IF NOT EXISTS idx_collection_workouts_workout_id ON public.collection_workouts(workout_id); + +-- ============================================================ +-- FUNCTIONS +-- ============================================================ + +-- Update timestamps automatically +CREATE OR REPLACE FUNCTION public.update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Create triggers for updated_at +CREATE TRIGGER update_trainers_updated_at BEFORE UPDATE ON public.trainers + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +CREATE TRIGGER update_workouts_updated_at BEFORE UPDATE ON public.workouts + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +CREATE TRIGGER update_collections_updated_at BEFORE UPDATE ON public.collections + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +-- Update workout count for trainers +CREATE OR REPLACE FUNCTION public.update_trainer_workout_count() +RETURNS TRIGGER AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + UPDATE public.trainers SET workout_count = workout_count + 1 WHERE id = NEW.trainer_id; + ELSIF (TG_OP = 'DELETE') THEN + UPDATE public.trainers SET workout_count = workout_count - 1 WHERE id = OLD.trainer_id; + ELSIF (TG_OP = 'UPDATE' AND OLD.trainer_id IS DISTINCT FROM NEW.trainer_id) THEN + UPDATE public.trainers SET workout_count = workout_count - 1 WHERE id = OLD.trainer_id; + UPDATE public.trainers SET workout_count = workout_count + 1 WHERE id = NEW.trainer_id; + END IF; + RETURN NULL; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER update_trainer_workout_count_trigger + AFTER INSERT OR UPDATE OR DELETE ON public.workouts + FOR EACH ROW EXECUTE FUNCTION public.update_trainer_workout_count(); + +-- ============================================================ +-- ROW LEVEL SECURITY (RLS) +-- ============================================================ + +-- Enable RLS on all tables +ALTER TABLE public.trainers ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.workouts ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.collections ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.collection_workouts ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.admin_users ENABLE ROW LEVEL SECURITY; + +-- Trainers policies +CREATE POLICY "Allow public read access" ON public.trainers + FOR SELECT USING (true); + +CREATE POLICY "Allow admin write access" ON public.trainers + FOR ALL USING ( + EXISTS (SELECT 1 FROM public.admin_users WHERE id = auth.uid()) + ); + +-- Workouts policies +CREATE POLICY "Allow public read access" ON public.workouts + FOR SELECT USING (true); + +CREATE POLICY "Allow admin write access" ON public.workouts + FOR ALL USING ( + EXISTS (SELECT 1 FROM public.admin_users WHERE id = auth.uid()) + ); + +-- Collections policies +CREATE POLICY "Allow public read access" ON public.collections + FOR SELECT USING (true); + +CREATE POLICY "Allow admin write access" ON public.collections + FOR ALL USING ( + EXISTS (SELECT 1 FROM public.admin_users WHERE id = auth.uid()) + ); + +-- Collection_workouts policies +CREATE POLICY "Allow public read access" ON public.collection_workouts + FOR SELECT USING (true); + +CREATE POLICY "Allow admin write access" ON public.collection_workouts + FOR ALL USING ( + EXISTS (SELECT 1 FROM public.admin_users WHERE id = auth.uid()) + ); + +-- Admin users policies +CREATE POLICY "Allow admin self access" ON public.admin_users + FOR ALL USING (id = auth.uid()); + +-- ============================================================ +-- STORAGE BUCKETS +-- ============================================================ + +-- Create storage buckets (run in Supabase Dashboard or use supabase CLI) +-- These need to be created via the UI or Storage API + +/* +Bucket name: workout-videos +Public: true +Allowed MIME types: video/mp4, video/webm, video/quicktime +Max file size: 500MB + +Bucket name: workout-thumbnails +Public: true +Allowed MIME types: image/jpeg, image/png, image/webp +Max file size: 10MB + +Bucket name: trainer-avatars +Public: true +Allowed MIME types: image/jpeg, image/png, image/webp +Max file size: 5MB +*/ + +-- ============================================================ +-- SAMPLE DATA (Optional) +-- ============================================================ + +-- Insert sample trainers +INSERT INTO public.trainers (name, specialty, color) VALUES + ('Emma', 'HIIT Specialist', '#FF6B35'), + ('Jake', 'Strength Coach', '#5AC8FA'), + ('Alex', 'Cardio Expert', '#30D158'), + ('Sarah', 'Yoga & Mobility', '#FF9500'), + ('Mike', 'CrossFit Trainer', '#AF52DE') +ON CONFLICT DO NOTHING; + +-- Insert sample collections +INSERT INTO public.collections (title, description, icon, gradient) VALUES + ('Quick Burns', 'Short workouts for busy days', 'flame', ARRAY['#FF6B35', '#FF9500']), + ('Full Body Power', 'Complete body workouts', 'figure.strengthtraining.traditional', ARRAY['#5AC8FA', '#30D158']), + ('Core Crusher', 'Focus on abdominal strength', 'target', ARRAY['#AF52DE', '#FF6B35']), + ('Beginner Friendly', 'Perfect for getting started', 'star', ARRAY['#30D158', '#5AC8FA']), + ('Advanced Challenge', 'Push your limits', 'bolt', ARRAY['#FF3B30', '#FF6B35']) +ON CONFLICT DO NOTHING; + +-- ============================================================ +-- ADMIN SETUP +-- ============================================================ + +-- To add yourself as admin, run this after creating your account: +-- INSERT INTO public.admin_users (id, email, role) +-- SELECT id, email, 'admin' +-- FROM auth.users +-- WHERE email = 'your-email@example.com';