- Replace browse tab with Supabase-connected explore tab with filters - Add React Query for data fetching with loading states - Add 3 structured programs with weekly progression - Add Supabase anonymous auth sync service - Add PostHog analytics with screen tracking and events - Add comprehensive test strategy (Vitest + Maestro E2E) - Add RevenueCat subscription system with DEV simulation - Add i18n translations for new screens (EN/FR/DE/ES) - Add data deletion modal, sync consent modal - Add assessment screen and program routes - Add GitHub Actions CI workflow - Update activity store with sync integration
312 lines
12 KiB
PL/PgSQL
312 lines
12 KiB
PL/PgSQL
-- ============================================================
|
|
-- 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';
|
|
|
|
-- ============================================================
|
|
-- USER SYNC TABLES (Premium Feature - Anonymous Auth)
|
|
-- ============================================================
|
|
|
|
-- User profiles (created after opt-in to sync for personalization)
|
|
CREATE TABLE IF NOT EXISTS public.user_profiles (
|
|
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
email TEXT,
|
|
name TEXT NOT NULL,
|
|
fitness_level TEXT NOT NULL,
|
|
goal TEXT NOT NULL,
|
|
weekly_frequency INTEGER NOT NULL,
|
|
barriers JSONB DEFAULT '[]',
|
|
is_anonymous BOOLEAN DEFAULT true,
|
|
sync_enabled BOOLEAN DEFAULT true,
|
|
subscription_plan TEXT DEFAULT 'premium-yearly',
|
|
onboarding_completed_at TIMESTAMP WITH TIME ZONE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
-- Workout sessions (synced history for personalization)
|
|
CREATE TABLE IF NOT EXISTS public.workout_sessions (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
workout_id UUID NOT NULL REFERENCES public.workouts(id),
|
|
completed_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
duration_seconds INTEGER,
|
|
calories_burned INTEGER,
|
|
feeling_rating INTEGER CHECK (feeling_rating BETWEEN 1 AND 5),
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
-- User preferences for personalization
|
|
CREATE TABLE IF NOT EXISTS public.user_preferences (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
preferred_categories TEXT[] DEFAULT '{}',
|
|
preferred_trainers TEXT[] DEFAULT '{}',
|
|
preferred_durations INTEGER[] DEFAULT '{}',
|
|
difficulty_preference TEXT DEFAULT 'matched',
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
UNIQUE(user_id)
|
|
);
|
|
|
|
-- Indexes for performance
|
|
CREATE INDEX IF NOT EXISTS idx_workout_sessions_user_id ON public.workout_sessions(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_workout_sessions_completed_at ON public.workout_sessions(completed_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_user_profiles_email ON public.user_profiles(email);
|
|
|
|
-- RLS Policies for user data protection
|
|
ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.workout_sessions ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.user_preferences ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY "Users manage own profile"
|
|
ON public.user_profiles FOR ALL USING (id = auth.uid());
|
|
|
|
CREATE POLICY "Users manage own sessions"
|
|
ON public.workout_sessions FOR ALL USING (user_id = auth.uid());
|
|
|
|
CREATE POLICY "Users manage own preferences"
|
|
ON public.user_preferences FOR ALL USING (user_id = auth.uid());
|
|
|
|
-- Triggers for updated_at
|
|
CREATE TRIGGER update_user_profiles_updated_at
|
|
BEFORE UPDATE ON public.user_profiles
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_user_preferences_updated_at
|
|
BEFORE UPDATE ON public.user_preferences
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|