From 0990ec8e11c23175f2e59ef7ec0d78d9cd34349d Mon Sep 17 00:00:00 2001 From: Millian Lamiaux Date: Mon, 13 Apr 2026 22:06:11 +0200 Subject: [PATCH] refactor: remove explore tab, simplify to 3-tab layout (Home, Progress, Profile) Co-Authored-By: Claude Opus 4.6 --- app/(tabs)/_layout.tsx | 27 +- app/(tabs)/activity.tsx | 152 ++-- app/(tabs)/explore.tsx | 1005 ----------------------- app/_layout.tsx | 20 +- app/explore-filters.tsx | 222 ----- src/shared/stores/exploreFilterStore.ts | 30 - src/shared/stores/index.ts | 2 +- 7 files changed, 72 insertions(+), 1386 deletions(-) delete mode 100644 app/(tabs)/explore.tsx delete mode 100644 app/explore-filters.tsx delete mode 100644 src/shared/stores/exploreFilterStore.ts diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 3f6197c..d4553b9 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,14 +1,14 @@ /** * TabataFit Tab Layout - * Native iOS tabs with liquid glass effect - * 4 tabs: Home, Workouts, Activity, Profile + * Native liquid glass tab bar (iOS 26+) — Dark Medical design system + * 3 tabs: Home, Progress, Profile * Redirects to onboarding if not completed */ import { Redirect } from 'expo-router' import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs' import { useTranslation } from 'react-i18next' -import { BRAND } from '@/src/shared/constants/colors' +import { BRAND, TEXT, NAVY } from '@/src/shared/constants/colors' import { useUserStore } from '@/src/shared/stores' export default function TabLayout() { @@ -21,24 +21,25 @@ export default function TabLayout() { return ( - + - - - - - - + - + - + diff --git a/app/(tabs)/activity.tsx b/app/(tabs)/activity.tsx index 36156ee..a42f82d 100644 --- a/app/(tabs)/activity.tsx +++ b/app/(tabs)/activity.tsx @@ -6,10 +6,7 @@ import { View, StyleSheet, ScrollView, Dimensions, Pressable } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useRouter } from 'expo-router' -import { LinearGradient } from 'expo-linear-gradient' -import { BlurView } from 'expo-blur' import { Icon, type IconName } from '@/src/shared/components/Icon' -import Svg, { Circle } from 'react-native-svg' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -17,62 +14,34 @@ import { useActivityStore, getWeeklyActivity } from '@/src/shared/stores' import { getWorkoutById } from '@/src/shared/data' import { ACHIEVEMENTS } from '@/src/shared/data' import { StyledText } from '@/src/shared/components/StyledText' +import { NativeGauge } from '@/src/shared/components/native' +import { NativeButton } from '@/src/shared/components/native' -import { useThemeColors, BRAND, PHASE, GRADIENTS } from '@/src/shared/theme' +import { useThemeColors, BRAND, PHASE } from '@/src/shared/theme' import type { ThemeColors } from '@/src/shared/theme/types' import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' import { RADIUS } from '@/src/shared/constants/borderRadius' +import { GREEN, NAVY, BORDER_COLORS, TEXT } from '@/src/shared/constants/colors' const { width: SCREEN_WIDTH } = Dimensions.get('window') // ═══════════════════════════════════════════════════════════════════════════ -// STAT RING — Custom circular progress (pure RN, no SwiftUI) +// STAT RING — Native SwiftUI Gauge // ═══════════════════════════════════════════════════════════════════════════ function StatRing({ value, max, color, - size = 64, + size = 52, }: { value: number max: number color: string size?: number }) { - const colors = useThemeColors() - const strokeWidth = 5 - const radius = (size - strokeWidth) / 2 - const circumference = 2 * Math.PI * radius - const progress = Math.min(value / max, 1) - const strokeDashoffset = circumference * (1 - progress) - return ( - - {/* Track */} - - {/* Progress */} - 0 ? 1 : 0.3} - /> - + ) } @@ -97,7 +66,6 @@ function StatCard({ const styles = useMemo(() => createStyles(colors), [colors]) return ( - @@ -131,14 +99,7 @@ function WeeklyBar({ const styles = useMemo(() => createStyles(colors), [colors]) return ( - - {completed && ( - - )} - + void }) { return ( - + {t('screens:activity.emptyTitle')} @@ -170,18 +131,14 @@ function EmptyState({ onStartWorkout }: { onStartWorkout: () => void }) { {t('screens:activity.emptySubtitle')} - - + - - - {t('screens:activity.startFirstWorkout')} - - + ) } @@ -235,7 +192,7 @@ export default function ActivityScreen() { {/* Header */} @@ -250,34 +207,28 @@ export default function ActivityScreen() { {/* Empty state when no history */} {history.length === 0 ? ( - router.push('/(tabs)/explore' as any)} /> + router.push('/(tabs)' as any)} /> ) : ( <> {/* Streak Banner */} - - + - + {String(streak.current || 0)} - + {t('screens:activity.dayStreak')} - + {t('screens:activity.longest')} - + {String(streak.longest)} @@ -290,7 +241,7 @@ export default function ActivityScreen() { label={t('screens:activity.workouts')} value={totalWorkouts} max={100} - color={BRAND.PRIMARY} + color={GREEN[500]} icon="dumbbell" /> - {weeklyActivity.map((d, i) => ( - {recentWorkouts.map((result, idx) => { const workout = getWorkoutById(result.workoutId) const workoutTitle = workout ? t(`content:workouts.${workout.id}`, { defaultValue: workout.title }) : t('screens:activity.workouts') @@ -356,7 +305,7 @@ export default function ActivityScreen() { - + @@ -366,7 +315,7 @@ export default function ActivityScreen() { {formatDate(result.completedAt) + ' \u00B7 ' + t('units.minUnit', { count: result.durationMinutes })} - + {t('units.calUnit', { count: result.calories })} @@ -386,19 +335,18 @@ export default function ActivityScreen() { {displayAchievements.map((a) => ( - = { - all: 'all', - 'full-body': 'fullBody', - 'upper-body': 'upperBody', - 'lower-body': 'lowerBody', - core: 'core', - cardio: 'cardio', -} - -const ALL_CATEGORIES: (WorkoutCategory | 'all')[] = [ - 'all', 'full-body', 'upper-body', 'lower-body', 'core', 'cardio', -] - -type FilterState = { - category: WorkoutCategory | 'all' -} - -// ═══════════════════════════════════════════════════════════════════════════ -// TRAINER AVATAR -// ═══════════════════════════════════════════════════════════════════════════ - -function TrainerAvatar({ - trainer, - onPress, - colors, -}: { - trainer: Trainer - onPress: () => void - colors: ThemeColors -}) { - const haptics = useHaptics() - - return ( - { - haptics.buttonTap() - onPress() - }} - > - - - {trainer.name.charAt(0)} - - - - {trainer.name} - - - {trainer.specialty} - - - ) -} - -const trainerStyles = StyleSheet.create({ - container: { - alignItems: 'center', - width: 72, - marginRight: SPACING[4], - }, - avatar: { - width: 56, - height: 56, - borderRadius: 28, - alignItems: 'center', - justifyContent: 'center', - marginBottom: SPACING[1], - borderCurve: 'continuous', - }, - name: { - marginTop: 2, - textAlign: 'center', - }, -}) - -// ═══════════════════════════════════════════════════════════════════════════ -// FILTER PILL -// ═══════════════════════════════════════════════════════════════════════════ - -function FilterPill({ - label, - isSelected, - color, - onPress, - colors, -}: { - label: string - isSelected: boolean - color: string - onPress: () => void - colors: ThemeColors -}) { - const haptics = useHaptics() - - return ( - { - haptics.selection() - onPress() - }} - > - - {label} - - - ) -} - -const filterPillStyles = StyleSheet.create({ - pill: { - paddingHorizontal: SPACING[4], - paddingVertical: SPACING[2], - borderRadius: RADIUS.FULL, - marginRight: SPACING[2], - borderWidth: 1, - borderCurve: 'continuous', - }, -}) - -// ═══════════════════════════════════════════════════════════════════════════ -// SKELETON LOADER -// ═══════════════════════════════════════════════════════════════════════════ - -function LoadingSkeleton({ colors }: { colors: ThemeColors }) { - const { width: screenWidth } = useWindowDimensions() - const pulseAnim = useRef(new Animated.Value(0.3)).current - - useEffect(() => { - const animation = Animated.loop( - Animated.sequence([ - Animated.timing(pulseAnim, { toValue: 0.7, duration: 800, useNativeDriver: true }), - Animated.timing(pulseAnim, { toValue: 0.3, duration: 800, useNativeDriver: true }), - ]) - ) - animation.start() - return () => animation.stop() - }, [pulseAnim]) - - const cardWidth = (screenWidth - LAYOUT.SCREEN_PADDING * 2 - SPACING[3]) / 2 - - return ( - - {/* Search bar skeleton */} - - {/* Hero collection skeleton */} - - {/* Trainer row skeleton */} - - {[0, 1, 2, 3, 4].map((i) => ( - - ))} - - {/* Workout grid skeleton */} - - {[0, 1].map((i) => ( - - ))} - - - ) -} - -// ═══════════════════════════════════════════════════════════════════════════ -// SECTION HEADER -// ═══════════════════════════════════════════════════════════════════════════ - -function SectionHeader({ - title, - rightLabel, - onRightPress, - colors, -}: { - title: string - rightLabel?: string - onRightPress?: () => void - colors: ThemeColors -}) { - return ( - - - {title} - - {rightLabel && onRightPress && ( - - - {rightLabel} - - - )} - - ) -} - -const sectionHeaderStyles = StyleSheet.create({ - container: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: SPACING[4], - }, -}) - -// ═══════════════════════════════════════════════════════════════════════════ -// MAIN SCREEN -// ═══════════════════════════════════════════════════════════════════════════ - -export default function ExploreScreen() { - const { t } = useTranslation() - const insets = useSafeAreaInsets() - const router = useRouter() - const haptics = useHaptics() - const colors = useThemeColors() - const { width: screenWidth } = useWindowDimensions() - const { isPremium } = usePurchases() - const styles = useMemo(() => createStyles(colors, screenWidth), [colors, screenWidth]) - - // ── State ────────────────────────────────────────────────────────────── - const [searchQuery, setSearchQuery] = useState('') - const [filters, setFilters] = useState({ - category: 'all', - }) - - // Level + Equipment from shared store (so filter sheet can read/write) - const storeLevel = useExploreFilterStore((s) => s.level) - const storeEquipment = useExploreFilterStore((s) => s.equipment) - const setEquipmentOptions = useExploreFilterStore((s) => s.setEquipmentOptions) - const resetStoreFilters = useExploreFilterStore((s) => s.resetFilters) - - // ── Data ─────────────────────────────────────────────────────────────── - const { data: workoutsData, isLoading: workoutsLoading, isError: workoutsError, refetch: refetchWorkouts } = useWorkouts() - const { data: collectionsData, isLoading: collectionsLoading } = useCollections() - const { data: featuredData } = useFeaturedWorkouts() - const { data: trainersData } = useTrainers() - const history = useActivityStore((s) => s.history) - - const workouts = workoutsData || [] - const collections = collectionsData || [] - const featured = featuredData || [] - const trainers = trainersData || [] - - // ── Analytics ────────────────────────────────────────────────────────── - useEffect(() => { - trackScreen('explore') - }, []) - - // ── Derived: Featured Collection ─────────────────────────────────────── - const featuredCollection = useMemo( - () => collections.find((c) => c.id === FEATURED_COLLECTION_ID), - [collections] - ) - const otherCollections = useMemo( - () => collections.filter((c) => c.id !== FEATURED_COLLECTION_ID), - [collections] - ) - - // ── Derived: Equipment options from data ─────────────────────────────── - const equipmentOptions = useMemo(() => { - const allEquipment = new Set() - workouts.forEach((w) => { - w.equipment.forEach((e) => { - const normalized = e.toLowerCase() - if (normalized.includes('dumbbell')) allEquipment.add('dumbbells') - else if (normalized.includes('band')) allEquipment.add('band') - else if (normalized.includes('mat')) allEquipment.add('mat') - else if (normalized.includes('no equipment') || normalized.includes('optional')) { - // skip — we always show "none" - } else allEquipment.add(normalized) - }) - }) - const options = ['all', 'none', ...Array.from(allEquipment).sort()] - setEquipmentOptions(options) - return options - }, [workouts, setEquipmentOptions]) - - // ── Derived: Search ──────────────────────────────────────────────────── - const searchResults = useMemo(() => { - if (!searchQuery.trim()) return null - const q = searchQuery.toLowerCase().trim() - return workouts.filter((w) => { - if (w.title.toLowerCase().includes(q)) return true - const trainer = getTrainerById(w.trainerId) - if (trainer?.name.toLowerCase().includes(q)) return true - if (w.exercises.some((e) => e.name.toLowerCase().includes(q))) return true - if (w.category.toLowerCase().includes(q)) return true - return false - }) - }, [workouts, searchQuery]) - - const isSearchActive = searchQuery.trim().length > 0 - - // ── Derived: Filtered workouts ───────────────────────────────────────── - const filteredWorkouts = useMemo(() => { - const source = searchResults ?? workouts - return source.filter((workout) => { - if (filters.category !== 'all' && workout.category !== filters.category) return false - if (storeLevel !== 'all' && workout.level !== storeLevel) return false - if (storeEquipment !== 'all') { - if (storeEquipment === 'none') { - return workout.equipment.length === 0 || - workout.equipment.every((e) => e.toLowerCase().includes('no equipment') || e.toLowerCase().includes('optional')) - } - return workout.equipment.some((e) => e.toLowerCase().includes(storeEquipment.toLowerCase())) - } - return true - }) - }, [workouts, searchResults, filters, storeLevel, storeEquipment]) - - // ── Derived: Recommendations ─────────────────────────────────────────── - const recommendedWorkouts = useMemo(() => { - if (history.length === 0) return [] - - // Find categories user has done - const completedWorkoutIds = new Set(history.map((r) => r.workoutId)) - const completedCategories = new Set() - workouts.forEach((w) => { - if (completedWorkoutIds.has(w.id)) completedCategories.add(w.category) - }) - - // Determine user's most common level - const levelCounts: Record = {} - history.forEach((r) => { - const w = workouts.find((wo) => wo.id === r.workoutId) - if (w) levelCounts[w.level] = (levelCounts[w.level] || 0) + 1 - }) - const preferredLevel = Object.entries(levelCounts).sort((a, b) => b[1] - a[1])[0]?.[0] - - // Recommend: workouts NOT yet done, preferring unexplored categories & matching level - return workouts - .filter((w) => !completedWorkoutIds.has(w.id)) - .sort((a, b) => { - // Prioritize unexplored categories - const aExplored = completedCategories.has(a.category) ? 1 : 0 - const bExplored = completedCategories.has(b.category) ? 1 : 0 - if (aExplored !== bExplored) return aExplored - bExplored - // Then matching level - const aLevel = a.level === preferredLevel ? 0 : 1 - const bLevel = b.level === preferredLevel ? 0 : 1 - return aLevel - bLevel - }) - .slice(0, 8) - }, [workouts, history]) - - // ── Active filter count ──────────────────────────────────────────────── - const activeFilterCount = (storeLevel !== 'all' ? 1 : 0) + (storeEquipment !== 'all' ? 1 : 0) - const hasActiveFilters = filters.category !== 'all' || activeFilterCount > 0 - - // ── Handlers ─────────────────────────────────────────────────────────── - const handleWorkoutPress = useCallback((workout: Workout) => { - haptics.buttonTap() - track('explore_workout_tapped', { - workout_id: workout.id, - workout_title: workout.title, - source: isSearchActive ? 'search' : 'browse', - }) - router.push(`/workout/${workout.id}`) - }, [haptics, router, isSearchActive]) - - const handleCollectionPress = useCallback((id: string) => { - haptics.buttonTap() - track('explore_collection_tapped', { collection_id: id }) - router.push(`/collection/${id}` as any) - }, [haptics, router]) - - const handleTrainerPress = useCallback((trainer: Trainer) => { - haptics.buttonTap() - track('explore_trainer_tapped', { trainer_id: trainer.id, trainer_name: trainer.name }) - // Navigate to workouts filtered by this trainer - // For now, filter in place by setting search to trainer name - setSearchQuery(trainer.name) - }, [haptics]) - - const handleFilterSheetPress = useCallback(() => { - haptics.buttonTap() - router.push('/explore-filters' as any) - }, [haptics, router]) - - const handleCategoryChange = useCallback((cat: WorkoutCategory | 'all') => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - setFilters((f) => ({ ...f, category: cat })) - track('explore_filter_applied', { filter_type: 'category', value: cat }) - }, []) - - const clearFilters = useCallback(() => { - haptics.selection() - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) - setFilters({ category: 'all' }) - resetStoreFilters() - }, [haptics, resetStoreFilters]) - - const clearSearch = useCallback(() => { - haptics.selection() - setSearchQuery('') - }, [haptics]) - - const handleSearchChange = useCallback((text: string) => { - setSearchQuery(text) - if (text.length > 2) { - track('explore_search_used', { query_length: text.length }) - } - }, []) - - const handleRetry = useCallback(() => { - haptics.buttonTap() - refetchWorkouts() - }, [haptics, refetchWorkouts]) - - // ── Loading State ────────────────────────────────────────────────────── - if (workoutsLoading || collectionsLoading) { - return ( - - - - - ) - } - - // ── Error State ──────────────────────────────────────────────────────── - if (workoutsError) { - return ( - - - - - {t('screens:explore.errorTitle')} - - - - - {t('screens:explore.errorRetry')} - - - - - ) - } - - return ( - - {/* Ambient gradient glow */} - - - - {/* ── Header ──────────────────────────────────────────────── */} - - - {t('screens:explore.title')} - - - {t('screens:explore.workoutsCount', { count: workouts.length })} - - - - {/* ── Search Bar ──────────────────────────────────────────── */} - - - - - {isSearchActive && ( - - - - )} - - - {/* ── When search is active, show only results ────────────── */} - {isSearchActive ? ( - - - {filteredWorkouts.length > 0 ? ( - - {filteredWorkouts.map((workout) => ( - handleWorkoutPress(workout)} - /> - ))} - - ) : ( - - - - {t('screens:explore.noResults')} - - - {t('screens:explore.tryAdjustingFilters')} - - - )} - - ) : ( - <> - {/* ── Featured Collection Hero ──────────────────────────── */} - {featuredCollection && ( - - - {t('screens:explore.featuredCollection').toUpperCase()} - - handleCollectionPress(featuredCollection.id)} - workoutCountLabel={t('screens:explore.workoutsCount', { count: featuredCollection.workoutIds.length })} - /> - - )} - - {/* ── Trainers ─────────────────────────────────────────── */} - {trainers.length > 0 && ( - - - item.id} - showsHorizontalScrollIndicator={false} - contentContainerStyle={styles.horizontalListContent} - renderItem={({ item }) => ( - handleTrainerPress(item)} - colors={colors} - /> - )} - /> - - )} - - {/* ── Collections Carousel ─────────────────────────────── */} - {otherCollections.length > 0 && ( - - - item.id} - showsHorizontalScrollIndicator={false} - contentContainerStyle={styles.horizontalListContent} - renderItem={({ item }) => ( - - handleCollectionPress(item.id)} - workoutCountLabel={t('screens:explore.workoutsCount', { count: item.workoutIds.length })} - /> - - )} - /> - - )} - - {/* ── Recommended For You ──────────────────────────────── */} - {recommendedWorkouts.length > 0 && ( - - - item.id} - showsHorizontalScrollIndicator={false} - contentContainerStyle={styles.horizontalListContent} - renderItem={({ item }) => ( - - handleWorkoutPress(item)} - /> - - )} - /> - - )} - - {history.length === 0 && ( - - - - - {t('screens:explore.startFirstWorkout')} - - - )} - - {/* ── Featured Workouts ────────────────────────────────── */} - {featured.length > 0 && ( - - - - {featured.slice(0, 4).map((workout) => ( - handleWorkoutPress(workout)} - /> - ))} - - - )} - - {/* ── All Workouts + Filters ───────────────────────────── */} - - {/* Section header with filter button */} - - - {t('screens:explore.allWorkouts')} - - - {hasActiveFilters && ( - - - {t('screens:explore.clearFilters')} - - - )} - 0 && { borderColor: BRAND.PRIMARY }]} - onPress={handleFilterSheetPress} - > - 0 ? BRAND.PRIMARY : colors.text.secondary} /> - {activeFilterCount > 0 && ( - - - {activeFilterCount} - - - )} - - - - - {/* Category pills — single row */} - - {ALL_CATEGORIES.map((cat) => ( - handleCategoryChange(cat)} - colors={colors} - /> - ))} - - - {/* Workout grid */} - {filteredWorkouts.length > 0 ? ( - - {filteredWorkouts.map((workout) => ( - handleWorkoutPress(workout)} - /> - ))} - - ) : ( - - - - {t('screens:explore.noResults')} - - - {t('screens:explore.tryAdjustingFilters')} - - - )} - - - )} - - - ) -} - -// ═══════════════════════════════════════════════════════════════════════════ -// STYLES -// ═══════════════════════════════════════════════════════════════════════════ - -function createStyles(colors: ThemeColors, screenWidth: number) { - const cardWidth = (screenWidth - LAYOUT.SCREEN_PADDING * 2 - SPACING[3]) / 2 - - return StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.bg.base, - }, - scrollView: { - flex: 1, - }, - scrollContent: { - paddingHorizontal: LAYOUT.SCREEN_PADDING, - }, - - // Ambient glow - ambientGlow: { - position: 'absolute', - top: 0, - left: 0, - width: 300, - height: 300, - borderRadius: 150, - }, - - // Header - header: { - marginBottom: SPACING[5], - }, - - // Search - searchContainer: { - flexDirection: 'row', - alignItems: 'center', - height: 44, - borderRadius: RADIUS.LG, - paddingHorizontal: SPACING[3], - gap: SPACING[2], - marginBottom: SPACING[6], - overflow: 'hidden', - borderWidth: 1, - borderColor: colors.border.glass, - borderCurve: 'continuous', - backgroundColor: colors.glass.base.backgroundColor, - }, - searchInput: { - flex: 1, - height: 44, - fontSize: 16, - }, - - // Sections - section: { - marginBottom: SPACING[6], - }, - - // Horizontal lists - horizontalListContent: { - paddingRight: LAYOUT.SCREEN_PADDING, - }, - - // All Workouts header - allWorkoutsHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: SPACING[4], - }, - filterHeaderRight: { - flexDirection: 'row', - alignItems: 'center', - }, - filterIconButton: { - width: 36, - height: 36, - borderRadius: 18, - alignItems: 'center', - justifyContent: 'center', - borderWidth: 1, - borderColor: colors.border.glass, - borderCurve: 'continuous', - backgroundColor: colors.glass.base.backgroundColor, - }, - filterBadge: { - position: 'absolute', - top: -4, - right: -4, - width: 18, - height: 18, - borderRadius: 9, - backgroundColor: BRAND.PRIMARY, - alignItems: 'center', - justifyContent: 'center', - }, - - // Category pills - categoryScroll: { - marginHorizontal: -LAYOUT.SCREEN_PADDING, - marginBottom: SPACING[4], - }, - categoryScrollContent: { - paddingHorizontal: LAYOUT.SCREEN_PADDING, - }, - - // Workout grid - workoutsGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: SPACING[3], - }, - - // No results - noResults: { - alignItems: 'center', - justifyContent: 'center', - paddingVertical: SPACING[10], - }, - - // Recommendation CTA - recommendationCta: { - alignItems: 'center', - justifyContent: 'center', - paddingVertical: SPACING[6], - paddingHorizontal: SPACING[5], - borderRadius: RADIUS.GLASS_CARD, - overflow: 'hidden', - borderWidth: 1, - borderColor: colors.border.glass, - borderCurve: 'continuous', - marginBottom: SPACING[6], - backgroundColor: colors.glass.base.backgroundColor, - }, - - // Error - errorContainer: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - }, - retryButton: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING[2], - marginTop: SPACING[4], - paddingVertical: SPACING[3], - paddingHorizontal: SPACING[5], - borderRadius: RADIUS.FULL, - borderWidth: 1, - borderColor: BRAND.PRIMARY, - borderCurve: 'continuous', - }, - }) -} diff --git a/app/_layout.tsx b/app/_layout.tsx index 7b801f7..af079d2 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -25,6 +25,7 @@ import { import { PostHogProvider } from 'posthog-react-native' import { ThemeProvider, useThemeColors } from '@/src/shared/theme' +import { TEXT, NAVY } from '@/src/shared/constants/colors' import { useUserStore } from '@/src/shared/stores' import { useNotifications } from '@/src/shared/hooks' import { initializePurchases } from '@/src/shared/services/purchases' @@ -120,12 +121,11 @@ function RootLayoutInner() { name="workout/[id]" options={{ headerShown: true, - headerTransparent: true, - headerBlurEffect: colors.colorScheme === 'dark' ? 'dark' : 'light', + headerStyle: { backgroundColor: colors.bg.base }, headerShadowVisible: false, headerTitle: '', headerBackButtonDisplayMode: 'minimal', - headerTintColor: colors.colorScheme === 'dark' ? '#FFFFFF' : '#000000', + headerTintColor: colors.text.primary, animation: 'slide_from_right', }} /> @@ -139,12 +139,11 @@ function RootLayoutInner() { name="program/[id]" options={{ headerShown: true, - headerTransparent: true, - headerBlurEffect: colors.colorScheme === 'dark' ? 'dark' : 'light', + headerStyle: { backgroundColor: colors.bg.base }, headerShadowVisible: false, headerTitle: '', headerBackButtonDisplayMode: 'minimal', - headerTintColor: colors.colorScheme === 'dark' ? '#FFFFFF' : '#000000', + headerTintColor: colors.text.primary, animation: 'slide_from_right', }} /> @@ -173,15 +172,6 @@ function RootLayoutInner() { animation: 'fade', }} /> - diff --git a/app/explore-filters.tsx b/app/explore-filters.tsx deleted file mode 100644 index c66900b..0000000 --- a/app/explore-filters.tsx +++ /dev/null @@ -1,222 +0,0 @@ -/** - * TabataFit Explore Filters Sheet - * Form-sheet modal for Level + Equipment filter selection. - * Reads/writes from useExploreFilterStore. - */ - -import { useCallback } from 'react' -import { - View, - StyleSheet, - Pressable, - Text, -} from 'react-native' -import { useRouter } from 'expo-router' -import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { Icon } from '@/src/shared/components/Icon' -import { useTranslation } from 'react-i18next' - -import { useHaptics } from '@/src/shared/hooks' -import { useExploreFilterStore } from '@/src/shared/stores' -import { StyledText } from '@/src/shared/components/StyledText' -import { useThemeColors, BRAND } from '@/src/shared/theme' -import type { ThemeColors } from '@/src/shared/theme/types' -import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' -import { RADIUS } from '@/src/shared/constants/borderRadius' -import type { WorkoutLevel } from '@/src/shared/types' - -// ═══════════════════════════════════════════════════════════════════════════ -// CONSTANTS -// ═══════════════════════════════════════════════════════════════════════════ - -const ALL_LEVELS: (WorkoutLevel | 'all')[] = ['all', 'Beginner', 'Intermediate', 'Advanced'] - -const LEVEL_TRANSLATION_KEYS: Record = { - all: 'all', - Beginner: 'beginner', - Intermediate: 'intermediate', - Advanced: 'advanced', -} - -const EQUIPMENT_TRANSLATION_KEYS: Record = { - none: 'none', - dumbbells: 'dumbbells', - band: 'band', - mat: 'mat', -} - -// ═══════════════════════════════════════════════════════════════════════════ -// CHOICE CHIP -// ═══════════════════════════════════════════════════════════════════════════ - -function ChoiceChip({ - label, - isSelected, - onPress, - colors, -}: { - label: string - isSelected: boolean - onPress: () => void - colors: ThemeColors -}) { - const haptics = useHaptics() - - return ( - { - haptics.selection() - onPress() - }} - > - {isSelected && ( - - )} - - {label} - - - ) -} - -const chipStyles = StyleSheet.create({ - chip: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: SPACING[4], - paddingVertical: SPACING[3], - borderRadius: RADIUS.LG, - borderWidth: 1, - borderCurve: 'continuous', - marginRight: SPACING[2], - marginBottom: SPACING[2], - }, -}) - -// ═══════════════════════════════════════════════════════════════════════════ -// MAIN SCREEN -// ═══════════════════════════════════════════════════════════════════════════ - -export default function ExploreFiltersScreen() { - const { t } = useTranslation() - const router = useRouter() - const haptics = useHaptics() - const colors = useThemeColors() - const insets = useSafeAreaInsets() - - // ── Store state ──────────────────────────────────────────────────────── - const level = useExploreFilterStore((s) => s.level) - const equipment = useExploreFilterStore((s) => s.equipment) - const equipmentOptions = useExploreFilterStore((s) => s.equipmentOptions) - const setLevel = useExploreFilterStore((s) => s.setLevel) - const setEquipment = useExploreFilterStore((s) => s.setEquipment) - const resetFilters = useExploreFilterStore((s) => s.resetFilters) - - const hasActiveFilters = level !== 'all' || equipment !== 'all' - - // ── Handlers ─────────────────────────────────────────────────────────── - const handleReset = useCallback(() => { - haptics.selection() - resetFilters() - }, [haptics, resetFilters]) - - // ── Equipment label helper ───────────────────────────────────────────── - const getEquipmentLabel = useCallback( - (equip: string) => { - if (equip === 'all') return t('screens:explore.allEquipment') - const key = EQUIPMENT_TRANSLATION_KEYS[equip] - if (key) return t(`screens:explore.equipmentOptions.${key}`) - return equip.charAt(0).toUpperCase() + equip.slice(1) - }, - [t] - ) - - // ── Level label helper ───────────────────────────────────────────────── - const getLevelLabel = useCallback( - (lvl: WorkoutLevel | 'all') => { - if (lvl === 'all') return t('common:categories.all') - const key = LEVEL_TRANSLATION_KEYS[lvl] - return t(`common:levels.${key}`) - }, - [t] - ) - - return ( - - {/* ── Title row ─────────────────────────────────────────────── */} - - - {t('screens:explore.filters')} - - {hasActiveFilters && ( - - - {t('screens:explore.resetFilters')} - - - )} - - - {/* ── Filter sections ───────────────────────────────────────── */} - - {/* Level */} - - {t('screens:explore.filterLevel').toUpperCase()} - - - {ALL_LEVELS.map((lvl) => ( - setLevel(lvl)} - colors={colors} - /> - ))} - - - {/* Equipment */} - - {t('screens:explore.filterEquipment').toUpperCase()} - - - {equipmentOptions.map((equip) => ( - setEquipment(equip)} - colors={colors} - /> - ))} - - - - {/* ── Apply Button ──────────────────────────────────────────── */} - - { - haptics.buttonTap() - router.back() - }} - > - - {t('screens:explore.applyFilters')} - - - - - ) -} diff --git a/src/shared/stores/exploreFilterStore.ts b/src/shared/stores/exploreFilterStore.ts deleted file mode 100644 index 145b29f..0000000 --- a/src/shared/stores/exploreFilterStore.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * TabataFit Explore Filter Store - * Lightweight Zustand store (no persistence) for sharing filter state - * between the Explore screen and the filter sheet modal. - */ - -import { create } from 'zustand' -import type { WorkoutLevel } from '../types' - -interface ExploreFilterState { - level: WorkoutLevel | 'all' - equipment: string | 'all' - /** Derived equipment options from workout data — set once by Explore screen */ - equipmentOptions: string[] - // Actions - setLevel: (level: WorkoutLevel | 'all') => void - setEquipment: (equipment: string | 'all') => void - setEquipmentOptions: (options: string[]) => void - resetFilters: () => void -} - -export const useExploreFilterStore = create()((set) => ({ - level: 'all', - equipment: 'all', - equipmentOptions: [], - setLevel: (level) => set({ level }), - setEquipment: (equipment) => set({ equipment }), - setEquipmentOptions: (equipmentOptions) => set({ equipmentOptions }), - resetFilters: () => set({ level: 'all', equipment: 'all' }), -})) diff --git a/src/shared/stores/index.ts b/src/shared/stores/index.ts index a20eb4d..74cb5e6 100644 --- a/src/shared/stores/index.ts +++ b/src/shared/stores/index.ts @@ -6,4 +6,4 @@ export { useUserStore } from './userStore' export { useActivityStore, getWeeklyActivity } from './activityStore' export { usePlayerStore } from './playerStore' export { useProgramStore } from './programStore' -export { useExploreFilterStore } from './exploreFilterStore' +export { useKineProgramStore } from './kineProgramStore'