diff --git a/app/(tabs)/browse.tsx b/app/(tabs)/browse.tsx index b526e5f..aac955f 100644 --- a/app/(tabs)/browse.tsx +++ b/app/(tabs)/browse.tsx @@ -1,32 +1,32 @@ /** - * TabataFit Browse Screen - * React Native UI — wired to shared data + * TabataFit Browse Screen - Premium Redesign + * React Native UI with glassmorphism */ -import { View, StyleSheet, ScrollView, Pressable, Dimensions, Text as RNText } from 'react-native' +import { View, StyleSheet, ScrollView, Pressable, Dimensions, TextInput } from 'react-native' import { useRouter } from 'expo-router' import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { LinearGradient } from 'expo-linear-gradient' import { BlurView } from 'expo-blur' import Ionicons from '@expo/vector-icons/Ionicons' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useHaptics } from '@/src/shared/hooks' import { COLLECTIONS, - PROGRAMS, getFeaturedCollection, - COLLECTION_COLORS, WORKOUTS, } from '@/src/shared/data' -import { useTranslatedCollections, useTranslatedPrograms, useTranslatedWorkouts } from '@/src/shared/data/useTranslatedData' +import { useTranslatedCollections, useTranslatedWorkouts } from '@/src/shared/data/useTranslatedData' import { StyledText } from '@/src/shared/components/StyledText' +import { WorkoutCard } from '@/src/shared/components/WorkoutCard' +import { CollectionCard } from '@/src/shared/components/CollectionCard' 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 { WorkoutCategory } from '@/src/shared/types' const { width: SCREEN_WIDTH } = Dimensions.get('window') @@ -40,18 +40,14 @@ const FONTS = { CAPTION_2: 11, } -function TextButton({ children, onPress }: { children: string; onPress?: () => void }) { - return ( - - - {children} - - - ) -} - -// New Releases: last 4 workouts -const NEW_RELEASES = WORKOUTS.slice(-4) +const CATEGORIES: { id: WorkoutCategory | 'all'; translationKey: string }[] = [ + { id: 'all', translationKey: 'common:categories.all' }, + { id: 'full-body', translationKey: 'common:categories.fullBody' }, + { id: 'core', translationKey: 'common:categories.core' }, + { id: 'upper-body', translationKey: 'common:categories.upperBody' }, + { id: 'lower-body', translationKey: 'common:categories.lowerBody' }, + { id: 'cardio', translationKey: 'common:categories.cardio' }, +] // ═══════════════════════════════════════════════════════════════════════════ // MAIN SCREEN @@ -65,10 +61,32 @@ export default function BrowseScreen() { const colors = useThemeColors() const styles = useMemo(() => createStyles(colors), [colors]) + const [searchQuery, setSearchQuery] = useState('') + const [selectedCategory, setSelectedCategory] = useState('all') + const featuredCollection = getFeaturedCollection() const translatedCollections = useTranslatedCollections(COLLECTIONS) - const translatedPrograms = useTranslatedPrograms(PROGRAMS) - const translatedNewReleases = useTranslatedWorkouts(NEW_RELEASES) + const translatedWorkouts = useTranslatedWorkouts(WORKOUTS) + + // Filter workouts based on search and category + const filteredWorkouts = useMemo(() => { + let filtered = translatedWorkouts + + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase() + filtered = filtered.filter( + (w) => + w.title.toLowerCase().includes(query) || + w.category.toLowerCase().includes(query) + ) + } + + if (selectedCategory !== 'all') { + filtered = filtered.filter((w) => w.category === selectedCategory) + } + + return filtered + }, [translatedWorkouts, searchQuery, selectedCategory]) const handleWorkoutPress = (id: string) => { haptics.buttonTap() @@ -88,126 +106,151 @@ export default function BrowseScreen() { showsVerticalScrollIndicator={false} > {/* Header */} - {t('screens:browse.title')} + + + {t('screens:browse.title')} + + + + {/* Search Bar */} + + + + + {searchQuery.length > 0 && ( + setSearchQuery('')} + hitSlop={8} + > + + + )} + + + {/* Category Filter Chips */} + + {CATEGORIES.map((cat) => ( + { + haptics.buttonTap() + setSelectedCategory(cat.id) + }} + > + {selectedCategory === cat.id && ( + + )} + + {t(cat.translationKey)} + + + ))} + {/* Featured Collection */} - {featuredCollection && ( - handleCollectionPress(featuredCollection.id)}> - + + + {t('screens:browse.featured')} + + + handleCollectionPress(featuredCollection.id)} /> - - - - {t('screens:browse.featured')} - - - - {t(`content:collections.${featuredCollection.id}.title`, { defaultValue: featuredCollection.title })} - {t(`content:collections.${featuredCollection.id}.description`, { defaultValue: featuredCollection.description })} - - - - - {t('plurals.workout', { count: featuredCollection.workoutIds.length })} - - - - + )} {/* Collections Grid */} - - {t('screens:browse.collections')} - - {translatedCollections.map((collection) => { - const color = COLLECTION_COLORS[collection.id] ?? BRAND.PRIMARY - return ( - + + + {t('screens:browse.collections')} + + + + {translatedCollections.map((collection) => ( + handleCollectionPress(collection.id)} - > - - - {collection.icon} - - - {collection.title} - - - {t('plurals.workout', { count: collection.workoutIds.length })} - - - ) - })} + /> + ))} + - + )} - {/* Programs */} + {/* All Workouts Grid */} - {t('screens:browse.programs')} - {t('seeAll')} + + {searchQuery ? t('screens:browse.searchResults') || 'Results' : t('screens:browse.allWorkouts') || 'All Workouts'} + + + {filteredWorkouts.length} {t('plurals.workout', { count: filteredWorkouts.length })} + - - {translatedPrograms.map((program) => ( - - - - - {t(`levels.${program.level.toLowerCase()}`)} - - - - {program.title} - - - - - {t('screens:browse.weeksCount', { count: program.weeks })} - - - - {t('screens:browse.timesPerWeek', { count: program.workoutsPerWeek })} - - - - ))} - - - - {/* New Releases */} - - - {t('screens:browse.newReleases')} - - {translatedNewReleases.map((workout) => ( - handleWorkoutPress(workout.id)} - > - - - - - {workout.title} - - {t('durationLevel', { duration: workout.duration, level: t(`levels.${workout.level.toLowerCase()}`) })} - - - - - ))} + {filteredWorkouts.length > 0 ? ( + + {filteredWorkouts.map((workout) => ( + handleWorkoutPress(workout.id)} + /> + ))} + + ) : ( + + + + {t('screens:browse.noResults') || 'No workouts found'} + + + {t('screens:browse.tryDifferentSearch') || 'Try a different search or category'} + + + )} @@ -218,8 +261,6 @@ export default function BrowseScreen() { // STYLES // ═══════════════════════════════════════════════════════════════════════════ -const COLLECTION_CARD_WIDTH = (SCREEN_WIDTH - LAYOUT.SCREEN_PADDING * 2 - SPACING[3]) / 2 - function createStyles(colors: ThemeColors) { return StyleSheet.create({ container: { @@ -233,52 +274,57 @@ function createStyles(colors: ThemeColors) { paddingHorizontal: LAYOUT.SCREEN_PADDING, }, - // Featured Collection - featuredCard: { - height: 200, - borderRadius: RADIUS.GLASS_CARD, - overflow: 'hidden', - marginBottom: SPACING[8], - marginTop: SPACING[4], - ...colors.shadow.lg, - }, - featuredBadge: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.3)', - paddingHorizontal: SPACING[2], - paddingVertical: SPACING[1], - borderRadius: RADIUS.SM, - alignSelf: 'flex-start', - margin: SPACING[4], - gap: SPACING[1], - }, - featuredBadgeText: { - fontSize: 11, - fontWeight: 'bold', - color: '#FFFFFF', - }, - featuredInfo: { - position: 'absolute', - bottom: SPACING[5], - left: SPACING[5], - right: SPACING[5], - }, - featuredStats: { - flexDirection: 'row', - gap: SPACING[4], - marginTop: SPACING[3], - }, - featuredStat: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING[1], + // Header + header: { + marginBottom: SPACING[4], }, - // Section - section: { + // Search Bar + searchContainer: { + flexDirection: 'row', + alignItems: 'center', + height: 48, + borderRadius: RADIUS.LG, + borderWidth: 1, + borderColor: colors.border.glass, + paddingHorizontal: SPACING[4], + marginBottom: SPACING[4], + overflow: 'hidden', + }, + searchInput: { + flex: 1, + marginLeft: SPACING[3], + marginRight: SPACING[2], + fontSize: FONTS.HEADLINE, + color: colors.text.primary, + height: '100%', + }, + + // Categories + categoriesContainer: { marginBottom: SPACING[6], }, + categoriesScroll: { + gap: SPACING[2], + paddingRight: SPACING[4], + }, + categoryChip: { + paddingHorizontal: SPACING[4], + paddingVertical: SPACING[2], + borderRadius: RADIUS.FULL, + overflow: 'hidden', + borderWidth: 1, + borderColor: colors.border.glass, + }, + categoryChipActive: { + borderColor: BRAND.PRIMARY, + backgroundColor: `${BRAND.PRIMARY}30`, + }, + + // Sections + section: { + marginBottom: SPACING[8], + }, sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', @@ -286,96 +332,25 @@ function createStyles(colors: ThemeColors) { marginBottom: SPACING[4], }, - // Collections Grid + // Collections collectionsGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING[3], - marginTop: SPACING[3], - }, - collectionCard: { - width: COLLECTION_CARD_WIDTH, - paddingVertical: SPACING[4], - paddingHorizontal: SPACING[3], - borderRadius: RADIUS.LG, - overflow: 'hidden', - borderWidth: 1, - borderColor: colors.border.glass, - gap: SPACING[1], - }, - collectionIconBg: { - width: 44, - height: 44, - borderRadius: 12, - alignItems: 'center', - justifyContent: 'center', - marginBottom: SPACING[2], - }, - collectionEmoji: { - fontSize: 22, }, - // Programs - programsScroll: { + // Workouts Grid + workoutsGrid: { + flexDirection: 'row', + flexWrap: 'wrap', gap: SPACING[3], }, - programCard: { - width: 200, - height: 140, - borderRadius: RADIUS.LG, - overflow: 'hidden', - padding: SPACING[4], - borderWidth: 1, - borderColor: colors.border.glass, - }, - programHeader: { - flexDirection: 'row', - justifyContent: 'flex-end', - marginBottom: SPACING[2], - }, - programLevelBadge: { - backgroundColor: 'rgba(255, 107, 53, 0.2)', - paddingHorizontal: SPACING[2], - paddingVertical: SPACING[1], - borderRadius: RADIUS.SM, - }, - programMeta: { - flexDirection: 'row', - gap: SPACING[3], - marginTop: SPACING[3], - }, - programMetaItem: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING[1], - }, - // New Releases - releaseRow: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: SPACING[3], - paddingHorizontal: SPACING[4], - backgroundColor: colors.bg.surface, - borderRadius: RADIUS.LG, - marginBottom: SPACING[2], - gap: SPACING[3], - }, - releaseAvatar: { - width: 44, - height: 44, - borderRadius: 22, + // Empty State + emptyState: { alignItems: 'center', justifyContent: 'center', - }, - releaseInitial: { - fontSize: 18, - fontWeight: '700', - color: '#FFFFFF', - }, - releaseInfo: { - flex: 1, - gap: 2, + paddingVertical: SPACING[12], }, }) } diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 91bbc31..363d36f 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,16 +1,16 @@ /** - * TabataFit Home Screen - * React Native UI — wired to shared data + * TabataFit Home Screen - Premium Redesign + * React Native UI with glassmorphism */ -import { View, StyleSheet, ScrollView, Pressable, Dimensions, Text as RNText, Image as RNImage } from 'react-native' +import { View, StyleSheet, ScrollView, Pressable, Dimensions, Text as RNText } from 'react-native' import { useRouter } from 'expo-router' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { LinearGradient } from 'expo-linear-gradient' import { BlurView } from 'expo-blur' import Ionicons from '@expo/vector-icons/Ionicons' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useHaptics } from '@/src/shared/hooks' import { useUserStore, useActivityStore } from '@/src/shared/stores' @@ -22,50 +22,35 @@ import { } from '@/src/shared/data' import { useTranslatedWorkouts, useTranslatedCollections } from '@/src/shared/data/useTranslatedData' import { StyledText } from '@/src/shared/components/StyledText' +import { WorkoutCard } from '@/src/shared/components/WorkoutCard' +import { CollectionCard } from '@/src/shared/components/CollectionCard' import { useThemeColors, BRAND, GRADIENTS } 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 { WorkoutCategory } from '@/src/shared/types' const { width: SCREEN_WIDTH } = Dimensions.get('window') -const CARD_WIDTH = SCREEN_WIDTH - LAYOUT.SCREEN_PADDING * 2 const FONTS = { LARGE_TITLE: 34, TITLE: 28, TITLE_2: 22, HEADLINE: 17, - BODY: 17, SUBHEADLINE: 15, CAPTION_1: 12, CAPTION_2: 11, } -// ═══════════════════════════════════════════════════════════════════════════ -// HELPERS -// ═══════════════════════════════════════════════════════════════════════════ - -function PrimaryButton({ children, onPress }: { children: string; onPress?: () => void }) { - const colors = useThemeColors() - const styles = useMemo(() => createStyles(colors), [colors]) - return ( - - - {children} - - ) -} - -function PlainButton({ children, onPress }: { children: string; onPress?: () => void }) { - const colors = useThemeColors() - const styles = useMemo(() => createStyles(colors), [colors]) - return ( - - {children} - - ) -} +const CATEGORIES: { id: WorkoutCategory | 'all'; key: string }[] = [ + { id: 'all', key: 'all' }, + { id: 'full-body', key: 'fullBody' }, + { id: 'core', key: 'core' }, + { id: 'upper-body', key: 'upperBody' }, + { id: 'lower-body', key: 'lowerBody' }, + { id: 'cardio', key: 'cardio' }, +] // ═══════════════════════════════════════════════════════════════════════════ // MAIN SCREEN @@ -82,8 +67,10 @@ export default function HomeScreen() { const history = useActivityStore((s) => s.history) const recentWorkouts = useMemo(() => history.slice(0, 3), [history]) + const [selectedCategory, setSelectedCategory] = useState('all') + const featured = getFeaturedWorkouts()[0] ?? WORKOUTS[0] - const popular = getPopularWorkouts(4) + const popular = getPopularWorkouts(6) const translatedPopular = useTranslatedWorkouts(popular) const translatedCollections = useTranslatedCollections(COLLECTIONS) @@ -94,13 +81,21 @@ export default function HomeScreen() { return t('greetings.evening') })() - const featuredTitle = t(`content:workouts.${featured.id}`, { defaultValue: featured.title }) - const handleWorkoutPress = (id: string) => { haptics.buttonTap() router.push(`/workout/${id}`) } + const handleCollectionPress = (id: string) => { + haptics.buttonTap() + router.push(`/collection/${id}`) + } + + const filteredWorkouts = useMemo(() => { + if (selectedCategory === 'all') return translatedPopular + return translatedPopular.filter((w) => w.category === selectedCategory) + }, [translatedPopular, selectedCategory]) + return ( - {/* Header */} - - - - {greeting} - + {/* Hero Section */} + + + {greeting} + + {userName} + + + - - - - {/* Featured */} - handleWorkoutPress(featured.id)} - > - {featured.thumbnailUrl ? ( - - ) : ( - - )} - - - - {'🔥 ' + t('screens:home.featured')} - - - - - {featuredTitle} - - - {t('workoutMeta', { duration: featured.duration, level: t(`levels.${featured.level.toLowerCase()}`), calories: featured.calories })} - - - - handleWorkoutPress(featured.id)}>{t('start')} - - - - - - - - {/* Continue Watching — from activity store */} - {recentWorkouts.length > 0 && ( - - - {t('screens:home.recent')} - {t('seeAll')} - - - {recentWorkouts.map((result) => { - const workout = WORKOUTS.find(w => w.id === result.workoutId) - if (!workout) return null - const workoutTitle = t(`content:workouts.${workout.id}`, { defaultValue: workout.title }) - return ( - handleWorkoutPress(result.workoutId)} - > - - - - - {workoutTitle} - - {t('calMin', { calories: result.calories, duration: result.durationMinutes })} - - - ) - })} - - - )} - - {/* Popular This Week */} + {/* Featured Workout */} + + + + {t('screens:home.featured')} + + + handleWorkoutPress(featured.id)} + /> + + + {/* Category Filter */} - {t('screens:home.popularThisWeek')} - {translatedPopular.map((item) => ( + {CATEGORIES.map((cat) => ( handleWorkoutPress(item.id)} + key={cat.id} + style={[ + styles.categoryChip, + selectedCategory === cat.id && styles.categoryChipActive, + ]} + onPress={() => { + haptics.buttonTap() + setSelectedCategory(cat.id) + }} > - - - - {item.title} - {t('units.minUnit', { count: item.duration })} + {selectedCategory === cat.id && ( + + )} + + {t(`categories.${cat.key}`)} + ))} - {/* Collections */} + {/* Popular Workouts - Horizontal */} + {filteredWorkouts.length > 0 && ( + + + + {t('screens:home.popularThisWeek')} + + + + {t('seeAll')} + + + + + {filteredWorkouts.map((workout) => ( + handleWorkoutPress(workout.id)} + /> + ))} + + + )} + + {/* Collections Grid */} - {t('screens:home.collections')} - {translatedCollections.map((item) => ( - { haptics.buttonTap(); router.push(`/collection/${item.id}`) }}> - - - {item.icon} - - {item.title} - - {t('plurals.workout', { count: item.workoutIds.length }) + ' \u00B7 ' + item.description} - - - - - - ))} + + + {t('screens:home.collections')} + + + + {translatedCollections.map((collection) => ( + handleCollectionPress(collection.id)} + /> + ))} + @@ -276,89 +244,25 @@ function createStyles(colors: ThemeColors) { paddingHorizontal: LAYOUT.SCREEN_PADDING, }, - // Header - header: { + // Hero Section + heroSection: { + marginBottom: SPACING[6], + }, + heroHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - marginBottom: SPACING[6], + marginTop: SPACING[2], + }, + heroTitle: { + flex: 1, + marginRight: SPACING[3], }, profileButton: { - width: 40, - height: 40, - alignItems: 'center', - justifyContent: 'center', - }, - - // Buttons - primaryButton: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: BRAND.PRIMARY, - paddingHorizontal: SPACING[4], - paddingVertical: SPACING[3], - borderRadius: RADIUS.SM, - }, - buttonIcon: { - marginRight: SPACING[2], - }, - primaryButtonText: { - fontSize: 14, - fontWeight: '600', - color: '#FFFFFF', - }, - plainButtonText: { - fontSize: FONTS.BODY, - color: BRAND.PRIMARY, - }, - - // Featured - featuredCard: { - width: CARD_WIDTH, - height: 220, - borderRadius: RADIUS.GLASS_CARD, - overflow: 'hidden', - marginBottom: SPACING[8], - ...colors.shadow.lg, - }, - featuredBadge: { - position: 'absolute', - top: SPACING[4], - left: SPACING[4], - backgroundColor: 'rgba(255, 255, 255, 0.15)', - paddingHorizontal: SPACING[3], - paddingVertical: SPACING[1], - borderRadius: RADIUS.SM, - borderWidth: 1, - borderColor: 'rgba(255, 255, 255, 0.2)', - }, - featuredBadgeText: { - fontSize: 11, - fontWeight: 'bold', - color: '#FFFFFF', - }, - featuredContent: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - padding: SPACING[5], - }, - featuredButtons: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING[3], - marginTop: SPACING[4], - }, - saveButton: { width: 44, height: 44, alignItems: 'center', justifyContent: 'center', - backgroundColor: 'rgba(255, 255, 255, 0.1)', - borderRadius: 22, - borderWidth: 1, - borderColor: 'rgba(255, 255, 255, 0.15)', }, // Sections @@ -371,61 +275,36 @@ function createStyles(colors: ThemeColors) { alignItems: 'center', marginBottom: SPACING[4], }, - horizontalScroll: { + + // Categories + categoriesScroll: { + gap: SPACING[2], + paddingRight: SPACING[4], + }, + categoryChip: { + paddingHorizontal: SPACING[4], + paddingVertical: SPACING[2], + borderRadius: RADIUS.FULL, + overflow: 'hidden', + borderWidth: 1, + borderColor: colors.border.glass, + }, + categoryChipActive: { + borderColor: BRAND.PRIMARY, + backgroundColor: `${BRAND.PRIMARY}30`, + }, + + // Workouts Scroll + workoutsScroll: { gap: SPACING[3], + paddingRight: SPACING[4], }, - // Continue Card - continueCard: { - width: 140, - }, - continueThumb: { - width: 140, - height: 200, - borderRadius: RADIUS.LG, - overflow: 'hidden', - alignItems: 'center', - justifyContent: 'center', - marginBottom: SPACING[2], - }, - - // Popular Card - popularCard: { - width: 120, - }, - popularThumb: { - width: 120, - height: 120, - borderRadius: RADIUS.LG, - alignItems: 'center', - justifyContent: 'center', - marginBottom: SPACING[2], - borderWidth: 1, - borderColor: colors.border.glass, - backgroundColor: colors.bg.overlay1, - }, - - // Collection Card - collectionCard: { - height: 80, - borderRadius: RADIUS.LG, - overflow: 'hidden', - marginBottom: SPACING[3], - borderWidth: 1, - borderColor: colors.border.glass, - }, - collectionContent: { - flex: 1, + // Collections Grid + collectionsGrid: { flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: SPACING[5], - }, - collectionIcon: { - fontSize: 28, - marginRight: SPACING[4], - }, - collectionText: { - flex: 1, + flexWrap: 'wrap', + gap: SPACING[3], }, }) } diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index 4639f36..46d781f 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -1,40 +1,62 @@ /** - * TabataFit Profile Screen - * SwiftUI-first settings with native iOS look + * TabataFit Profile Screen — Premium React Native + * Apple Fitness+ inspired design, pure React Native components */ -import { View, StyleSheet, ScrollView } from 'react-native' import { useRouter } from 'expo-router' -import { useSafeAreaInsets } from 'react-native-safe-area-context' import { - Host, - List, - Section, + View, + ScrollView, + StyleSheet, + TouchableOpacity, Switch, - LabeledContent, - DateTimePicker, - Button, - VStack, - Text, -} from '@expo/ui/swift-ui' -import { useMemo } from 'react' + Text as RNText, + TextStyle, +} from 'react-native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import * as Linking from 'expo-linking' +import Constants from 'expo-constants' import { useTranslation } from 'react-i18next' +import { useMemo } from 'react' import { useUserStore } from '@/src/shared/stores' import { requestNotificationPermissions, usePurchases } from '@/src/shared/hooks' -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' -const FONTS = { - LARGE_TITLE: 34, - TITLE_2: 22, - CAPTION_1: 12, +// ═══════════════════════════════════════════════════════════════════════════ +// STYLED TEXT COMPONENT +// ═══════════════════════════════════════════════════════════════════════════ + +interface TextProps { + children: React.ReactNode + style?: TextStyle + size?: number + weight?: 'normal' | 'bold' | '600' | '700' | '800' | '900' + color?: string + center?: boolean +} + +function Text({ children, style, size, weight, color, center }: TextProps) { + const colors = useThemeColors() + return ( + + {children} + + ) } // ═══════════════════════════════════════════════════════════════════════════ -// MAIN SCREEN +// COMPONENT: PROFILE SCREEN // ═══════════════════════════════════════════════════════════════════════════ export default function ProfileScreen() { @@ -46,10 +68,28 @@ export default function ProfileScreen() { const profile = useUserStore((s) => s.profile) const settings = useUserStore((s) => s.settings) const updateSettings = useUserStore((s) => s.updateSettings) - const { restorePurchases } = usePurchases() + const updateProfile = useUserStore((s) => s.updateProfile) + const { restorePurchases, isPremium } = usePurchases() - const isPremium = profile.subscription !== 'free' const planLabel = isPremium ? 'TabataFit+' : t('profile.freePlan') + const avatarInitial = profile.name?.[0]?.toUpperCase() || 'U' + + // Mock stats (replace with real data from activityStore when available) + const stats = { + workouts: 47, + streak: 12, + calories: 12500, + } + + const handleSignOut = () => { + updateProfile({ + name: '', + email: '', + subscription: 'free', + onboardingCompleted: false, + }) + router.replace('/onboarding') + } const handleRestore = async () => { await restorePurchases() @@ -63,43 +103,24 @@ export default function ProfileScreen() { updateSettings({ reminders: enabled }) } - const handleTimeChange = (date: Date) => { - const hh = String(date.getHours()).padStart(2, '0') - const mm = String(date.getMinutes()).padStart(2, '0') - updateSettings({ reminderTime: `${hh}:${mm}` }) + const handleRateApp = () => { + Linking.openURL('https://apps.apple.com/app/tabatafit/id1234567890') } - // Build initial date string for the picker (today at reminderTime) - const today = new Date() - const [rh, rm] = settings.reminderTime.split(':').map(Number) - const pickerDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), rh, rm) - const pickerInitial = pickerDate.toISOString() + const handleContactUs = () => { + Linking.openURL('mailto:contact@tabatafit.app') + } - // Calculate total height for single SwiftUI island - // insetGrouped style: ~50px top/bottom margins, section header ~35px, row ~44px - const basePadding = 100 // top + bottom margins for insetGrouped + const handlePrivacyPolicy = () => { + router.push('/privacy') + } - // Account section - const accountRows = 1 + (isPremium ? 1 : 0) // plan, [+ restore] - const accountHeight = 35 + accountRows * 44 + const handleFAQ = () => { + Linking.openURL('https://tabatafit.app/faq') + } - // Upgrade section (free users only) - const upgradeHeight = isPremium ? 0 : 35 + 80 // header + VStack content - - // Workout section - const workoutHeight = 35 + 3 * 44 // haptics, sound, voice - - // Notifications section - const notificationRows = settings.reminders ? 2 : 1 - const notificationHeight = 35 + notificationRows * 44 - - // About section - const aboutHeight = 35 + 2 * 44 // version, privacy - - // Sign out section - const signOutHeight = 44 // single button row - - const totalHeight = basePadding + accountHeight + upgradeHeight + workoutHeight + notificationHeight + aboutHeight + signOutHeight + // App version + const appVersion = Constants.expoConfig?.version ?? '1.0.0' return ( @@ -108,125 +129,195 @@ export default function ProfileScreen() { contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 100 }]} showsVerticalScrollIndicator={false} > - {/* Header */} - - {t('profile.title')} - - - {/* Profile Header Card */} - + {/* ════════════════════════════════════════════════════════════════════ + PROFILE HEADER CARD + ═══════════════════════════════════════════════════════════════════ */} + + + {/* Avatar with gradient background */} - - {profile.name?.[0] || '?'} - + + {avatarInitial} + - - + + {/* Name & Plan */} + + {profile.name || t('profile.guest')} - - {isPremium && ( - - - {planLabel} - - - )} + + + + {planLabel} + + {isPremium && ( + + ✓ + + )} + + + + {/* Stats Row */} + + + + 🔥 {stats.workouts} + + + {t('profile.statsWorkouts')} + + + + + 📅 {stats.streak} + + + {t('profile.statsStreak')} + + + + + ⚡️ {Math.round(stats.calories / 1000)}k + + + {t('profile.statsCalories')} + + + - {/* All Settings in Single SwiftUI Island */} - - - {/* Account Section */} -
- - {planLabel} - - {isPremium && ( - - )} -
+ {/* ════════════════════════════════════════════════════════════════════ + UPGRADE CTA (FREE USERS ONLY) + ═══════════════════════════════════════════════════════════════════ */} + {!isPremium && ( + + router.push('/paywall')} + > + + + ✨ {t('profile.upgradeTitle')} + + + {t('profile.upgradeDescription')} + + + + {t('profile.learnMore')} → + + + + )} - {/* Upgrade CTA for Free Users */} - {!isPremium && ( -
- - - {t('profile.upgradeTitle')} - - - {t('profile.upgradeDescription')} - - - -
- )} + {/* ════════════════════════════════════════════════════════════════════ + WORKOUT SETTINGS + ═══════════════════════════════════════════════════════════════════ */} + {t('profile.sectionWorkout')} + + + {t('profile.hapticFeedback')} + updateSettings({ haptics: v })} + trackColor={{ false: colors.bg.overlay1, true: BRAND.PRIMARY }} + thumbColor="#FFFFFF" + /> + + + {t('profile.soundEffects')} + updateSettings({ soundEffects: v })} + trackColor={{ false: colors.bg.overlay1, true: BRAND.PRIMARY }} + thumbColor="#FFFFFF" + /> + + + {t('profile.voiceCoaching')} + updateSettings({ voiceCoaching: v })} + trackColor={{ false: colors.bg.overlay1, true: BRAND.PRIMARY }} + thumbColor="#FFFFFF" + /> + + - {/* Workout Settings */} -
- updateSettings({ haptics: v })} - color={BRAND.PRIMARY} - /> - updateSettings({ soundEffects: v })} - color={BRAND.PRIMARY} - /> - updateSettings({ voiceCoaching: v })} - color={BRAND.PRIMARY} - /> -
+ {/* ════════════════════════════════════════════════════════════════════ + NOTIFICATIONS + ═══════════════════════════════════════════════════════════════════ */} + {t('profile.sectionNotifications')} + + + {t('profile.dailyReminders')} + + + {settings.reminders && ( + + {t('profile.reminderTime')} + {settings.reminderTime} + + )} + - {/* Notification Settings */} -
- - {settings.reminders && ( - - - - )} -
+ {/* ════════════════════════════════════════════════════════════════════ + ABOUT + ═══════════════════════════════════════════════════════════════════ */} + {t('profile.sectionAbout')} + + + {t('profile.version')} + {appVersion} + + + {t('profile.rateApp')} + + + + {t('profile.contactUs')} + + + + {t('profile.faq')} + + + + {t('profile.privacyPolicy')} + + + - {/* About Section */} -
- - 1.0.0 - - -
+ {/* ════════════════════════════════════════════════════════════════════ + ACCOUNT (PREMIUM USERS ONLY) + ═══════════════════════════════════════════════════════════════════ */} + {isPremium && ( + <> + {t('profile.sectionAccount')} + + + {t('profile.restorePurchases')} + + + + + )} - {/* Sign Out */} -
- -
-
-
+ {/* ════════════════════════════════════════════════════════════════════ + SIGN OUT + ═══════════════════════════════════════════════════════════════════ */} + + + {t('profile.signOut')} + +
) @@ -246,34 +337,107 @@ function createStyles(colors: ThemeColors) { flex: 1, }, scrollContent: { - paddingHorizontal: LAYOUT.SCREEN_PADDING, + flexGrow: 1, }, - - // Profile Header - profileHeader: { - flexDirection: 'row', + section: { + marginHorizontal: 16, + marginTop: 20, + backgroundColor: colors.bg.surface, + borderRadius: 10, + overflow: 'hidden', + }, + sectionHeader: { + fontSize: 13, + fontWeight: '600', + color: colors.text.tertiary, + textTransform: 'uppercase', + marginLeft: 32, + marginTop: 20, + marginBottom: 8, + }, + headerContainer: { alignItems: 'center', - paddingVertical: SPACING[5], - gap: SPACING[4], + paddingVertical: 24, + paddingHorizontal: 16, }, avatarContainer: { - width: 60, - height: 60, - borderRadius: 30, + width: 90, + height: 90, + borderRadius: 45, backgroundColor: BRAND.PRIMARY, - alignItems: 'center', justifyContent: 'center', + alignItems: 'center', + shadowColor: BRAND.PRIMARY, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.5, + shadowRadius: 20, + elevation: 10, }, - profileInfo: { - flex: 1, + nameContainer: { + marginTop: 16, + alignItems: 'center', }, - premiumBadge: { - backgroundColor: 'rgba(255, 107, 53, 0.15)', - paddingHorizontal: SPACING[3], - paddingVertical: SPACING[1], - borderRadius: 12, - alignSelf: 'flex-start', - marginTop: SPACING[1], + planContainer: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 4, + gap: 4, + }, + statsContainer: { + flexDirection: 'row', + justifyContent: 'center', + marginTop: 16, + gap: 32, + }, + statItem: { + alignItems: 'center', + }, + premiumContainer: { + paddingVertical: 16, + paddingHorizontal: 16, + }, + premiumContent: { + gap: 4, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 12, + paddingHorizontal: 16, + borderBottomWidth: 0.5, + borderBottomColor: colors.border.glassLight, + }, + rowLast: { + borderBottomWidth: 0, + }, + rowLabel: { + fontSize: 17, + color: colors.text.primary, + }, + rowValue: { + fontSize: 17, + color: colors.text.tertiary, + }, + rowTime: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 12, + paddingHorizontal: 16, + borderTopWidth: 0.5, + borderTopColor: colors.border.glassLight, + }, + button: { + paddingVertical: 14, + alignItems: 'center', + }, + destructive: { + fontSize: 17, + color: BRAND.DANGER, + }, + signOutSection: { + marginTop: 20, }, }) } diff --git a/app/paywall.tsx b/app/paywall.tsx index d808a28..66c3d44 100644 --- a/app/paywall.tsx +++ b/app/paywall.tsx @@ -3,7 +3,7 @@ * Premium subscription purchase flow */ -import React from 'react' +import React, { useMemo } from 'react' import { View, StyleSheet, @@ -18,7 +18,8 @@ import Ionicons from '@expo/vector-icons/Ionicons' import { useTranslation } from 'react-i18next' import { useHaptics, usePurchases } from '@/src/shared/hooks' -import { BRAND, darkColors } from '@/src/shared/theme' +import { useThemeColors, BRAND, GRADIENTS } from '@/src/shared/theme' +import type { ThemeColors } from '@/src/shared/theme/types' import { SPACING } from '@/src/shared/constants/spacing' import { RADIUS } from '@/src/shared/constants/borderRadius' @@ -39,6 +40,18 @@ const PREMIUM_FEATURES = [ // COMPONENTS // ═══════════════════════════════════════════════════════════════════════════ +interface PlanCardStyles { + planCard: object + planCardPressed: object + savingsBadge: object + savingsText: object + planInfo: object + planTitle: object + planPeriod: object + planPrice: object + checkmark: object +} + function PlanCard({ title, price, @@ -46,6 +59,8 @@ function PlanCard({ savings, isSelected, onPress, + colors, + styles, }: { title: string price: string @@ -53,6 +68,8 @@ function PlanCard({ savings?: string isSelected: boolean onPress: () => void + colors: ThemeColors + styles: PlanCardStyles }) { const haptics = useHaptics() @@ -66,8 +83,12 @@ function PlanCard({ onPress={handlePress} style={({ pressed }) => [ styles.planCard, - isSelected && styles.planCardSelected, + isSelected && { borderColor: BRAND.PRIMARY }, pressed && styles.planCardPressed, + { + backgroundColor: colors.bg.surface, + borderColor: isSelected ? BRAND.PRIMARY : colors.border.glass, + }, ]} > {savings && ( @@ -76,10 +97,16 @@ function PlanCard({
)} - {title} - {period} + + {title} + + + {period} + - {price} + + {price} + {isSelected && ( @@ -98,6 +125,25 @@ export default function PaywallScreen() { const router = useRouter() const insets = useSafeAreaInsets() const haptics = useHaptics() + const colors = useThemeColors() + const styles = useMemo(() => createStyles(colors), [colors]) + + // Extract plan card styles for the child component + const planCardStyles = useMemo( + () => ({ + planCard: styles.planCard, + planCardPressed: styles.planCardPressed, + savingsBadge: styles.savingsBadge, + savingsText: styles.savingsText, + planInfo: styles.planInfo, + planTitle: styles.planTitle, + planPeriod: styles.planPeriod, + planPrice: styles.planPrice, + checkmark: styles.checkmark, + }), + [styles], + ) + const { monthlyPackage, annualPackage, @@ -148,15 +194,9 @@ export default function PaywallScreen() { return ( - {/* Background Gradient */} - - {/* Close Button */} - - + + {PREMIUM_FEATURES.map((feature) => ( - + - + {t(`paywall.features.${feature.key}`)} @@ -196,6 +236,8 @@ export default function PaywallScreen() { savings={t('paywall.save50')} isSelected={selectedPlan === 'annual'} onPress={() => setSelectedPlan('annual')} + colors={colors} + styles={planCardStyles} /> setSelectedPlan('monthly')} + colors={colors} + styles={planCardStyles} /> {/* Price Note */} {selectedPlan === 'annual' && ( - + {t('paywall.equivalent', { price: annualMonthlyEquivalent })} )} @@ -220,12 +264,12 @@ export default function PaywallScreen() { disabled={isLoading} > - + {isLoading ? t('paywall.processing') : t('paywall.subscribe')} @@ -234,10 +278,14 @@ export default function PaywallScreen() { {/* Restore & Terms */} - {t('paywall.restore')} + + {t('paywall.restore')} + - {t('paywall.terms')} + + {t('paywall.terms')} + @@ -248,162 +296,147 @@ export default function PaywallScreen() { // STYLES // ═══════════════════════════════════════════════════════════════════════════ -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - gradient: { - ...StyleSheet.absoluteFillObject, - }, - closeButton: { - position: 'absolute', - top: SPACING[4], - right: SPACING[4], - width: 44, - height: 44, - alignItems: 'center', - justifyContent: 'center', - zIndex: 10, - }, - scrollView: { - flex: 1, - }, - scrollContent: { - paddingHorizontal: SPACING[5], - paddingTop: SPACING[8], - }, - header: { - alignItems: 'center', - }, - title: { - fontSize: 32, - fontWeight: '700', - color: '#FFF', - textAlign: 'center', - }, - subtitle: { - fontSize: 16, - color: darkColors.text.secondary, - textAlign: 'center', - marginTop: SPACING[2], - }, - featuresGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - marginTop: SPACING[6], - marginHorizontal: -SPACING[2], - }, - featureItem: { - width: '33%', - alignItems: 'center', - paddingVertical: SPACING[3], - }, - featureIcon: { - width: 48, - height: 48, - borderRadius: 24, - backgroundColor: 'rgba(255, 107, 53, 0.15)', - alignItems: 'center', - justifyContent: 'center', - marginBottom: SPACING[2], - }, - featureText: { - fontSize: 13, - color: darkColors.text.secondary, - textAlign: 'center', - }, - plansContainer: { - marginTop: SPACING[6], - gap: SPACING[3], - }, - planCard: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: 'rgba(255, 255, 255, 0.08)', - borderRadius: RADIUS.LG, - padding: SPACING[4], - borderWidth: 2, - borderColor: 'transparent', - }, - planCardSelected: { - borderColor: BRAND.PRIMARY, - backgroundColor: 'rgba(255, 107, 53, 0.1)', - }, - planCardPressed: { - opacity: 0.8, - }, - savingsBadge: { - position: 'absolute', - top: -8, - right: SPACING[3], - backgroundColor: BRAND.PRIMARY, - paddingHorizontal: SPACING[2], - paddingVertical: 2, - borderRadius: RADIUS.SM, - }, - savingsText: { - fontSize: 10, - fontWeight: '700', - color: '#FFF', - }, - planInfo: { - flex: 1, - }, - planTitle: { - fontSize: 16, - fontWeight: '600', - color: darkColors.text.primary, - }, - planPeriod: { - fontSize: 13, - color: darkColors.text.tertiary, - marginTop: 2, - }, - planPrice: { - fontSize: 20, - fontWeight: '700', - color: BRAND.PRIMARY, - }, - checkmark: { - marginLeft: SPACING[2], - }, - priceNote: { - fontSize: 13, - color: darkColors.text.tertiary, - textAlign: 'center', - marginTop: SPACING[3], - }, - ctaButton: { - borderRadius: RADIUS.LG, - overflow: 'hidden', - marginTop: SPACING[6], - }, - ctaButtonDisabled: { - opacity: 0.6, - }, - ctaGradient: { - paddingVertical: SPACING[4], - alignItems: 'center', - }, - ctaText: { - fontSize: 17, - fontWeight: '600', - color: '#FFF', - }, - footer: { - marginTop: SPACING[5], - alignItems: 'center', - gap: SPACING[4], - }, - restoreText: { - fontSize: 14, - color: darkColors.text.tertiary, - }, - termsText: { - fontSize: 11, - color: darkColors.text.tertiary, - textAlign: 'center', - lineHeight: 18, - paddingHorizontal: SPACING[4], - }, -}) +function createStyles(colors: ThemeColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.bg.base, + }, + closeButton: { + position: 'absolute', + top: SPACING[4], + right: SPACING[4], + width: 44, + height: 44, + alignItems: 'center', + justifyContent: 'center', + zIndex: 10, + }, + scrollView: { + flex: 1, + }, + scrollContent: { + paddingHorizontal: SPACING[5], + paddingTop: SPACING[8], + }, + header: { + alignItems: 'center', + }, + title: { + fontSize: 32, + fontWeight: '700', + color: colors.text.primary, + textAlign: 'center', + }, + subtitle: { + fontSize: 16, + color: colors.text.secondary, + textAlign: 'center', + marginTop: SPACING[2], + }, + featuresGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: SPACING[6], + marginHorizontal: -SPACING[2], + }, + featureItem: { + width: '33%', + alignItems: 'center', + paddingVertical: SPACING[3], + }, + featureIcon: { + width: 48, + height: 48, + borderRadius: 24, + alignItems: 'center', + justifyContent: 'center', + marginBottom: SPACING[2], + }, + featureText: { + fontSize: 13, + textAlign: 'center', + }, + plansContainer: { + marginTop: SPACING[6], + gap: SPACING[3], + }, + planCard: { + flexDirection: 'row', + alignItems: 'center', + borderRadius: RADIUS.LG, + padding: SPACING[4], + borderWidth: 2, + }, + planCardPressed: { + opacity: 0.8, + }, + savingsBadge: { + position: 'absolute', + top: -8, + right: SPACING[3], + backgroundColor: BRAND.PRIMARY, + paddingHorizontal: SPACING[2], + paddingVertical: 2, + borderRadius: RADIUS.SM, + }, + savingsText: { + fontSize: 10, + fontWeight: '700', + color: colors.text.primary, + }, + planInfo: { + flex: 1, + }, + planTitle: { + fontSize: 16, + fontWeight: '600', + }, + planPeriod: { + fontSize: 13, + marginTop: 2, + }, + planPrice: { + fontSize: 20, + fontWeight: '700', + }, + checkmark: { + marginLeft: SPACING[2], + }, + priceNote: { + fontSize: 13, + textAlign: 'center', + marginTop: SPACING[3], + }, + ctaButton: { + borderRadius: RADIUS.LG, + overflow: 'hidden', + marginTop: SPACING[6], + }, + ctaButtonDisabled: { + opacity: 0.6, + }, + ctaGradient: { + paddingVertical: SPACING[4], + alignItems: 'center', + }, + ctaText: { + fontSize: 17, + fontWeight: '600', + }, + footer: { + marginTop: SPACING[5], + alignItems: 'center', + gap: SPACING[4], + }, + restoreText: { + fontSize: 14, + }, + termsText: { + fontSize: 11, + textAlign: 'center', + lineHeight: 18, + paddingHorizontal: SPACING[4], + }, + }) +}