app framework
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { SetupSection } from '@/components/common/SetupSection';
|
||||
import { ScreenContainer } from '@/components/common/ScreenContainer';
|
||||
import { StatCard } from '@/components/common/StatCard';
|
||||
import { shootingStyleOptions } from '@/data/shootingStyles';
|
||||
import { useAppStore } from '@/store/useAppStore';
|
||||
import { colors } from '@/theme/colors';
|
||||
import { spacing } from '@/theme/spacing';
|
||||
|
||||
export function LeagueModeScreen(): React.JSX.Element {
|
||||
const archerProfile = useAppStore((state) => state.archerProfile);
|
||||
const sessionSetup = useAppStore((state) => state.sessionSetups.league);
|
||||
const setSessionStyle = useAppStore((state) => state.setSessionStyle);
|
||||
const setSessionTargetFace = useAppStore((state) => state.setSessionTargetFace);
|
||||
const applyProfileDefaultsToSessions = useAppStore(
|
||||
(state) => state.applyProfileDefaultsToSessions
|
||||
);
|
||||
const activeSession = useAppStore((state) => state.activeSession);
|
||||
const startSession = useAppStore((state) => state.startSession);
|
||||
const clearActiveSession = useAppStore((state) => state.clearActiveSession);
|
||||
|
||||
const enabledStyles = shootingStyleOptions.filter((style) =>
|
||||
archerProfile.stylePreferences.some(
|
||||
(preference) => preference.styleId === style.id && preference.enabled
|
||||
)
|
||||
);
|
||||
const activeStyleLabel =
|
||||
shootingStyleOptions.find((style) => style.id === sessionSetup.styleId)?.label ?? 'Not set';
|
||||
const liveLeagueStyle =
|
||||
shootingStyleOptions.find((style) => style.id === activeSession?.styleId)?.label ?? 'Not set';
|
||||
const leagueIsActive = activeSession?.mode === 'league';
|
||||
|
||||
return (
|
||||
<ScreenContainer>
|
||||
<Text style={styles.title}>League mode</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Build league session setup around the local profile first, then extend this flow with
|
||||
squads, match assignments, lane management, and live sync.
|
||||
</Text>
|
||||
<SetupSection
|
||||
mode="league"
|
||||
title="League setup"
|
||||
description="Prefill the session from your profile defaults and override the style or target face when a league night requires something different."
|
||||
selectedStyleId={sessionSetup.styleId}
|
||||
selectedTargetFace={sessionSetup.targetFace}
|
||||
enabledStyles={enabledStyles}
|
||||
canStart={Boolean(sessionSetup.styleId)}
|
||||
startLabel="Start League"
|
||||
onStyleSelect={(styleId) => setSessionStyle('league', styleId)}
|
||||
onTargetFaceSelect={(targetFace) => setSessionTargetFace('league', targetFace)}
|
||||
onApplyDefaults={applyProfileDefaultsToSessions}
|
||||
onStart={() => startSession('league')}
|
||||
/>
|
||||
{leagueIsActive ? (
|
||||
<View style={styles.activeSessionCard}>
|
||||
<View style={styles.activeSessionHeader}>
|
||||
<View style={styles.activeSessionText}>
|
||||
<Text style={styles.activeSessionLabel}>League session live</Text>
|
||||
<Text style={styles.activeSessionValue}>
|
||||
{liveLeagueStyle} •{' '}
|
||||
{activeSession.targetFace === 'single-face' ? 'Single Face' : 'Multi Face'}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable onPress={clearActiveSession} style={styles.endButton}>
|
||||
<Text style={styles.endButtonText}>End Session</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Text style={styles.activeSessionMeta}>
|
||||
Started at {new Date(activeSession.startedAt).toLocaleTimeString()}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<View style={styles.statsRow}>
|
||||
<StatCard label="League style" value={activeStyleLabel} />
|
||||
<StatCard
|
||||
label="Target face"
|
||||
value={sessionSetup.targetFace === 'single-face' ? 'Single Face' : 'Multi Face'}
|
||||
/>
|
||||
<StatCard label="Active league" value="Spring Indoor" />
|
||||
</View>
|
||||
</ScreenContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
fontSize: 30,
|
||||
fontWeight: '800',
|
||||
color: colors.text,
|
||||
},
|
||||
subtitle: {
|
||||
color: colors.muted,
|
||||
lineHeight: 22,
|
||||
},
|
||||
statsRow: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: spacing.md,
|
||||
},
|
||||
activeSessionCard: {
|
||||
borderRadius: 20,
|
||||
padding: spacing.md,
|
||||
backgroundColor: colors.surfaceAlt,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.success,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
activeSessionHeader: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.md,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
activeSessionText: {
|
||||
flex: 1,
|
||||
gap: spacing.xs,
|
||||
},
|
||||
activeSessionLabel: {
|
||||
color: colors.success,
|
||||
fontSize: 13,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
activeSessionValue: {
|
||||
color: colors.text,
|
||||
fontSize: 20,
|
||||
fontWeight: '800',
|
||||
},
|
||||
activeSessionMeta: {
|
||||
color: colors.muted,
|
||||
},
|
||||
endButton: {
|
||||
borderRadius: 999,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
endButtonText: {
|
||||
color: colors.text,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
|
||||
import { ActionCard } from '@/components/common/ActionCard';
|
||||
import { ScreenContainer } from '@/components/common/ScreenContainer';
|
||||
import { shootingStyleOptions } from '@/data/shootingStyles';
|
||||
import { ModesStackParamList } from '@/navigation/types';
|
||||
import { useAppStore } from '@/store/useAppStore';
|
||||
import { colors } from '@/theme/colors';
|
||||
import { spacing } from '@/theme/spacing';
|
||||
|
||||
type Props = NativeStackScreenProps<ModesStackParamList, 'ModeHub'>;
|
||||
|
||||
export function ModeHubScreen({ navigation }: Props): React.JSX.Element {
|
||||
const archerProfile = useAppStore((state) => state.archerProfile);
|
||||
const activePreference = archerProfile.stylePreferences.find(
|
||||
(preference) => preference.styleId === archerProfile.activeStyleId
|
||||
);
|
||||
const activeStyle = shootingStyleOptions.find((style) => style.id === activePreference?.styleId);
|
||||
|
||||
return (
|
||||
<ScreenContainer>
|
||||
<Text style={styles.title}>Choose a mode</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Split free practice, league play, tournaments, and coaching drills into separate flows here.
|
||||
</Text>
|
||||
<View style={styles.defaultCard}>
|
||||
<Text style={styles.defaultLabel}>Current profile default</Text>
|
||||
<Text style={styles.defaultValue}>
|
||||
{activePreference && activeStyle
|
||||
? `${activeStyle.label} • ${activePreference.defaultTargetFace}`
|
||||
: 'No active style has been selected in Profile yet'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.grid}>
|
||||
<ActionCard
|
||||
title="Practice"
|
||||
description="Quick solo sessions, warm-ups, and custom drills."
|
||||
cta="Open"
|
||||
onPress={() => navigation.navigate('PracticeMode')}
|
||||
/>
|
||||
<ActionCard
|
||||
title="League"
|
||||
description="Shared scorecards, squads, matchplay brackets, and scheduled events."
|
||||
cta="Open"
|
||||
onPress={() => navigation.navigate('LeagueMode')}
|
||||
/>
|
||||
</View>
|
||||
</ScreenContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
fontSize: 30,
|
||||
fontWeight: '800',
|
||||
color: colors.text,
|
||||
},
|
||||
subtitle: {
|
||||
color: colors.muted,
|
||||
lineHeight: 22,
|
||||
},
|
||||
grid: {
|
||||
gap: spacing.md,
|
||||
},
|
||||
defaultCard: {
|
||||
gap: spacing.xs,
|
||||
padding: spacing.md,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.surfaceAlt,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
defaultLabel: {
|
||||
color: colors.muted,
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
defaultValue: {
|
||||
color: colors.text,
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
|
||||
import { SetupSection } from '@/components/common/SetupSection';
|
||||
import { ScreenContainer } from '@/components/common/ScreenContainer';
|
||||
import { StatCard } from '@/components/common/StatCard';
|
||||
import { shootingStyleOptions } from '@/data/shootingStyles';
|
||||
import { ModesStackParamList } from '@/navigation/types';
|
||||
import { useAppStore } from '@/store/useAppStore';
|
||||
import { colors } from '@/theme/colors';
|
||||
import { spacing } from '@/theme/spacing';
|
||||
|
||||
type Props = NativeStackScreenProps<ModesStackParamList, 'PracticeMode'>;
|
||||
|
||||
export function PracticeModeScreen({ navigation }: Props): React.JSX.Element {
|
||||
const archerProfile = useAppStore((state) => state.archerProfile);
|
||||
const sessionSetup = useAppStore((state) => state.sessionSetups.practice);
|
||||
const setSessionStyle = useAppStore((state) => state.setSessionStyle);
|
||||
const setSessionTargetFace = useAppStore((state) => state.setSessionTargetFace);
|
||||
const applyProfileDefaultsToSessions = useAppStore(
|
||||
(state) => state.applyProfileDefaultsToSessions
|
||||
);
|
||||
const activeSession = useAppStore((state) => state.activeSession);
|
||||
const startSession = useAppStore((state) => state.startSession);
|
||||
const clearActiveSession = useAppStore((state) => state.clearActiveSession);
|
||||
|
||||
const enabledStyles = shootingStyleOptions.filter((style) =>
|
||||
archerProfile.stylePreferences.some(
|
||||
(preference) => preference.styleId === style.id && preference.enabled
|
||||
)
|
||||
);
|
||||
const activeStyleLabel =
|
||||
shootingStyleOptions.find((style) => style.id === sessionSetup.styleId)?.label ?? 'Not set';
|
||||
const livePracticeStyle =
|
||||
shootingStyleOptions.find((style) => style.id === activeSession?.styleId)?.label ?? 'Not set';
|
||||
const practiceIsActive = activeSession?.mode === 'practice';
|
||||
|
||||
return (
|
||||
<ScreenContainer>
|
||||
<Text style={styles.title}>Practice mode</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Start by confirming the profile defaults for this practice session, then layer in drills,
|
||||
timers, notes, and equipment tuning checkpoints.
|
||||
</Text>
|
||||
<SetupSection
|
||||
mode="practice"
|
||||
title="Practice setup"
|
||||
description="Use your local profile as a starting point, then override style or target face for this session if needed."
|
||||
selectedStyleId={sessionSetup.styleId}
|
||||
selectedTargetFace={sessionSetup.targetFace}
|
||||
enabledStyles={enabledStyles}
|
||||
canStart={Boolean(sessionSetup.styleId)}
|
||||
startLabel={practiceIsActive ? 'Resume Practice' : 'Start Practice'}
|
||||
onStyleSelect={(styleId) => setSessionStyle('practice', styleId)}
|
||||
onTargetFaceSelect={(targetFace) => setSessionTargetFace('practice', targetFace)}
|
||||
onApplyDefaults={applyProfileDefaultsToSessions}
|
||||
onStart={() => {
|
||||
if (!practiceIsActive) {
|
||||
startSession('practice');
|
||||
}
|
||||
navigation.navigate('PracticeSession');
|
||||
}}
|
||||
/>
|
||||
{practiceIsActive ? (
|
||||
<View style={styles.activeSessionCard}>
|
||||
<View style={styles.activeSessionHeader}>
|
||||
<View style={styles.activeSessionText}>
|
||||
<Text style={styles.activeSessionLabel}>Practice session live</Text>
|
||||
<Text style={styles.activeSessionValue}>
|
||||
{livePracticeStyle} •{' '}
|
||||
{activeSession.targetFace === 'single-face' ? 'Single Face' : 'Multi Face'}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable onPress={clearActiveSession} style={styles.endButton}>
|
||||
<Text style={styles.endButtonText}>End Session</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Text style={styles.activeSessionMeta}>
|
||||
Started at {new Date(activeSession.startedAt).toLocaleTimeString()}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<View style={styles.statsRow}>
|
||||
<StatCard label="Practice style" value={activeStyleLabel} />
|
||||
<StatCard
|
||||
label="Default face"
|
||||
value={sessionSetup.targetFace === 'single-face' ? 'Single Face' : 'Multi Face'}
|
||||
/>
|
||||
<StatCard label="Current drill" value="Blank Bale" />
|
||||
</View>
|
||||
</ScreenContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
fontSize: 30,
|
||||
fontWeight: '800',
|
||||
color: colors.text,
|
||||
},
|
||||
subtitle: {
|
||||
color: colors.muted,
|
||||
lineHeight: 22,
|
||||
},
|
||||
statsRow: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: spacing.md,
|
||||
},
|
||||
activeSessionCard: {
|
||||
borderRadius: 20,
|
||||
padding: spacing.md,
|
||||
backgroundColor: colors.surfaceAlt,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.success,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
activeSessionHeader: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.md,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
activeSessionText: {
|
||||
flex: 1,
|
||||
gap: spacing.xs,
|
||||
},
|
||||
activeSessionLabel: {
|
||||
color: colors.success,
|
||||
fontSize: 13,
|
||||
fontWeight: '800',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
activeSessionValue: {
|
||||
color: colors.text,
|
||||
fontSize: 20,
|
||||
fontWeight: '800',
|
||||
},
|
||||
activeSessionMeta: {
|
||||
color: colors.muted,
|
||||
},
|
||||
endButton: {
|
||||
borderRadius: 999,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
endButtonText: {
|
||||
color: colors.text,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,346 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
|
||||
import { ScreenContainer } from '@/components/common/ScreenContainer';
|
||||
import { StatCard } from '@/components/common/StatCard';
|
||||
import { shootingStyleOptions } from '@/data/shootingStyles';
|
||||
import { ModesStackParamList } from '@/navigation/types';
|
||||
import { useAppStore } from '@/store/useAppStore';
|
||||
import { colors } from '@/theme/colors';
|
||||
import { spacing } from '@/theme/spacing';
|
||||
import { ScoreEntry, TargetFacePreference } from '@/types';
|
||||
|
||||
type Props = NativeStackScreenProps<ModesStackParamList, 'PracticeSession'>;
|
||||
|
||||
function getScoreOptions(targetFace: TargetFacePreference): ScoreEntry[] {
|
||||
if (targetFace === 'multi-face') {
|
||||
return [
|
||||
{ arrowNumber: 0, value: 10, isX: false },
|
||||
{ arrowNumber: 0, value: 9 },
|
||||
{ arrowNumber: 0, value: 8 },
|
||||
{ arrowNumber: 0, value: 7 },
|
||||
{ arrowNumber: 0, value: 6 },
|
||||
{ arrowNumber: 0, value: 5 },
|
||||
{ arrowNumber: 0, value: 0 },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ arrowNumber: 0, value: 10, isX: true },
|
||||
{ arrowNumber: 0, value: 10, isX: false },
|
||||
{ arrowNumber: 0, value: 9 },
|
||||
{ arrowNumber: 0, value: 8 },
|
||||
{ arrowNumber: 0, value: 7 },
|
||||
{ arrowNumber: 0, value: 6 },
|
||||
{ arrowNumber: 0, value: 5 },
|
||||
{ arrowNumber: 0, value: 4 },
|
||||
{ arrowNumber: 0, value: 3 },
|
||||
{ arrowNumber: 0, value: 2 },
|
||||
{ arrowNumber: 0, value: 1 },
|
||||
{ arrowNumber: 0, value: 0 },
|
||||
];
|
||||
}
|
||||
|
||||
function getScoreLabel(entry: ScoreEntry): string {
|
||||
if (entry.value === 0) {
|
||||
return 'M';
|
||||
}
|
||||
|
||||
return entry.isX ? 'X' : String(entry.value);
|
||||
}
|
||||
|
||||
export function PracticeSessionScreen({ navigation }: Props): React.JSX.Element {
|
||||
const activeSession = useAppStore((state) => state.activeSession);
|
||||
const practiceSession = useAppStore((state) => state.practiceSession);
|
||||
const selectedRound = useAppStore((state) => state.selectedRound);
|
||||
const addPracticeArrow = useAppStore((state) => state.addPracticeArrow);
|
||||
const removeLastPracticeArrow = useAppStore((state) => state.removeLastPracticeArrow);
|
||||
const savePracticeEnd = useAppStore((state) => state.savePracticeEnd);
|
||||
const finishPracticeSession = useAppStore((state) => state.finishPracticeSession);
|
||||
|
||||
if (activeSession?.mode !== 'practice' || !practiceSession) {
|
||||
return (
|
||||
<ScreenContainer>
|
||||
<Text style={styles.title}>No active practice session</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Start a practice session from the practice setup screen before entering scoring.
|
||||
</Text>
|
||||
<Pressable style={styles.primaryButton} onPress={() => navigation.goBack()}>
|
||||
<Text style={styles.primaryButtonText}>Back to Practice Setup</Text>
|
||||
</Pressable>
|
||||
</ScreenContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const styleLabel =
|
||||
shootingStyleOptions.find((style) => style.id === activeSession.styleId)?.label ?? 'Unknown';
|
||||
const scoreOptions = getScoreOptions(activeSession.targetFace);
|
||||
|
||||
return (
|
||||
<ScreenContainer>
|
||||
<Text style={styles.title}>Practice scoring</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
This is the local scoring flow for practice sessions. Later we can swap in target plotting,
|
||||
notes, timers, and richer analytics without changing the core session lifecycle.
|
||||
</Text>
|
||||
<View style={styles.rulesCard}>
|
||||
<Text style={styles.rulesTitle}>Available scores</Text>
|
||||
<Text style={styles.rulesText}>
|
||||
{activeSession.targetFace === 'single-face'
|
||||
? 'Single-face scoring includes X, 10 through 1, and M.'
|
||||
: 'Multi-face scoring is limited to 10 through 5 and M.'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.statsRow}>
|
||||
<StatCard label="Style" value={styleLabel} />
|
||||
<StatCard
|
||||
label="Target Face"
|
||||
value={activeSession.targetFace === 'single-face' ? 'Single Face' : 'Multi Face'}
|
||||
/>
|
||||
<StatCard label="Round" value={selectedRound?.name ?? 'Open Practice'} />
|
||||
</View>
|
||||
|
||||
<View style={styles.summaryCard}>
|
||||
<Text style={styles.summaryTitle}>End {practiceSession.currentEndNumber}</Text>
|
||||
<Text style={styles.summaryText}>
|
||||
Current arrows: {practiceSession.currentArrows.length}/{practiceSession.arrowsPerEnd}
|
||||
</Text>
|
||||
<Text style={styles.summaryText}>
|
||||
Running total: {practiceSession.totalScore}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.currentArrowsCard}>
|
||||
<Text style={styles.sectionTitle}>Current arrows</Text>
|
||||
<View style={styles.arrowRow}>
|
||||
{Array.from({ length: practiceSession.arrowsPerEnd }).map((_, index) => (
|
||||
<View key={index} style={styles.arrowSlot}>
|
||||
<Text style={styles.arrowSlotText}>
|
||||
{practiceSession.currentArrows[index]
|
||||
? getScoreLabel(practiceSession.currentArrows[index])
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.scoreGrid}>
|
||||
{scoreOptions.map((entry, index) => (
|
||||
<Pressable
|
||||
key={`${getScoreLabel(entry)}-${index}`}
|
||||
style={styles.scoreButton}
|
||||
onPress={() =>
|
||||
addPracticeArrow({
|
||||
...entry,
|
||||
arrowNumber: practiceSession.currentArrows.length + 1,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Text style={styles.scoreButtonText}>{getScoreLabel(entry)}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View style={styles.actionRow}>
|
||||
<Pressable style={styles.secondaryButton} onPress={removeLastPracticeArrow}>
|
||||
<Text style={styles.secondaryButtonText}>Undo Last</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.secondaryButton} onPress={savePracticeEnd}>
|
||||
<Text style={styles.secondaryButtonText}>Save End</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={styles.completedCard}>
|
||||
<Text style={styles.sectionTitle}>Completed ends</Text>
|
||||
{practiceSession.completedEnds.length === 0 ? (
|
||||
<Text style={styles.emptyText}>No ends saved yet.</Text>
|
||||
) : (
|
||||
practiceSession.completedEnds.map((end) => (
|
||||
<View key={end.endNumber} style={styles.completedRow}>
|
||||
<Text style={styles.completedLabel}>End {end.endNumber}</Text>
|
||||
<Text style={styles.completedMeta}>
|
||||
{end.arrows.map((arrow) => getScoreLabel(arrow)).join(' • ')}
|
||||
</Text>
|
||||
<Text style={styles.completedTotal}>{end.total}</Text>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
style={styles.primaryButton}
|
||||
onPress={() => {
|
||||
finishPracticeSession();
|
||||
navigation.popToTop();
|
||||
}}
|
||||
>
|
||||
<Text style={styles.primaryButtonText}>Finish Practice</Text>
|
||||
</Pressable>
|
||||
</ScreenContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
fontSize: 30,
|
||||
fontWeight: '800',
|
||||
color: colors.text,
|
||||
},
|
||||
subtitle: {
|
||||
color: colors.muted,
|
||||
lineHeight: 22,
|
||||
},
|
||||
statsRow: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: spacing.md,
|
||||
},
|
||||
rulesCard: {
|
||||
borderRadius: 20,
|
||||
padding: spacing.md,
|
||||
backgroundColor: colors.surfaceAlt,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
gap: spacing.xs,
|
||||
},
|
||||
rulesTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
},
|
||||
rulesText: {
|
||||
color: colors.muted,
|
||||
lineHeight: 20,
|
||||
},
|
||||
summaryCard: {
|
||||
borderRadius: 20,
|
||||
padding: spacing.md,
|
||||
backgroundColor: colors.surfaceAlt,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
gap: spacing.xs,
|
||||
},
|
||||
summaryTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 20,
|
||||
fontWeight: '800',
|
||||
},
|
||||
summaryText: {
|
||||
color: colors.muted,
|
||||
},
|
||||
currentArrowsCard: {
|
||||
borderRadius: 20,
|
||||
padding: spacing.md,
|
||||
backgroundColor: colors.surfaceAlt,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
gap: spacing.md,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: colors.text,
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
},
|
||||
arrowRow: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.md,
|
||||
},
|
||||
arrowSlot: {
|
||||
flex: 1,
|
||||
minHeight: 64,
|
||||
borderRadius: 16,
|
||||
backgroundColor: colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
arrowSlotText: {
|
||||
color: colors.text,
|
||||
fontSize: 24,
|
||||
fontWeight: '800',
|
||||
},
|
||||
scoreGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
scoreButton: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.accent,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primarySoft,
|
||||
},
|
||||
scoreButtonText: {
|
||||
color: colors.text,
|
||||
fontSize: 20,
|
||||
fontWeight: '800',
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.md,
|
||||
},
|
||||
secondaryButton: {
|
||||
flex: 1,
|
||||
borderRadius: 16,
|
||||
paddingVertical: 14,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surfaceAlt,
|
||||
},
|
||||
secondaryButtonText: {
|
||||
color: colors.text,
|
||||
fontWeight: '700',
|
||||
},
|
||||
completedCard: {
|
||||
borderRadius: 20,
|
||||
padding: spacing.md,
|
||||
backgroundColor: colors.surfaceAlt,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
emptyText: {
|
||||
color: colors.muted,
|
||||
},
|
||||
completedRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
completedLabel: {
|
||||
width: 56,
|
||||
color: colors.text,
|
||||
fontWeight: '700',
|
||||
},
|
||||
completedMeta: {
|
||||
flex: 1,
|
||||
color: colors.muted,
|
||||
},
|
||||
completedTotal: {
|
||||
color: colors.primary,
|
||||
fontWeight: '800',
|
||||
},
|
||||
primaryButton: {
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
primaryButtonText: {
|
||||
color: '#04111F',
|
||||
fontWeight: '800',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user