Make plans use stack navigation

This commit is contained in:
Brandon Presley 2022-07-11 12:28:30 +12:00
parent 6b8780c62e
commit e72cdc8db7
9 changed files with 279 additions and 221 deletions

View File

@ -26,7 +26,7 @@ export type DrawerParamList = {
Home: {}; Home: {};
Settings: {}; Settings: {};
Best: {}; Best: {};
Plan: {}; Plans: {};
}; };
export const DatabaseContext = React.createContext<SQLiteDatabase>({} as any); export const DatabaseContext = React.createContext<SQLiteDatabase>({} as any);
@ -83,7 +83,7 @@ const App = () => {
/> />
), ),
}} }}
name="Plan" name="Plans"
component={PlanPage} component={PlanPage}
/> />
<Drawer.Screen <Drawer.Screen

View File

@ -1,23 +1,36 @@
import {
RouteProp,
useFocusEffect,
useNavigation,
useRoute,
} from '@react-navigation/native';
import React, {useCallback, useContext, useEffect, useState} from 'react'; import React, {useCallback, useContext, useEffect, useState} from 'react';
import {ScrollView, StyleSheet, Text, View} from 'react-native'; import {ScrollView, StyleSheet, Text, View} from 'react-native';
import {Button, Dialog, Portal, Switch} from 'react-native-paper'; import {Button, IconButton, Switch} from 'react-native-paper';
import {DatabaseContext} from './App'; import {DatabaseContext} from './App';
import {Plan} from './plan'; import {PlanPageParams} from './PlanPage';
import {DAYS} from './time'; import {DAYS} from './time';
export default function EditPlan({ export default function EditPlan() {
plan, const {params} = useRoute<RouteProp<PlanPageParams, 'EditPlan'>>();
onSave, const [days, setDays] = useState<string[]>(params.plan.days.split(','));
setPlan, const [workouts, setWorkouts] = useState<string[]>(
}: { params.plan.workouts.split(','),
onSave: () => void; );
plan?: Plan;
setPlan: (plan?: Plan) => void;
}) {
const [days, setDays] = useState<string[]>([]);
const [workouts, setWorkouts] = useState<string[]>([]);
const [names, setNames] = useState<string[]>([]); const [names, setNames] = useState<string[]>([]);
const db = useContext(DatabaseContext); const db = useContext(DatabaseContext);
const navigation = useNavigation();
useFocusEffect(
useCallback(() => {
navigation.getParent()?.setOptions({
headerLeft: () => (
<IconButton icon="arrow-back" onPress={() => navigation.goBack()} />
),
title: 'Plan',
});
}, []),
);
useEffect(() => { useEffect(() => {
const refresh = async () => { const refresh = async () => {
@ -26,19 +39,16 @@ export default function EditPlan({
); );
if (!namesResult.rows.length) return setNames([]); if (!namesResult.rows.length) return setNames([]);
setNames(namesResult.rows.raw().map(({name}) => name)); setNames(namesResult.rows.raw().map(({name}) => name));
if (!plan) return;
if (plan.days) setDays(plan.days.split(','));
if (plan.workouts) setWorkouts(plan.workouts.split(','));
}; };
refresh(); refresh();
}, [plan, db]); }, [db]);
const save = useCallback(async () => { const save = useCallback(async () => {
console.log(`${EditPlan.name}.save`, {days, workouts, plan}); console.log(`${EditPlan.name}.save`, {days, workouts, params});
if (!days || !workouts) return; if (!days || !workouts) return;
const newWorkouts = workouts.filter(workout => workout).join(','); const newWorkouts = workouts.filter(workout => workout).join(',');
const newDays = days.filter(day => day).join(','); const newDays = days.filter(day => day).join(',');
if (!plan?.id) if (!params.plan.id)
await db.executeSql(`INSERT INTO plans(days, workouts) VALUES (?, ?)`, [ await db.executeSql(`INSERT INTO plans(days, workouts) VALUES (?, ?)`, [
newDays, newDays,
newWorkouts, newWorkouts,
@ -46,11 +56,10 @@ export default function EditPlan({
else else
await db.executeSql( await db.executeSql(
`UPDATE plans SET days = ?, workouts = ? WHERE id = ?`, `UPDATE plans SET days = ?, workouts = ? WHERE id = ?`,
[newDays, newWorkouts, plan.id], [newDays, newWorkouts, params.plan.id],
); );
setPlan(undefined); navigation.goBack();
onSave(); }, [days, workouts, db, params.plan]);
}, [days, workouts, db, onSave, plan, setPlan]);
const toggleWorkout = useCallback( const toggleWorkout = useCallback(
(on: boolean, name: string) => { (on: boolean, name: string) => {
@ -75,60 +84,49 @@ export default function EditPlan({
); );
return ( return (
<Portal> <View style={{padding: 10}}>
<Dialog visible={!!plan} onDismiss={() => setPlan(undefined)}> <ScrollView style={{height: '90%'}}>
<Dialog.Title> <Text style={styles.title}>Days</Text>
{plan?.days ? `Edit "${days.slice(0, 2).join(', ')}"` : 'Add a plan'} {DAYS.map(day => (
</Dialog.Title> <View key={day} style={[styles.row, {alignItems: 'center'}]}>
<Dialog.ScrollArea> <Switch
<ScrollView value={days.includes(day)}
style={{height: '80%'}} style={{marginRight: 5}}
contentContainerStyle={{paddingHorizontal: 24}}> onValueChange={value => toggleDay(value, day)}
<Text style={styles.title}>Days</Text> />
{DAYS.map(day => ( <Text onPress={() => toggleDay(!days.includes(day), day)}>
<View key={day} style={[styles.row, {alignItems: 'center'}]}> {day}
<Switch </Text>
value={days.includes(day)} </View>
style={{marginRight: 5}} ))}
onValueChange={value => toggleDay(value, day)} <Text style={[styles.title, {marginTop: 10}]}>Workouts</Text>
/> {names.length === 0 && (
<Text onPress={() => toggleDay(!days.includes(day), day)}> <Text style={{maxWidth: '80%'}}>
{day} No sets found. Try going to the home page and adding some workouts
</Text> first.
</View> </Text>
))} )}
<Text style={[styles.title, {marginTop: 10}]}>Workouts</Text> {names.map(name => (
{names.length === 0 && ( <View key={name} style={[styles.row, {alignItems: 'center'}]}>
<Text style={{maxWidth: '80%'}}> <Switch
No sets found. Try going to the home page and adding some value={workouts.includes(name)}
workouts first. style={{marginRight: 5}}
</Text> onValueChange={value => toggleWorkout(value, name)}
)} />
{names.map(name => ( <Text onPress={() => toggleWorkout(!workouts.includes(name), name)}>
<View key={name} style={[styles.row, {alignItems: 'center'}]}> {name}
<Switch </Text>
value={workouts.includes(name)} </View>
style={{marginRight: 5}} ))}
onValueChange={value => toggleWorkout(value, name)} </ScrollView>
/> <Button
<Text style={{marginTop: 10}}
onPress={() => toggleWorkout(!workouts.includes(name), name)}> mode="contained"
{name} icon="save"
</Text> onPress={save}>
</View> Save
))} </Button>
</ScrollView> </View>
</Dialog.ScrollArea>
<Dialog.Actions>
<Button icon="close" onPress={() => setPlan(undefined)}>
Cancel
</Button>
<Button mode="contained" icon="save" onPress={save}>
Save
</Button>
</Dialog.Actions>
</Dialog>
</Portal>
); );
} }

View File

@ -1,17 +1,22 @@
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import {RouteProp, useNavigation, useRoute} from '@react-navigation/native'; import {
RouteProp,
useFocusEffect,
useNavigation,
useRoute,
} from '@react-navigation/native';
import React, {useCallback, useContext, useEffect, useState} from 'react'; import React, {useCallback, useContext, useEffect, useState} from 'react';
import {NativeModules, ScrollView, StyleSheet} from 'react-native'; import {NativeModules, ScrollView, StyleSheet, View} from 'react-native';
import DateTimePickerModal from 'react-native-modal-datetime-picker'; import DateTimePickerModal from 'react-native-modal-datetime-picker';
import {Button, TextInput} from 'react-native-paper'; import {Button, IconButton, TextInput} from 'react-native-paper';
import {DatabaseContext} from './App'; import {DatabaseContext} from './App';
import {StackParams} from './HomePage'; import {HomePageParams} from './HomePage';
import {Plan} from './plan'; import {Plan} from './plan';
import Set from './set'; import Set from './set';
import {DAYS, format} from './time'; import {DAYS, format} from './time';
export default function EditSet() { export default function EditSet() {
const {params} = useRoute<RouteProp<StackParams, 'EditSet'>>(); const {params} = useRoute<RouteProp<HomePageParams, 'EditSet'>>();
const [name, setName] = useState(params.set.name); const [name, setName] = useState(params.set.name);
const [reps, setReps] = useState(params.set.reps.toString()); const [reps, setReps] = useState(params.set.reps.toString());
const [weight, setWeight] = useState(params.set.weight.toString()); const [weight, setWeight] = useState(params.set.weight.toString());
@ -21,6 +26,17 @@ export default function EditSet() {
const db = useContext(DatabaseContext); const db = useContext(DatabaseContext);
const navigation = useNavigation(); const navigation = useNavigation();
useFocusEffect(
useCallback(() => {
navigation.getParent()?.setOptions({
headerLeft: () => (
<IconButton icon="arrow-back" onPress={() => navigation.goBack()} />
),
title: 'Set',
});
}, []),
);
const getTodaysPlan = useCallback(async (): Promise<Plan[]> => { const getTodaysPlan = useCallback(async (): Promise<Plan[]> => {
const today = DAYS[new Date().getDay()]; const today = DAYS[new Date().getDay()];
const [result] = await db.executeSql( const [result] = await db.executeSql(
@ -113,55 +129,57 @@ export default function EditSet() {
}, [update, add, params.set.id]); }, [update, add, params.set.id]);
return ( return (
<ScrollView style={{padding: 10}}> <View style={{padding: 10}}>
<TextInput <ScrollView style={{height: '90%'}}>
style={styles.marginBottom} <TextInput
autoFocus style={styles.marginBottom}
label="Name *" autoFocus
value={name} label="Name *"
onChangeText={setName} value={name}
autoCorrect={false} onChangeText={setName}
/> autoCorrect={false}
<TextInput />
style={styles.marginBottom} <TextInput
label="Reps *" style={styles.marginBottom}
keyboardType="numeric" label="Reps *"
value={reps} keyboardType="numeric"
onChangeText={setReps} value={reps}
/> onChangeText={setReps}
<TextInput />
style={styles.marginBottom} <TextInput
label="Weight *" style={styles.marginBottom}
keyboardType="numeric" label="Weight *"
value={weight} keyboardType="numeric"
onChangeText={setWeight} value={weight}
onSubmitEditing={save} onChangeText={setWeight}
/> onSubmitEditing={save}
<TextInput />
style={styles.marginBottom} <TextInput
label="Unit (kg)" style={styles.marginBottom}
value={unit} label="Unit (kg)"
onChangeText={setUnit} value={unit}
onSubmitEditing={save} onChangeText={setUnit}
/> onSubmitEditing={save}
/>
<Button <Button
style={styles.marginBottom} style={styles.marginBottom}
icon="calendar-outline" icon="calendar-outline"
onPress={() => setShowDate(true)}> onPress={() => setShowDate(true)}>
{format(created)} {format(created)}
</Button> </Button>
<DateTimePickerModal <DateTimePickerModal
isVisible={showDate} isVisible={showDate}
mode="datetime" mode="datetime"
onConfirm={onConfirm} onConfirm={onConfirm}
onCancel={() => setShowDate(false)} onCancel={() => setShowDate(false)}
date={created} date={created}
/> />
</ScrollView>
<Button mode="contained" icon="save" onPress={save}> <Button mode="contained" icon="save" onPress={save}>
Save Save
</Button> </Button>
</ScrollView> </View>
); );
} }

View File

@ -8,8 +8,8 @@ import EditSet from './EditSet';
import Set from './set'; import Set from './set';
import SetList from './SetList'; import SetList from './SetList';
const Stack = createStackNavigator<StackParams>(); const Stack = createStackNavigator<HomePageParams>();
export type StackParams = { export type HomePageParams = {
Sets: {}; Sets: {};
EditSet: { EditSet: {
set: Set; set: Set;
@ -27,14 +27,6 @@ export default function HomePage() {
name="EditSet" name="EditSet"
component={EditSet} component={EditSet}
listeners={{ listeners={{
focus: () => {
navigation.setOptions({
headerLeft: () => (
<IconButton icon="arrow-back" onPress={navigation.goBack} />
),
title: 'Set',
});
},
beforeRemove: () => { beforeRemove: () => {
navigation.setOptions({ navigation.setOptions({
headerLeft: () => ( headerLeft: () => (

View File

@ -1,21 +1,22 @@
import {NavigationProp, useNavigation} from '@react-navigation/native';
import React, {useCallback, useContext, useState} from 'react'; import React, {useCallback, useContext, useState} from 'react';
import {GestureResponderEvent} from 'react-native'; import {GestureResponderEvent} from 'react-native';
import {List, Menu} from 'react-native-paper'; import {List, Menu} from 'react-native-paper';
import {DatabaseContext} from './App'; import {DatabaseContext} from './App';
import {Plan} from './plan'; import {Plan} from './plan';
import {PlanPageParams} from './PlanPage';
export default function PlanItem({ export default function PlanItem({
item, item,
setPlan,
onRemove, onRemove,
}: { }: {
item: Plan; item: Plan;
setPlan: (plan: Plan) => void;
onRemove: () => void; onRemove: () => void;
}) { }) {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [anchor, setAnchor] = useState({x: 0, y: 0}); const [anchor, setAnchor] = useState({x: 0, y: 0});
const db = useContext(DatabaseContext); const db = useContext(DatabaseContext);
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
const remove = useCallback(async () => { const remove = useCallback(async () => {
await db.executeSql(`DELETE FROM plans WHERE id = ?`, [item.id]); await db.executeSql(`DELETE FROM plans WHERE id = ?`, [item.id]);
@ -34,7 +35,7 @@ export default function PlanItem({
return ( return (
<> <>
<List.Item <List.Item
onPress={() => setPlan(item)} onPress={() => navigation.navigate('EditPlan', {plan: item})}
title={item.days.replace(/,/g, ', ')} title={item.days.replace(/,/g, ', ')}
description={item.workouts.replace(/,/g, ', ')} description={item.workouts.replace(/,/g, ', ')}
onLongPress={longPress} onLongPress={longPress}

92
PlanList.tsx Normal file
View File

@ -0,0 +1,92 @@
import {
NavigationProp,
useFocusEffect,
useNavigation,
} from '@react-navigation/native';
import React, {useCallback, useContext, useEffect, useState} from 'react';
import {FlatList, StyleSheet, View} from 'react-native';
import {List, Searchbar} from 'react-native-paper';
import {DatabaseContext} from './App';
import MassiveFab from './MassiveFab';
import {Plan} from './plan';
import PlanItem from './PlanItem';
import {PlanPageParams} from './PlanPage';
export default function PlanList() {
const [search, setSearch] = useState('');
const [plans, setPlans] = useState<Plan[]>([]);
const [refreshing, setRefresing] = useState(false);
const db = useContext(DatabaseContext);
const navigation = useNavigation<NavigationProp<PlanPageParams>>();
const refresh = useCallback(async () => {
const selectPlans = `
SELECT * from plans
WHERE days LIKE ? OR workouts LIKE ?
`;
const getPlans = ({s}: {s: string}) =>
db.executeSql(selectPlans, [`%${s}%`, `%${s}%`]);
const [plansResult] = await getPlans({s: search});
setPlans(plansResult.rows.raw());
}, [search, db]);
useFocusEffect(
useCallback(() => {
refresh();
}, [refresh]),
);
useEffect(() => {
if (!search) return;
refresh();
}, [search, refresh]);
const renderItem = useCallback(
({item}: {item: Plan}) => (
<PlanItem item={item} key={item.id} onRemove={refresh} />
),
[refresh],
);
return (
<View style={styles.container}>
<Searchbar value={search} onChangeText={setSearch} placeholder="Search" />
<FlatList
style={{height: '100%'}}
data={plans}
renderItem={renderItem}
keyExtractor={set => set.id.toString()}
refreshing={refreshing}
onRefresh={() => {
setRefresing(true);
refresh().finally(() => setRefresing(false));
}}
ListEmptyComponent={
<List.Item
title="No plans yet"
description="A plan is a list of workouts for certain days."
/>
}
/>
<MassiveFab
onPress={() =>
navigation.navigate('EditPlan', {
plan: {days: '', workouts: '', id: 0},
})
}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flexGrow: 1,
padding: 10,
paddingBottom: '10%',
},
progress: {
marginTop: 10,
},
});

View File

@ -1,85 +1,42 @@
import React, {useCallback, useContext, useEffect, useState} from 'react'; import {DrawerNavigationProp} from '@react-navigation/drawer';
import {FlatList, StyleSheet, View} from 'react-native'; import {useNavigation} from '@react-navigation/native';
import {List, Searchbar} from 'react-native-paper'; import {createStackNavigator} from '@react-navigation/stack';
import {DatabaseContext} from './App'; import React from 'react';
import {IconButton} from 'react-native-paper';
import {DrawerParamList} from './App';
import EditPlan from './EditPlan'; import EditPlan from './EditPlan';
import MassiveFab from './MassiveFab';
import {Plan} from './plan'; import {Plan} from './plan';
import PlanItem from './PlanItem'; import PlanList from './PlanList';
const Stack = createStackNavigator<PlanPageParams>();
export type PlanPageParams = {
PlanList: {};
EditPlan: {
plan: Plan;
};
};
export default function PlanPage() { export default function PlanPage() {
const [search, setSearch] = useState(''); const navigation = useNavigation<DrawerNavigationProp<DrawerParamList>>();
const [plans, setPlans] = useState<Plan[]>([]);
const [refreshing, setRefresing] = useState(false);
const [plan, setPlan] = useState<Plan>();
const db = useContext(DatabaseContext);
const refresh = useCallback(async () => {
const selectPlans = `
SELECT * from plans
WHERE days LIKE ? OR workouts LIKE ?
`;
const getPlans = ({s}: {s: string}) =>
db.executeSql(selectPlans, [`%${s}%`, `%${s}%`]);
const [plansResult] = await getPlans({s: search});
setPlans(plansResult.rows.raw());
}, [search, db]);
useEffect(() => {
refresh();
}, [search, refresh]);
const renderItem = useCallback(
({item}: {item: Plan}) => (
<PlanItem
item={item}
key={item.id}
setPlan={setPlan}
onRemove={refresh}
/>
),
[setPlan, refresh],
);
return ( return (
<View style={styles.container}> <Stack.Navigator
<Searchbar value={search} onChangeText={setSearch} placeholder="Search" /> screenOptions={{headerShown: false, animationEnabled: false}}>
<FlatList <Stack.Screen name="PlanList" component={PlanList} />
style={{height: '100%'}} <Stack.Screen
data={plans} name="EditPlan"
renderItem={renderItem} component={EditPlan}
keyExtractor={set => set.id.toString()} listeners={{
refreshing={refreshing} beforeRemove: () => {
onRefresh={() => { navigation.setOptions({
setRefresing(true); headerLeft: () => (
refresh().finally(() => setRefresing(false)); <IconButton icon="menu" onPress={navigation.openDrawer} />
}} ),
ListEmptyComponent={ title: 'Plans',
<List.Item });
title="No plans yet" },
description="A plan is a list of workouts for certain days."
/>
}
/>
<EditPlan setPlan={setPlan} onSave={refresh} plan={plan} />
<MassiveFab
onPress={() => {
setPlan({} as Plan);
}} }}
/> />
</View> </Stack.Navigator>
); );
} }
const styles = StyleSheet.create({
container: {
flexGrow: 1,
padding: 10,
paddingBottom: '10%',
},
progress: {
marginTop: 10,
},
});

View File

@ -3,7 +3,7 @@ import React, {useCallback, useContext, useState} from 'react';
import {GestureResponderEvent} from 'react-native'; import {GestureResponderEvent} from 'react-native';
import {List, Menu} from 'react-native-paper'; import {List, Menu} from 'react-native-paper';
import {DatabaseContext} from './App'; import {DatabaseContext} from './App';
import {StackParams} from './HomePage'; import {HomePageParams} from './HomePage';
import Set from './set'; import Set from './set';
export default function SetItem({ export default function SetItem({
@ -16,7 +16,7 @@ export default function SetItem({
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const [anchor, setAnchor] = useState({x: 0, y: 0}); const [anchor, setAnchor] = useState({x: 0, y: 0});
const db = useContext(DatabaseContext); const db = useContext(DatabaseContext);
const navigation = useNavigation<NavigationProp<StackParams>>(); const navigation = useNavigation<NavigationProp<HomePageParams>>();
const remove = useCallback(async () => { const remove = useCallback(async () => {
await db.executeSql(`DELETE FROM sets WHERE id = ?`, [item.id]); await db.executeSql(`DELETE FROM sets WHERE id = ?`, [item.id]);

View File

@ -7,7 +7,7 @@ import React, {useCallback, useContext, useEffect, useState} from 'react';
import {FlatList, StyleSheet, View} from 'react-native'; import {FlatList, StyleSheet, View} from 'react-native';
import {List, Searchbar} from 'react-native-paper'; import {List, Searchbar} from 'react-native-paper';
import {DatabaseContext} from './App'; import {DatabaseContext} from './App';
import {StackParams} from './HomePage'; import {HomePageParams} from './HomePage';
import MassiveFab from './MassiveFab'; import MassiveFab from './MassiveFab';
import Set from './set'; import Set from './set';
import SetItem from './SetItem'; import SetItem from './SetItem';
@ -21,7 +21,7 @@ export default function SetList() {
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [end, setEnd] = useState(false); const [end, setEnd] = useState(false);
const db = useContext(DatabaseContext); const db = useContext(DatabaseContext);
const navigation = useNavigation<NavigationProp<StackParams>>(); const navigation = useNavigation<NavigationProp<HomePageParams>>();
const selectSets = ` const selectSets = `
SELECT * from sets SELECT * from sets