diff --git a/app/_layout.tsx b/app/_layout.tsx
index 72ab931..a125182 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -29,6 +29,7 @@ import { useUserStore } from '@/src/shared/stores'
import { useNotifications } from '@/src/shared/hooks'
import { initializePurchases } from '@/src/shared/services/purchases'
import { initializeAnalytics, getPostHogClient, trackScreen } from '@/src/shared/services/analytics'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
Notifications.setNotificationHandler({
handleNotification: async () => ({
@@ -42,6 +43,18 @@ Notifications.setNotificationHandler({
SplashScreen.preventAutoHideAsync()
+// Create React Query Client
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 1000 * 60 * 5, // 5 minutes
+ gcTime: 1000 * 60 * 60 * 24, // 24 hours
+ retry: 2,
+ refetchOnWindowFocus: false,
+ },
+ },
+})
+
function RootLayoutInner() {
const colors = useThemeColors()
@@ -86,6 +99,7 @@ function RootLayoutInner() {
}
const content = (
+
+
)
const posthogClient = getPostHogClient()
diff --git a/package.json b/package.json
index fdb8ba4..14ea2c0 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,9 @@
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@supabase/supabase-js": "^2.98.0",
+ "@tanstack/query-async-storage-persister": "^5.90.24",
+ "@tanstack/react-query": "^5.90.21",
+ "@tanstack/react-query-persist-client": "^5.90.24",
"expo": "~54.0.33",
"expo-application": "~7.0.8",
"expo-av": "~16.0.8",
diff --git a/src/shared/components/loading/Skeleton.tsx b/src/shared/components/loading/Skeleton.tsx
new file mode 100644
index 0000000..be7cfb5
--- /dev/null
+++ b/src/shared/components/loading/Skeleton.tsx
@@ -0,0 +1,190 @@
+/**
+ * Loading Skeleton Components
+ * Shimmer loading states for better UX
+ */
+
+import { View, StyleSheet, Animated } from 'react-native'
+import { useEffect, useRef } from 'react'
+import { useThemeColors } from '@/src/shared/theme'
+import { SPACING } from '@/src/shared/constants/spacing'
+import { RADIUS } from '@/src/shared/constants/borderRadius'
+
+interface SkeletonProps {
+ width?: number | string
+ height?: number
+ borderRadius?: number
+ style?: any
+}
+
+/**
+ * Shimmer Skeleton Component
+ */
+export function Skeleton({ width = '100%', height = 20, borderRadius = RADIUS.MD, style }: SkeletonProps) {
+ const colors = useThemeColors()
+ const shimmerValue = useRef(new Animated.Value(0)).current
+
+ useEffect(() => {
+ const shimmer = Animated.loop(
+ Animated.sequence([
+ Animated.timing(shimmerValue, {
+ toValue: 1,
+ duration: 1500,
+ useNativeDriver: true,
+ }),
+ Animated.timing(shimmerValue, {
+ toValue: 0,
+ duration: 1500,
+ useNativeDriver: true,
+ }),
+ ])
+ )
+ shimmer.start()
+ return () => shimmer.stop()
+ }, [])
+
+ const translateX = shimmerValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [-200, 200],
+ })
+
+ return (
+
+
+
+ )
+}
+
+/**
+ * Workout Card Skeleton
+ */
+export function WorkoutCardSkeleton() {
+ const colors = useThemeColors()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * Trainer Card Skeleton
+ */
+export function TrainerCardSkeleton() {
+ const colors = useThemeColors()
+
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * Collection Card Skeleton
+ */
+export function CollectionCardSkeleton() {
+ return (
+
+
+
+
+ )
+}
+
+/**
+ * Stats Card Skeleton
+ */
+export function StatsCardSkeleton() {
+ const colors = useThemeColors()
+
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ skeleton: {
+ overflow: 'hidden',
+ },
+ shimmer: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
+ width: 100,
+ },
+ card: {
+ borderRadius: RADIUS.LG,
+ overflow: 'hidden',
+ marginBottom: SPACING[4],
+ },
+ cardContent: {
+ padding: SPACING[4],
+ gap: SPACING[2],
+ },
+ row: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginTop: SPACING[2],
+ },
+ trainerCard: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: SPACING[4],
+ borderRadius: RADIUS.LG,
+ marginBottom: SPACING[3],
+ },
+ trainerInfo: {
+ marginLeft: SPACING[4],
+ flex: 1,
+ gap: SPACING[2],
+ },
+ collectionCard: {
+ alignItems: 'center',
+ padding: SPACING[4],
+ },
+ statsCard: {
+ padding: SPACING[4],
+ borderRadius: RADIUS.LG,
+ minWidth: 140,
+ },
+ statsHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+})
\ No newline at end of file
diff --git a/src/shared/hooks/useSupabaseData.ts b/src/shared/hooks/useSupabaseData.ts
index fc990a8..b9c1443 100644
--- a/src/shared/hooks/useSupabaseData.ts
+++ b/src/shared/hooks/useSupabaseData.ts
@@ -1,397 +1,151 @@
-import { useState, useEffect, useCallback } from 'react'
+/**
+ * React Query Data Hooks
+ * Replaces manual state management with React Query for better caching and performance
+ */
+
+import { useQuery } from '@tanstack/react-query'
import { dataService } from '../data/dataService'
import type { Workout, Trainer, Collection, Program } from '../types'
+// Query Keys
+export const queryKeys = {
+ workouts: 'workouts',
+ workout: (id: string) => ['workouts', id],
+ workoutsByCategory: (category: string) => ['workouts', 'category', category],
+ workoutsByTrainer: (trainerId: string) => ['workouts', 'trainer', trainerId],
+ featuredWorkouts: ['workouts', 'featured'],
+ trainers: 'trainers',
+ trainer: (id: string) => ['trainers', id],
+ collections: 'collections',
+ collection: (id: string) => ['collections', id],
+ programs: 'programs',
+} as const
+
+/**
+ * Hook to fetch all workouts
+ */
export function useWorkouts() {
- const [workouts, setWorkouts] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
-
- const fetchWorkouts = useCallback(async () => {
- try {
- setLoading(true)
- const data = await dataService.getAllWorkouts()
- setWorkouts(data)
- setError(null)
- } catch (err) {
- setError(err as Error)
- } finally {
- setLoading(false)
- }
- }, [])
-
- useEffect(() => {
- let mounted = true
-
- async function load() {
- try {
- setLoading(true)
- const data = await dataService.getAllWorkouts()
- if (mounted) {
- setWorkouts(data)
- setError(null)
- }
- } catch (err) {
- if (mounted) {
- setError(err as Error)
- }
- } finally {
- if (mounted) {
- setLoading(false)
- }
- }
- }
-
- load()
- return () => { mounted = false }
- }, [])
-
- return { workouts, loading, error, refetch: fetchWorkouts }
+ return useQuery({
+ queryKey: [queryKeys.workouts],
+ queryFn: () => dataService.getAllWorkouts(),
+ staleTime: 1000 * 60 * 5, // 5 minutes
+ })
}
+/**
+ * Hook to fetch single workout
+ */
export function useWorkout(id: string | undefined) {
- const [workout, setWorkout] = useState()
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
-
- useEffect(() => {
- let mounted = true
-
- async function load() {
- if (!id) {
- setLoading(false)
- return
- }
-
- try {
- setLoading(true)
- const data = await dataService.getWorkoutById(id)
- if (mounted) {
- setWorkout(data)
- setError(null)
- }
- } catch (err) {
- if (mounted) {
- setError(err as Error)
- }
- } finally {
- if (mounted) {
- setLoading(false)
- }
- }
- }
-
- load()
- return () => { mounted = false }
- }, [id])
-
- return { workout, loading, error }
+ return useQuery({
+ queryKey: queryKeys.workout(id || ''),
+ queryFn: () => dataService.getWorkoutById(id!),
+ enabled: !!id,
+ staleTime: 1000 * 60 * 5,
+ })
}
+/**
+ * Hook to fetch workouts by category
+ */
export function useWorkoutsByCategory(category: string) {
- const [workouts, setWorkouts] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
-
- useEffect(() => {
- let mounted = true
-
- async function load() {
- try {
- setLoading(true)
- const data = await dataService.getWorkoutsByCategory(category)
- if (mounted) {
- setWorkouts(data)
- setError(null)
- }
- } catch (err) {
- if (mounted) {
- setError(err as Error)
- }
- } finally {
- if (mounted) {
- setLoading(false)
- }
- }
- }
-
- load()
- return () => { mounted = false }
- }, [category])
-
- return { workouts, loading, error }
-}
-
-export function useTrainers() {
- const [trainers, setTrainers] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
-
- const fetchTrainers = useCallback(async () => {
- try {
- setLoading(true)
- const data = await dataService.getAllTrainers()
- setTrainers(data)
- setError(null)
- } catch (err) {
- setError(err as Error)
- } finally {
- setLoading(false)
- }
- }, [])
-
- useEffect(() => {
- let mounted = true
-
- async function load() {
- try {
- setLoading(true)
- const data = await dataService.getAllTrainers()
- if (mounted) {
- setTrainers(data)
- setError(null)
- }
- } catch (err) {
- if (mounted) {
- setError(err as Error)
- }
- } finally {
- if (mounted) {
- setLoading(false)
- }
- }
- }
-
- load()
- return () => { mounted = false }
- }, [])
-
- return { trainers, loading, error, refetch: fetchTrainers }
-}
-
-export function useTrainer(id: string | undefined) {
- const [trainer, setTrainer] = useState()
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
-
- useEffect(() => {
- let mounted = true
-
- async function load() {
- if (!id) {
- setLoading(false)
- return
- }
-
- try {
- setLoading(true)
- const data = await dataService.getTrainerById(id)
- if (mounted) {
- setTrainer(data)
- setError(null)
- }
- } catch (err) {
- if (mounted) {
- setError(err as Error)
- }
- } finally {
- if (mounted) {
- setLoading(false)
- }
- }
- }
-
- load()
- return () => { mounted = false }
- }, [id])
-
- return { trainer, loading, error }
-}
-
-export function useCollections() {
- const [collections, setCollections] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
-
- const fetchCollections = useCallback(async () => {
- try {
- setLoading(true)
- const data = await dataService.getAllCollections()
- setCollections(data)
- setError(null)
- } catch (err) {
- setError(err as Error)
- } finally {
- setLoading(false)
- }
- }, [])
-
- useEffect(() => {
- let mounted = true
-
- async function load() {
- try {
- setLoading(true)
- const data = await dataService.getAllCollections()
- if (mounted) {
- setCollections(data)
- setError(null)
- }
- } catch (err) {
- if (mounted) {
- setError(err as Error)
- }
- } finally {
- if (mounted) {
- setLoading(false)
- }
- }
- }
-
- load()
- return () => { mounted = false }
- }, [])
-
- return { collections, loading, error, refetch: fetchCollections }
-}
-
-export function useCollection(id: string | undefined) {
- const [collection, setCollection] = useState()
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
-
- useEffect(() => {
- let mounted = true
-
- async function load() {
- if (!id) {
- setLoading(false)
- return
- }
-
- try {
- setLoading(true)
- const data = await dataService.getCollectionById(id)
- if (mounted) {
- setCollection(data)
- setError(null)
- }
- } catch (err) {
- if (mounted) {
- setError(err as Error)
- }
- } finally {
- if (mounted) {
- setLoading(false)
- }
- }
- }
-
- load()
- return () => { mounted = false }
- }, [id])
-
- return { collection, loading, error }
-}
-
-export function usePrograms() {
- const [programs, setPrograms] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
-
- useEffect(() => {
- let mounted = true
-
- async function load() {
- try {
- setLoading(true)
- const data = await dataService.getAllPrograms()
- if (mounted) {
- setPrograms(data)
- setError(null)
- }
- } catch (err) {
- if (mounted) {
- setError(err as Error)
- }
- } finally {
- if (mounted) {
- setLoading(false)
- }
- }
- }
-
- load()
- return () => { mounted = false }
- }, [])
-
- return { programs, loading, error }
-}
-
-export function useFeaturedWorkouts() {
- const [workouts, setWorkouts] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
-
- useEffect(() => {
- let mounted = true
-
- async function load() {
- try {
- setLoading(true)
- const data = await dataService.getFeaturedWorkouts()
- if (mounted) {
- setWorkouts(data)
- setError(null)
- }
- } catch (err) {
- if (mounted) {
- setError(err as Error)
- }
- } finally {
- if (mounted) {
- setLoading(false)
- }
- }
- }
-
- load()
- return () => { mounted = false }
- }, [])
-
- return { workouts, loading, error }
+ return useQuery({
+ queryKey: queryKeys.workoutsByCategory(category),
+ queryFn: () => dataService.getWorkoutsByCategory(category),
+ enabled: !!category,
+ staleTime: 1000 * 60 * 5,
+ })
}
+/**
+ * Hook to fetch workouts by trainer
+ */
export function useWorkoutsByTrainer(trainerId: string) {
- const [workouts, setWorkouts] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
-
- useEffect(() => {
- let mounted = true
-
- async function load() {
- try {
- setLoading(true)
- const data = await dataService.getWorkoutsByTrainer(trainerId)
- if (mounted) {
- setWorkouts(data)
- setError(null)
- }
- } catch (err) {
- if (mounted) {
- setError(err as Error)
- }
- } finally {
- if (mounted) {
- setLoading(false)
- }
- }
- }
-
- load()
- return () => { mounted = false }
- }, [trainerId])
-
- return { workouts, loading, error }
+ return useQuery({
+ queryKey: queryKeys.workoutsByTrainer(trainerId),
+ queryFn: () => dataService.getWorkoutsByTrainer(trainerId),
+ enabled: !!trainerId,
+ staleTime: 1000 * 60 * 5,
+ })
}
+
+/**
+ * Hook to fetch featured workouts
+ */
+export function useFeaturedWorkouts() {
+ return useQuery({
+ queryKey: queryKeys.featuredWorkouts,
+ queryFn: () => dataService.getFeaturedWorkouts(),
+ staleTime: 1000 * 60 * 5,
+ })
+}
+
+/**
+ * Hook to fetch popular workouts (uses recent workouts as proxy)
+ */
+export function usePopularWorkouts(count: number = 8) {
+ return useQuery({
+ queryKey: ['workouts', 'popular', count],
+ queryFn: async () => {
+ const allWorkouts = await dataService.getAllWorkouts()
+ return allWorkouts.slice(0, count)
+ },
+ staleTime: 1000 * 60 * 5,
+ })
+}
+
+/**
+ * Hook to fetch all trainers
+ */
+export function useTrainers() {
+ return useQuery({
+ queryKey: [queryKeys.trainers],
+ queryFn: () => dataService.getAllTrainers(),
+ staleTime: 1000 * 60 * 5,
+ })
+}
+
+/**
+ * Hook to fetch single trainer
+ */
+export function useTrainer(id: string | undefined) {
+ return useQuery({
+ queryKey: queryKeys.trainer(id || ''),
+ queryFn: () => dataService.getTrainerById(id!),
+ enabled: !!id,
+ staleTime: 1000 * 60 * 5,
+ })
+}
+
+/**
+ * Hook to fetch all collections
+ */
+export function useCollections() {
+ return useQuery({
+ queryKey: [queryKeys.collections],
+ queryFn: () => dataService.getAllCollections(),
+ staleTime: 1000 * 60 * 5,
+ })
+}
+
+/**
+ * Hook to fetch single collection
+ */
+export function useCollection(id: string | undefined) {
+ return useQuery({
+ queryKey: queryKeys.collection(id || ''),
+ queryFn: () => dataService.getCollectionById(id!),
+ enabled: !!id,
+ staleTime: 1000 * 60 * 5,
+ })
+}
+
+/**
+ * Hook to fetch all programs
+ */
+export function usePrograms() {
+ return useQuery({
+ queryKey: [queryKeys.programs],
+ queryFn: () => dataService.getAllPrograms(),
+ staleTime: 1000 * 60 * 5,
+ })
+}
\ No newline at end of file