feat: 5 tab screens wired to centralized data layer

All tabs use shared data, stores, and SwiftUI islands:

- Home: greeting from userStore, featured/popular workouts,
  recent activity from activityStore, tappable collections
- Workouts: 50 workouts with SwiftUI Picker category filter,
  trainer avatars, workout grid, "See All" links to categories
- Activity: streak banner, SwiftUI Gauges (workouts/minutes/
  calories/best streak), weekly Chart, achievements grid
- Browse: featured collection hero, collection grid with emoji
  icons, programs carousel, new releases list
- Profile: user card, subscription banner, SwiftUI List with
  workout/notification settings (Switches persist via Zustand)

Tab layout uses NativeTabs with SF Symbols and haptic feedback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Millian Lamiaux
2026-02-20 13:24:06 +01:00
parent 13faf21b8d
commit 99d8fba852
7 changed files with 1656 additions and 164 deletions

236
app/(tabs)/profile.tsx Normal file
View File

@@ -0,0 +1,236 @@
/**
* TabataFit Profile Screen
* React Native + SwiftUI Islands — wired to shared data
*/
import { View, StyleSheet, ScrollView, Text as RNText } from 'react-native'
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 {
Host,
List,
Section,
Switch,
Text,
LabeledContent,
} from '@expo/ui/swift-ui'
import { useUserStore } from '@/src/shared/stores'
import { StyledText } from '@/src/shared/components/StyledText'
import {
BRAND,
DARK,
TEXT,
GLASS,
SHADOW,
GRADIENTS,
} from '@/src/shared/constants/colors'
import { SPACING, LAYOUT } from '@/src/shared/constants/spacing'
import { RADIUS } from '@/src/shared/constants/borderRadius'
const FONTS = {
LARGE_TITLE: 34,
TITLE_2: 22,
HEADLINE: 17,
SUBHEADLINE: 15,
CAPTION_1: 12,
}
// ═══════════════════════════════════════════════════════════════════════════
// MAIN SCREEN
// ═══════════════════════════════════════════════════════════════════════════
export default function ProfileScreen() {
const insets = useSafeAreaInsets()
const profile = useUserStore((s) => s.profile)
const settings = useUserStore((s) => s.settings)
const updateSettings = useUserStore((s) => s.updateSettings)
const isPremium = profile.subscription !== 'free'
const planLabel = isPremium ? 'TabataFit+' : 'Free'
return (
<View style={[styles.container, { paddingTop: insets.top }]}>
<ScrollView
style={styles.scrollView}
contentContainerStyle={[styles.scrollContent, { paddingBottom: insets.bottom + 100 }]}
showsVerticalScrollIndicator={false}
>
{/* Header */}
<StyledText size={FONTS.LARGE_TITLE} weight="bold" color={TEXT.PRIMARY}>
Profile
</StyledText>
{/* Profile Card */}
<View style={styles.profileCard}>
<BlurView intensity={GLASS.BLUR_MEDIUM} tint="dark" style={StyleSheet.absoluteFill} />
<View style={styles.avatarContainer}>
<LinearGradient
colors={GRADIENTS.CTA}
style={StyleSheet.absoluteFill}
/>
<StyledText size={FONTS.LARGE_TITLE} weight="bold" color={TEXT.PRIMARY}>
{profile.name[0]}
</StyledText>
</View>
<StyledText size={FONTS.TITLE_2} weight="semibold" color={TEXT.PRIMARY}>
{profile.name}
</StyledText>
<StyledText size={FONTS.SUBHEADLINE} color={TEXT.TERTIARY}>
{profile.email}
</StyledText>
{isPremium && (
<View style={styles.premiumBadge}>
<Ionicons name="star" size={12} color={BRAND.PRIMARY} />
<StyledText size={FONTS.CAPTION_1} weight="semibold" color={TEXT.PRIMARY}>
{planLabel}
</StyledText>
</View>
)}
</View>
{/* Subscription */}
{isPremium && (
<View style={styles.subscriptionCard}>
<LinearGradient
colors={GRADIENTS.CTA}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={StyleSheet.absoluteFill}
/>
<View style={styles.subscriptionContent}>
<Ionicons name="ribbon" size={24} color={TEXT.PRIMARY} />
<View style={styles.subscriptionInfo}>
<StyledText size={FONTS.HEADLINE} weight="bold" color={TEXT.PRIMARY}>
{planLabel}
</StyledText>
<StyledText size={FONTS.CAPTION_1} color={TEXT.SECONDARY}>
{'Member since ' + profile.joinDate}
</StyledText>
</View>
</View>
</View>
)}
{/* SwiftUI Island: Settings */}
<Host useViewportSizeMeasurement colorScheme="dark" style={{ height: 385 }}>
<List listStyle="insetGrouped" scrollEnabled={false}>
<Section title="WORKOUT">
<Switch
label="Haptic Feedback"
value={settings.haptics}
onValueChange={(v) => updateSettings({ haptics: v })}
color={BRAND.PRIMARY}
/>
<Switch
label="Sound Effects"
value={settings.soundEffects}
onValueChange={(v) => updateSettings({ soundEffects: v })}
color={BRAND.PRIMARY}
/>
<Switch
label="Voice Coaching"
value={settings.voiceCoaching}
onValueChange={(v) => updateSettings({ voiceCoaching: v })}
color={BRAND.PRIMARY}
/>
</Section>
<Section title="NOTIFICATIONS">
<Switch
label="Daily Reminders"
value={settings.reminders}
onValueChange={(v) => updateSettings({ reminders: v })}
color={BRAND.PRIMARY}
/>
<LabeledContent label="Reminder Time">
<Text color={TEXT.TERTIARY}>{settings.reminderTime.replace(':00', ':00 AM')}</Text>
</LabeledContent>
</Section>
</List>
</Host>
{/* Version */}
<StyledText size={FONTS.CAPTION_1} color={TEXT.TERTIARY} style={styles.versionText}>
TabataFit v1.0.0
</StyledText>
</ScrollView>
</View>
)
}
// ═══════════════════════════════════════════════════════════════════════════
// STYLES
// ═══════════════════════════════════════════════════════════════════════════
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: DARK.BASE,
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingHorizontal: LAYOUT.SCREEN_PADDING,
},
// Profile Card
profileCard: {
borderRadius: RADIUS.GLASS_CARD,
overflow: 'hidden',
marginBottom: SPACING[6],
marginTop: SPACING[4],
alignItems: 'center',
paddingVertical: SPACING[6],
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
...SHADOW.md,
},
avatarContainer: {
width: 80,
height: 80,
borderRadius: 40,
alignItems: 'center',
justifyContent: 'center',
marginBottom: SPACING[3],
overflow: 'hidden',
},
premiumBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 107, 53, 0.15)',
paddingHorizontal: SPACING[3],
paddingVertical: SPACING[1],
borderRadius: RADIUS.FULL,
marginTop: SPACING[3],
gap: SPACING[1],
},
// Subscription Card
subscriptionCard: {
height: 80,
borderRadius: RADIUS.LG,
overflow: 'hidden',
marginBottom: SPACING[6],
...SHADOW.BRAND_GLOW,
},
subscriptionContent: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: SPACING[4],
gap: SPACING[3],
},
subscriptionInfo: {
flex: 1,
},
// Version Text
versionText: {
textAlign: 'center',
marginTop: SPACING[6],
},
})