refactor screens, navigation & player for new architecture
Simplify Home, Activity, Profile, Complete, Player, and Program screens to work with the new Supabase-driven data layer. Update root and tab layouts. Add Settings, Terms, and Zone routes. Add OfflineBanner component and progressStore. Update all player sub-components to use the refreshed design system tokens.
This commit is contained in:
@@ -1,65 +1,87 @@
|
||||
/**
|
||||
* Tabata Program Detail Screen
|
||||
* Displays program overview, weeks, sessions, and progression for kiné programs
|
||||
* Workout Program Detail Screen
|
||||
* Shows Warmup → 3 Tabatas → Stretch preview, CTA to player.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { View, Text, StyleSheet, ScrollView, Pressable } from 'react-native'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { View, Text, StyleSheet, ScrollView, Pressable, ActivityIndicator } from 'react-native'
|
||||
import { Stack, useRouter, useLocalSearchParams } from 'expo-router'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { Icon } from '@/src/shared/components/Icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useTabataProgramStore } from '@/src/shared/stores/tabataProgramStore'
|
||||
import { getTabataProgramById, getTabataSessionsByWeek } from '@/src/shared/data/tabata'
|
||||
import { canAccessProgram } from '@/src/shared/services/access'
|
||||
import { Icon } from '@/src/shared/components/Icon'
|
||||
import { fetchProgramById, buildWorkoutProgramId } from '@/src/shared/data/workoutPrograms'
|
||||
import type { WorkoutProgram } from '@/src/shared/types/workoutProgram'
|
||||
import { BODY_ZONE_META, LEVEL_META } from '@/src/shared/types/workoutProgram'
|
||||
import { useUserStore } from '@/src/shared/stores/userStore'
|
||||
import type { TabataProgramId } from '@/src/shared/types/program'
|
||||
import { TYPOGRAPHY } from '@/src/shared/constants/typography'
|
||||
import { SPACING } from '@/src/shared/constants/spacing'
|
||||
import { RADIUS } from '@/src/shared/constants/borderRadius'
|
||||
import { TEXT, NAVY, GREEN, BORDER_COLORS, AMBER, DARK } from '@/src/shared/constants/colors'
|
||||
import { TEXT, NAVY, GREEN, BORDER_COLORS, DARK } from '@/src/shared/constants/colors'
|
||||
import { withOpacity } from '@/src/shared/utils/color'
|
||||
|
||||
export default function TabataProgramDetailScreen() {
|
||||
const FALLBACK_ACCENT = '#FF6B35'
|
||||
|
||||
export default function WorkoutProgramDetailScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>()
|
||||
const router = useRouter()
|
||||
const insets = useSafeAreaInsets()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const programId = id as TabataProgramId
|
||||
const program = getTabataProgramById(programId)
|
||||
|
||||
const selectProgram = useTabataProgramStore(s => s.selectProgram)
|
||||
const progress = useTabataProgramStore(s => s.programsProgress[programId])
|
||||
const isWeekUnlocked = useTabataProgramStore(s => s.isWeekUnlocked)
|
||||
const getCurrentSession = useTabataProgramStore(s => s.getCurrentSession)
|
||||
const completion = useTabataProgramStore(s => s.getProgramCompletion(programId))
|
||||
const getProgramStatus = useTabataProgramStore(s => s.getProgramStatus)
|
||||
const [program, setProgram] = useState<WorkoutProgram | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const isPremium = useUserStore(s => s.profile.subscription) !== 'free'
|
||||
const canAccess = canAccessProgram(programId, isPremium)
|
||||
const status = getProgramStatus(programId)
|
||||
|
||||
if (!program) {
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
setLoading(true)
|
||||
fetchProgramById(id)
|
||||
.then(p => {
|
||||
if (!cancelled) setProgram(p)
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [id])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.container, styles.center]}>
|
||||
<Stack.Screen options={{ headerShown: false }} />
|
||||
<Text style={styles.errorText}>Programme non trouvé</Text>
|
||||
<ActivityIndicator color={TEXT.PRIMARY} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const handleStartProgram = () => {
|
||||
selectProgram(programId)
|
||||
const session = getCurrentSession(programId)
|
||||
if (session) {
|
||||
router.push(`/workout/${session.id}`)
|
||||
}
|
||||
if (!program) {
|
||||
return (
|
||||
<View style={[styles.container, styles.center]}>
|
||||
<Stack.Screen options={{ headerShown: false }} />
|
||||
<Text style={styles.errorText}>{t('screens:program.notFound')}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const handleSessionPress = (sessionId: string) => {
|
||||
router.push(`/workout/${sessionId}`)
|
||||
const accent = program.accentColor ?? BODY_ZONE_META[program.bodyZone].color ?? FALLBACK_ACCENT
|
||||
const level = LEVEL_META[program.level]
|
||||
const zone = BODY_ZONE_META[program.bodyZone]
|
||||
const canAccess = program.isFree || isPremium
|
||||
|
||||
const handleStart = () => {
|
||||
if (!canAccess) {
|
||||
router.push('/paywall')
|
||||
return
|
||||
}
|
||||
router.push(`/player/${buildWorkoutProgramId(program.id)}`)
|
||||
}
|
||||
|
||||
const warmupMinutes = Math.round(program.warmup.totalDuration / 60)
|
||||
const stretchMinutes = Math.round(program.stretch.totalDuration / 60)
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Stack.Screen
|
||||
@@ -68,193 +90,207 @@ export default function TabataProgramDetailScreen() {
|
||||
headerTitle: program.title,
|
||||
headerStyle: { backgroundColor: NAVY[900] },
|
||||
headerTintColor: TEXT.PRIMARY,
|
||||
headerBackTitle: 'Retour',
|
||||
headerBackTitle: t('common:back'),
|
||||
}}
|
||||
/>
|
||||
|
||||
<ScrollView style={styles.scrollView} contentContainerStyle={{ paddingBottom: insets.bottom + 100 }}>
|
||||
{/* Program header */}
|
||||
<View style={[styles.heroSection, { backgroundColor: withOpacity(program.accentColor, 0.12) }]}>
|
||||
<View style={[styles.iconCircle, { backgroundColor: withOpacity(program.accentColor, 0.19) }]}>
|
||||
<Icon name={program.icon as any} size={32} tintColor={program.accentColor} />
|
||||
<ScrollView style={styles.scroll} contentContainerStyle={{ paddingBottom: insets.bottom + 120 }}>
|
||||
{/* Hero */}
|
||||
<View style={[styles.hero, { backgroundColor: withOpacity(accent, 0.12) }]}>
|
||||
<View style={[styles.iconCircle, { backgroundColor: withOpacity(accent, 0.2) }]}>
|
||||
<Icon name={(program.icon ?? zone.icon) as any} size={32} tintColor={accent} />
|
||||
</View>
|
||||
<Text style={styles.programTitle}>{program.title}</Text>
|
||||
<Text style={styles.programDescription}>{program.description}</Text>
|
||||
<Text style={styles.title}>{program.title}</Text>
|
||||
{program.description && <Text style={styles.description}>{program.description}</Text>}
|
||||
|
||||
{/* Tier badge */}
|
||||
<View style={[styles.tierBadge, { borderColor: program.accentColor }]}>
|
||||
<Text style={[styles.tierBadgeText, { color: program.accentColor }]}>
|
||||
{program.tier === 'free' ? 'GRATUIT' : 'PREMIUM'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Stats row */}
|
||||
<View style={styles.statsRow}>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{program.durationWeeks}</Text>
|
||||
<Text style={styles.statLabel}>Semaines</Text>
|
||||
<View style={styles.badgeRow}>
|
||||
<View style={[styles.badge, { borderColor: level.color }]}>
|
||||
<Text style={[styles.badgeText, { color: level.color }]}>{level.label}</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{program.sessionsPerWeek}</Text>
|
||||
<Text style={styles.statLabel}>Séances/sem</Text>
|
||||
<View style={[styles.badge, { borderColor: zone.color }]}>
|
||||
<Text style={[styles.badgeText, { color: zone.color }]}>{zone.label}</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{program.totalSessions}</Text>
|
||||
<Text style={styles.statLabel}>Séances</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Progress */}
|
||||
{status === 'in-progress' && (
|
||||
<View style={styles.progressSection}>
|
||||
<View style={styles.progressBar}>
|
||||
<View style={[styles.progressFill, { width: `${completion}%`, backgroundColor: program.accentColor }]} />
|
||||
{!program.isFree && (
|
||||
<View style={[styles.badge, { borderColor: accent }]}>
|
||||
<Text style={[styles.badgeText, { color: accent }]}>{t('screens:home.premiumBadge')}</Text>
|
||||
</View>
|
||||
<Text style={styles.progressText}>{completion}% complété</Text>
|
||||
</View>
|
||||
)}
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.statsRow}>
|
||||
<Stat value={program.estimatedDuration} label={t('common:units.min')} />
|
||||
<Stat value={program.tabatas.length} label="Tabatas" />
|
||||
<Stat value={program.estimatedCalories} label={t('common:units.cal')} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Principles */}
|
||||
{program.principles.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Principes</Text>
|
||||
{program.principles.map((p, i) => (
|
||||
<View key={i} style={styles.principleItem}>
|
||||
<Text style={styles.principleBullet}>•</Text>
|
||||
<Text style={styles.principleText}>{p}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
{/* Warmup */}
|
||||
<Section
|
||||
title={t('screens:program.warmup')}
|
||||
subtitle={`${warmupMinutes} ${t('common:units.min')}`}
|
||||
accent="#FFC86B"
|
||||
>
|
||||
{program.warmup.exercises.map((ex, i) => (
|
||||
<Row key={`w-${i}`} label={ex.name} detail={`${ex.duration}s`} />
|
||||
))}
|
||||
</Section>
|
||||
|
||||
{/* Weeks */}
|
||||
{program.weeks.map(week => {
|
||||
const unlocked = isWeekUnlocked(programId, week.weekNumber)
|
||||
return (
|
||||
<View key={week.weekNumber} style={styles.weekSection}>
|
||||
<View style={styles.weekHeader}>
|
||||
<Text style={styles.weekTitle}>Semaine {week.weekNumber}: {week.title}</Text>
|
||||
{week.isDeload && (
|
||||
<View style={styles.deloadBadge}>
|
||||
<Text style={styles.deloadText}>Décharge</Text>
|
||||
</View>
|
||||
)}
|
||||
{!unlocked && (
|
||||
<Icon name="lock" size={16} tintColor={TEXT.TERTIARY} />
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.weekFocus}>{week.focus}</Text>
|
||||
{/* Tabatas */}
|
||||
{program.tabatas.map((tabata, i) => (
|
||||
<Section
|
||||
key={tabata.id}
|
||||
title={t('screens:program.tabataLabel', { num: i + 1 })}
|
||||
subtitle={t('screens:program.tabataSubtitle', {
|
||||
rounds: tabata.rounds,
|
||||
work: tabata.workTime,
|
||||
rest: tabata.restTime,
|
||||
})}
|
||||
accent={accent}
|
||||
>
|
||||
<Row label={tabata.exercise1.name} detail={t('screens:program.exercise1')} />
|
||||
<Row label={tabata.exercise2.name} detail={t('screens:program.exercise2')} />
|
||||
</Section>
|
||||
))}
|
||||
|
||||
{/* Sessions */}
|
||||
{week.sessions.map(session => {
|
||||
const isCompleted = progress?.completedSessionIds.includes(session.id) ?? false
|
||||
const sessionLocked = !unlocked
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={session.id}
|
||||
style={[styles.sessionCard, sessionLocked && styles.sessionCardLocked]}
|
||||
onPress={() => !sessionLocked && canAccess && handleSessionPress(session.id)}
|
||||
disabled={sessionLocked || !canAccess}
|
||||
>
|
||||
<View style={styles.sessionInfo}>
|
||||
<View style={[styles.sessionDot, {
|
||||
backgroundColor: isCompleted ? GREEN[500] : sessionLocked ? BORDER_COLORS.DIM : program.accentColor,
|
||||
}]} />
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.sessionTitle, sessionLocked && { opacity: 0.4 }]}>
|
||||
Séance {session.order}: {session.title}
|
||||
</Text>
|
||||
<Text style={styles.sessionMeta}>
|
||||
{session.blocks.length} bloc{session.blocks.length > 1 ? 's' : ''} · {session.totalDuration} min · {session.calories} cal
|
||||
</Text>
|
||||
</View>
|
||||
{isCompleted && <Icon name="checkmark.circle" size={20} tintColor={GREEN[500]} />}
|
||||
{!canAccess && !sessionLocked && <Icon name="lock" size={16} tintColor={TEXT.TERTIARY} />}
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Completion criteria */}
|
||||
{program.completionCriteria.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Critères de passage</Text>
|
||||
{program.completionCriteria.map((c, i) => (
|
||||
<View key={i} style={styles.principleItem}>
|
||||
<Text style={styles.principleBullet}>✓</Text>
|
||||
<Text style={styles.principleText}>{c}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
{/* Stretch */}
|
||||
<Section
|
||||
title={t('screens:program.stretch')}
|
||||
subtitle={`${stretchMinutes} ${t('common:units.min')}`}
|
||||
accent="#B4A7E5"
|
||||
>
|
||||
{program.stretch.exercises.map((ex, i) => (
|
||||
<Row key={`s-${i}`} label={ex.name} detail={`${ex.duration}s`} />
|
||||
))}
|
||||
</Section>
|
||||
</ScrollView>
|
||||
|
||||
{/* CTA */}
|
||||
<View style={[styles.ctaContainer, { paddingBottom: insets.bottom + SPACING[4] }]}>
|
||||
{canAccess ? (
|
||||
<Pressable style={[styles.ctaButton, { backgroundColor: program.accentColor }]} onPress={handleStartProgram}>
|
||||
<Text style={styles.ctaText}>
|
||||
{status === 'in-progress' ? 'Continuer le programme' : status === 'completed' ? 'Recommencer' : 'Commencer le programme'}
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : (
|
||||
<Pressable style={[styles.ctaButton, { backgroundColor: GREEN[500] }]} onPress={() => router.push('/paywall')}>
|
||||
<Text style={styles.ctaText}>Débloquer avec Premium</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
<Pressable
|
||||
style={[styles.ctaButton, { backgroundColor: canAccess ? accent : GREEN[500] }]}
|
||||
onPress={handleStart}
|
||||
>
|
||||
<Text style={styles.ctaText}>
|
||||
{canAccess ? t('screens:program.startSession') : t('screens:program.unlockPremium')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Stat({ value, label }: { value: number; label: string }) {
|
||||
return (
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{value}</Text>
|
||||
<Text style={styles.statLabel}>{label}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Section({
|
||||
title,
|
||||
subtitle,
|
||||
accent,
|
||||
children,
|
||||
}: {
|
||||
title: string
|
||||
subtitle: string
|
||||
accent: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<View style={[styles.sectionDot, { backgroundColor: accent }]} />
|
||||
<Text style={styles.sectionTitle}>{title}</Text>
|
||||
<Text style={styles.sectionSubtitle}>{subtitle}</Text>
|
||||
</View>
|
||||
<View style={styles.sectionBody}>{children}</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function Row({ label, detail }: { label: string; detail: string }) {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.rowLabel} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
<Text style={styles.rowDetail}>{detail}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: NAVY[900] },
|
||||
scrollView: { flex: 1 },
|
||||
center: { alignItems: 'center', justifyContent: 'center' },
|
||||
scroll: { flex: 1 },
|
||||
errorText: { color: TEXT.SECONDARY, ...TYPOGRAPHY.BODY },
|
||||
|
||||
heroSection: { padding: SPACING[6], alignItems: 'center' },
|
||||
iconCircle: { width: 64, height: 64, borderRadius: 32, alignItems: 'center', justifyContent: 'center', marginBottom: SPACING[3] },
|
||||
programTitle: { ...TYPOGRAPHY.LARGE_TITLE, color: TEXT.PRIMARY, textAlign: 'center' },
|
||||
programDescription: { ...TYPOGRAPHY.BODY, color: TEXT.SECONDARY, textAlign: 'center', marginTop: SPACING[2], lineHeight: 22 },
|
||||
tierBadge: { marginTop: SPACING[3], paddingHorizontal: SPACING[2], paddingVertical: 3, borderRadius: RADIUS.SM, borderWidth: 1 },
|
||||
tierBadgeText: { ...TYPOGRAPHY.LABEL },
|
||||
hero: { padding: SPACING[6], alignItems: 'center' },
|
||||
iconCircle: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: SPACING[3],
|
||||
},
|
||||
title: { ...TYPOGRAPHY.LARGE_TITLE, color: TEXT.PRIMARY, textAlign: 'center' },
|
||||
description: {
|
||||
...TYPOGRAPHY.BODY,
|
||||
color: TEXT.SECONDARY,
|
||||
textAlign: 'center',
|
||||
marginTop: SPACING[2],
|
||||
lineHeight: 22,
|
||||
},
|
||||
|
||||
badgeRow: { flexDirection: 'row', gap: SPACING[2], marginTop: SPACING[3], flexWrap: 'wrap', justifyContent: 'center' },
|
||||
badge: { paddingHorizontal: SPACING[2], paddingVertical: 3, borderRadius: RADIUS.SM, borderWidth: 1 },
|
||||
badgeText: { ...TYPOGRAPHY.LABEL },
|
||||
|
||||
statsRow: { flexDirection: 'row', marginTop: SPACING[6], gap: SPACING[8] },
|
||||
statItem: { alignItems: 'center' },
|
||||
statValue: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY },
|
||||
statLabel: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: 2 },
|
||||
|
||||
progressSection: { marginTop: SPACING[4], width: '100%' },
|
||||
progressBar: { height: 4, borderRadius: RADIUS.PILL, backgroundColor: BORDER_COLORS.DIM, overflow: 'hidden' },
|
||||
progressFill: { height: '100%', borderRadius: RADIUS.PILL },
|
||||
progressText: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: SPACING[1], textAlign: 'center' },
|
||||
|
||||
section: { paddingHorizontal: SPACING[5], marginTop: SPACING[6] },
|
||||
sectionTitle: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY, marginBottom: SPACING[3] },
|
||||
principleItem: { flexDirection: 'row', gap: SPACING[2], marginBottom: SPACING[2] },
|
||||
principleBullet: { color: TEXT.TERTIARY, fontSize: 14 },
|
||||
principleText: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.SECONDARY, flex: 1, lineHeight: 20 },
|
||||
sectionHeader: { flexDirection: 'row', alignItems: 'center', gap: SPACING[2], marginBottom: SPACING[3] },
|
||||
sectionDot: { width: 8, height: 8, borderRadius: 4 },
|
||||
sectionTitle: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY, flex: 1 },
|
||||
sectionSubtitle: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY },
|
||||
|
||||
weekSection: { paddingHorizontal: SPACING[5], marginTop: SPACING[6] },
|
||||
weekHeader: { flexDirection: 'row', alignItems: 'center', gap: SPACING[2] },
|
||||
weekTitle: { ...TYPOGRAPHY.TITLE_3, color: TEXT.PRIMARY, flex: 1 },
|
||||
weekFocus: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: SPACING[1], marginBottom: SPACING[3] },
|
||||
deloadBadge: { backgroundColor: withOpacity(AMBER[500], 0.2), paddingHorizontal: SPACING[2], paddingVertical: 2, borderRadius: RADIUS.SM },
|
||||
deloadText: { ...TYPOGRAPHY.LABEL, color: AMBER[500] },
|
||||
sectionBody: {
|
||||
backgroundColor: NAVY[800],
|
||||
borderRadius: RADIUS.MD,
|
||||
borderWidth: 1,
|
||||
borderColor: BORDER_COLORS.DIM,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: SPACING[3],
|
||||
paddingVertical: SPACING[3],
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: BORDER_COLORS.DIM,
|
||||
gap: SPACING[3],
|
||||
},
|
||||
rowLabel: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.PRIMARY, flex: 1 },
|
||||
rowDetail: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY },
|
||||
|
||||
sessionCard: { backgroundColor: NAVY[800], borderRadius: RADIUS.MD, padding: SPACING[3], marginBottom: SPACING[2], borderWidth: 1, borderColor: BORDER_COLORS.DIM },
|
||||
sessionCardLocked: { opacity: 0.5 },
|
||||
sessionInfo: { flexDirection: 'row', alignItems: 'center', gap: SPACING[3] },
|
||||
sessionDot: { width: 8, height: 8, borderRadius: 4 },
|
||||
sessionTitle: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.PRIMARY },
|
||||
sessionMeta: { ...TYPOGRAPHY.CAPTION_2, color: TEXT.TERTIARY, marginTop: 2 },
|
||||
|
||||
ctaContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: SPACING[5], paddingTop: SPACING[3], backgroundColor: DARK.SCRIM, borderTopWidth: 1, borderTopColor: BORDER_COLORS.DIM },
|
||||
ctaContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
paddingHorizontal: SPACING[5],
|
||||
paddingTop: SPACING[3],
|
||||
backgroundColor: DARK.SCRIM,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: BORDER_COLORS.DIM,
|
||||
},
|
||||
ctaButton: { height: 52, borderRadius: RADIUS.MD, alignItems: 'center', justifyContent: 'center' },
|
||||
ctaText: { ...TYPOGRAPHY.BUTTON_MEDIUM, color: NAVY[900], letterSpacing: 0.5 },
|
||||
errorText: { color: TEXT.SECONDARY, textAlign: 'center', marginTop: 100 },
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user