Use stack navigation for homepage + EditSet
This commit is contained in:
parent
f4264b2589
commit
f15c6df20b
27
App.tsx
27
App.tsx
|
@ -1,5 +1,5 @@
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs';
|
import {createDrawerNavigator} from '@react-navigation/drawer';
|
||||||
import {
|
import {
|
||||||
DarkTheme,
|
DarkTheme,
|
||||||
DefaultTheme,
|
DefaultTheme,
|
||||||
|
@ -21,8 +21,8 @@ import HomePage from './HomePage';
|
||||||
import PlanPage from './PlanPage';
|
import PlanPage from './PlanPage';
|
||||||
import SettingsPage from './SettingsPage';
|
import SettingsPage from './SettingsPage';
|
||||||
|
|
||||||
const Tab = createMaterialTopTabNavigator<RootStackParamList>();
|
const Drawer = createDrawerNavigator<DrawerParamList>();
|
||||||
export type RootStackParamList = {
|
export type DrawerParamList = {
|
||||||
Home: {};
|
Home: {};
|
||||||
Settings: {};
|
Settings: {};
|
||||||
Best: {};
|
Best: {};
|
||||||
|
@ -64,19 +64,20 @@ const App = () => {
|
||||||
<StatusBar barStyle={dark ? 'light-content' : 'dark-content'} />
|
<StatusBar barStyle={dark ? 'light-content' : 'dark-content'} />
|
||||||
{db && (
|
{db && (
|
||||||
<DatabaseContext.Provider value={db}>
|
<DatabaseContext.Provider value={db}>
|
||||||
<Tab.Navigator>
|
<Drawer.Navigator
|
||||||
<Tab.Screen
|
screenOptions={{headerTintColor: dark ? 'white' : 'black'}}>
|
||||||
|
<Drawer.Screen
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: ({focused}) => (
|
drawerIcon: ({focused}) => (
|
||||||
<IconButton icon={focused ? 'home' : 'home-outline'} />
|
<IconButton icon={focused ? 'home' : 'home-outline'} />
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
name="Home"
|
name="Home"
|
||||||
component={HomePage}
|
component={HomePage}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen
|
<Drawer.Screen
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: ({focused}) => (
|
drawerIcon: ({focused}) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={focused ? 'calendar' : 'calendar-outline'}
|
icon={focused ? 'calendar' : 'calendar-outline'}
|
||||||
/>
|
/>
|
||||||
|
@ -85,9 +86,9 @@ const App = () => {
|
||||||
name="Plan"
|
name="Plan"
|
||||||
component={PlanPage}
|
component={PlanPage}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen
|
<Drawer.Screen
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: ({focused}) => (
|
drawerIcon: ({focused}) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={focused ? 'bar-chart' : 'bar-chart-outline'}
|
icon={focused ? 'bar-chart' : 'bar-chart-outline'}
|
||||||
/>
|
/>
|
||||||
|
@ -96,9 +97,9 @@ const App = () => {
|
||||||
name="Best"
|
name="Best"
|
||||||
component={BestPage}
|
component={BestPage}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen
|
<Drawer.Screen
|
||||||
options={{
|
options={{
|
||||||
tabBarLabel: ({focused}) => (
|
drawerIcon: ({focused}) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={focused ? 'settings' : 'settings-outline'}
|
icon={focused ? 'settings' : 'settings-outline'}
|
||||||
/>
|
/>
|
||||||
|
@ -107,7 +108,7 @@ const App = () => {
|
||||||
name="Settings"
|
name="Settings"
|
||||||
component={SettingsPage}
|
component={SettingsPage}
|
||||||
/>
|
/>
|
||||||
</Tab.Navigator>
|
</Drawer.Navigator>
|
||||||
</DatabaseContext.Provider>
|
</DatabaseContext.Provider>
|
||||||
)}
|
)}
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
|
|
180
EditSet.tsx
180
EditSet.tsx
|
@ -1,98 +1,164 @@
|
||||||
import React, {useState} from 'react';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import {ScrollView, StyleSheet} from 'react-native';
|
import {RouteProp, useNavigation, useRoute} from '@react-navigation/native';
|
||||||
|
import React, {useCallback, useContext, useEffect, useState} from 'react';
|
||||||
|
import {NativeModules, ScrollView, StyleSheet} from 'react-native';
|
||||||
import DateTimePickerModal from 'react-native-modal-datetime-picker';
|
import DateTimePickerModal from 'react-native-modal-datetime-picker';
|
||||||
import {Button, Dialog, Portal, TextInput} from 'react-native-paper';
|
import {Button, TextInput} from 'react-native-paper';
|
||||||
|
import {DatabaseContext} from './App';
|
||||||
|
import {StackParams} from './HomePage';
|
||||||
|
import {Plan} from './plan';
|
||||||
import Set from './set';
|
import Set from './set';
|
||||||
import {format} from './time';
|
import {DAYS, format} from './time';
|
||||||
|
|
||||||
export default function EditSet({
|
export default function EditSet() {
|
||||||
set,
|
const {params} = useRoute<RouteProp<StackParams, 'EditSet'>>();
|
||||||
setSet,
|
const [name, setName] = useState(params.set.name);
|
||||||
onSave,
|
const [reps, setReps] = useState(params.set.reps.toString());
|
||||||
title,
|
const [weight, setWeight] = useState(params.set.weight.toString());
|
||||||
saveText,
|
const [created, setCreated] = useState(new Date(params.set.created));
|
||||||
show,
|
const [unit, setUnit] = useState(params.set.unit);
|
||||||
setShow,
|
|
||||||
}: {
|
|
||||||
onSave: () => void;
|
|
||||||
set?: Set;
|
|
||||||
setSet: (set?: Set) => void;
|
|
||||||
title: string;
|
|
||||||
saveText: string;
|
|
||||||
show: boolean;
|
|
||||||
setShow: (show: boolean) => void;
|
|
||||||
}) {
|
|
||||||
const [showDate, setShowDate] = useState(false);
|
const [showDate, setShowDate] = useState(false);
|
||||||
|
const db = useContext(DatabaseContext);
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
const getTodaysPlan = useCallback(async (): Promise<Plan[]> => {
|
||||||
|
const today = DAYS[new Date().getDay()];
|
||||||
|
const [result] = await db.executeSql(
|
||||||
|
`SELECT * FROM plans WHERE days LIKE ? LIMIT 1`,
|
||||||
|
[`%${today}%`],
|
||||||
|
);
|
||||||
|
return result.rows.raw();
|
||||||
|
}, [db]);
|
||||||
|
|
||||||
|
const getTodaysSets = useCallback(async (): Promise<Set[]> => {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const [result] = await db.executeSql(
|
||||||
|
`SELECT * FROM sets WHERE created LIKE ? ORDER BY created DESC`,
|
||||||
|
[`${today}%`],
|
||||||
|
);
|
||||||
|
return result.rows.raw();
|
||||||
|
}, [db]);
|
||||||
|
|
||||||
|
const predict = useCallback(async () => {
|
||||||
|
if ((await AsyncStorage.getItem('predictiveSets')) === 'false') return;
|
||||||
|
const todaysPlan = await getTodaysPlan();
|
||||||
|
if (todaysPlan.length === 0) return;
|
||||||
|
const todaysSets = await getTodaysSets();
|
||||||
|
const todaysWorkouts = todaysPlan[0].workouts.split(',');
|
||||||
|
if (todaysSets.length === 0) return setName(todaysWorkouts[0]);
|
||||||
|
const count = todaysSets.filter(
|
||||||
|
set => set.name === todaysSets[0].name,
|
||||||
|
).length;
|
||||||
|
const maxSets = await AsyncStorage.getItem('maxSets');
|
||||||
|
if (count < Number(maxSets)) {
|
||||||
|
setName(todaysSets[0].name);
|
||||||
|
setReps(todaysSets[0].reps.toString());
|
||||||
|
setWeight(todaysSets[0].weight.toString());
|
||||||
|
return setUnit(todaysSets[0].unit);
|
||||||
|
}
|
||||||
|
const nextWorkout =
|
||||||
|
todaysWorkouts[todaysWorkouts.indexOf(todaysSets[0].name!) + 1];
|
||||||
|
if (!nextWorkout) return;
|
||||||
|
setName(nextWorkout);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (params.set.id) return;
|
||||||
|
predict();
|
||||||
|
}, [predict]);
|
||||||
|
|
||||||
const onConfirm = (created: Date) => {
|
const onConfirm = (created: Date) => {
|
||||||
setSet({...set, created: created.toISOString()});
|
setCreated(created);
|
||||||
setShowDate(false);
|
setShowDate(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const update = useCallback(async () => {
|
||||||
|
console.log(`${EditSet.name}.update`, params.set);
|
||||||
|
await db.executeSql(
|
||||||
|
`UPDATE sets SET name = ?, reps = ?, weight = ?, created = ?, unit = ? WHERE id = ?`,
|
||||||
|
[name, reps, weight, created.toISOString(), unit, params.set.id],
|
||||||
|
);
|
||||||
|
navigation.goBack();
|
||||||
|
}, [params.set, name, reps, weight, created, unit, db]);
|
||||||
|
|
||||||
|
const add = useCallback(async () => {
|
||||||
|
if (name === undefined || reps === '' || weight === '') return;
|
||||||
|
const insert = `
|
||||||
|
INSERT INTO sets(name, reps, weight, created, unit)
|
||||||
|
VALUES (?,?,?,?,?)
|
||||||
|
`;
|
||||||
|
await db.executeSql(insert, [
|
||||||
|
name,
|
||||||
|
reps,
|
||||||
|
weight,
|
||||||
|
created.toISOString(),
|
||||||
|
unit || 'kg',
|
||||||
|
]);
|
||||||
|
notify();
|
||||||
|
navigation.goBack();
|
||||||
|
}, [name, reps, weight, created, unit, db]);
|
||||||
|
|
||||||
|
const notify = useCallback(async () => {
|
||||||
|
const enabled = await AsyncStorage.getItem('alarmEnabled');
|
||||||
|
if (enabled !== 'true') return;
|
||||||
|
const minutes = await AsyncStorage.getItem('minutes');
|
||||||
|
const seconds = await AsyncStorage.getItem('seconds');
|
||||||
|
const milliseconds = Number(minutes) * 60 * 1000 + Number(seconds) * 1000;
|
||||||
|
NativeModules.AlarmModule.timer(milliseconds);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const save = useCallback(async () => {
|
||||||
|
if (params.set.id) return update();
|
||||||
|
return add();
|
||||||
|
}, [update, add]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<ScrollView style={{padding: 10}}>
|
||||||
<Dialog visible={show} onDismiss={() => setShow(false)}>
|
|
||||||
<Dialog.Title>{title}</Dialog.Title>
|
|
||||||
<Dialog.ScrollArea>
|
|
||||||
<ScrollView>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.text}
|
style={styles.text}
|
||||||
autoFocus
|
autoFocus
|
||||||
label="Name *"
|
label="Name *"
|
||||||
value={set?.name}
|
value={name}
|
||||||
onChangeText={name => setSet({...set, name})}
|
onChangeText={setName}
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.text}
|
style={styles.text}
|
||||||
label="Reps *"
|
label="Reps *"
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
value={set?.reps?.toString() || ''}
|
value={reps}
|
||||||
onChangeText={reps => setSet({...set, reps})}
|
onChangeText={setReps}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.text}
|
style={styles.text}
|
||||||
label="Weight *"
|
label="Weight *"
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
value={set?.weight?.toString() || ''}
|
value={weight}
|
||||||
onChangeText={weight => setSet({...set, weight})}
|
onChangeText={setWeight}
|
||||||
onSubmitEditing={onSave}
|
onSubmitEditing={save}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.text}
|
style={styles.text}
|
||||||
label="Unit (kg)"
|
label="Unit (kg)"
|
||||||
value={set?.unit}
|
value={unit}
|
||||||
onChangeText={unit => setSet({...set, unit})}
|
onChangeText={setUnit}
|
||||||
onSubmitEditing={onSave}
|
onSubmitEditing={save}
|
||||||
/>
|
/>
|
||||||
{set?.created && (
|
|
||||||
<>
|
<Button icon="calendar-outline" onPress={() => setShowDate(true)}>
|
||||||
<Button
|
{format(created)}
|
||||||
icon="calendar-outline"
|
|
||||||
onPress={() => setShowDate(true)}>
|
|
||||||
{format(set.created)}
|
|
||||||
</Button>
|
</Button>
|
||||||
<DateTimePickerModal
|
<DateTimePickerModal
|
||||||
isVisible={showDate}
|
isVisible={showDate}
|
||||||
mode="datetime"
|
mode="datetime"
|
||||||
onConfirm={onConfirm}
|
onConfirm={onConfirm}
|
||||||
onCancel={() => setShowDate(false)}
|
onCancel={() => setShowDate(false)}
|
||||||
date={new Date(set.created)}
|
date={created}
|
||||||
/>
|
/>
|
||||||
</>
|
<Button mode="contained" icon="save" onPress={save}>
|
||||||
)}
|
Save
|
||||||
|
</Button>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</Dialog.ScrollArea>
|
|
||||||
<Dialog.Actions>
|
|
||||||
<Button icon="close" onPress={() => setShow(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button mode="contained" icon="save" onPress={onSave}>
|
|
||||||
{saveText}
|
|
||||||
</Button>
|
|
||||||
</Dialog.Actions>
|
|
||||||
</Dialog>
|
|
||||||
</Portal>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
255
HomePage.tsx
255
HomePage.tsx
|
@ -1,226 +1,49 @@
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import {DrawerNavigationProp} from '@react-navigation/drawer';
|
||||||
import React, {useCallback, useContext, useEffect, useState} from 'react';
|
import {TypedNavigator, useNavigation} from '@react-navigation/native';
|
||||||
import {FlatList, NativeModules, StyleSheet, View} from 'react-native';
|
import {createNativeStackNavigator} from '@react-navigation/native-stack';
|
||||||
import {List, Searchbar} from 'react-native-paper';
|
import React from 'react';
|
||||||
import {DatabaseContext} from './App';
|
import {IconButton} from 'react-native-paper';
|
||||||
|
import {DrawerParamList} from './App';
|
||||||
import EditSet from './EditSet';
|
import EditSet from './EditSet';
|
||||||
import MassiveFab from './MassiveFab';
|
|
||||||
import {Plan} from './plan';
|
|
||||||
import Set from './set';
|
import Set from './set';
|
||||||
import SetItem from './SetItem';
|
import SetsPage from './SetsPage';
|
||||||
import {DAYS} from './time';
|
|
||||||
|
|
||||||
const limit = 15;
|
const Stack = createNativeStackNavigator<StackParams>();
|
||||||
|
export type StackParams = {
|
||||||
|
Sets: {};
|
||||||
|
EditSet: {
|
||||||
|
set: Set;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [sets, setSets] = useState<Set[]>();
|
const navigation = useNavigation<DrawerNavigationProp<DrawerParamList>>();
|
||||||
const [offset, setOffset] = useState(0);
|
|
||||||
const [edit, setEdit] = useState<Set>();
|
|
||||||
const [showEdit, setShowEdit] = useState(false);
|
|
||||||
const [showNew, setShowNew] = useState(false);
|
|
||||||
const [newSet, setNewSet] = useState<Set>();
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
|
||||||
const [end, setEnd] = useState(false);
|
|
||||||
const db = useContext(DatabaseContext);
|
|
||||||
|
|
||||||
const selectSets = `
|
|
||||||
SELECT * from sets
|
|
||||||
WHERE name LIKE ?
|
|
||||||
ORDER BY created DESC
|
|
||||||
LIMIT ? OFFSET ?
|
|
||||||
`;
|
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
|
||||||
const [result] = await db.executeSql(selectSets, [`%${search}%`, limit, 0]);
|
|
||||||
if (!result) return setSets([]);
|
|
||||||
console.log(`${HomePage.name}.refresh:`, {search, limit});
|
|
||||||
setSets(result.rows.raw());
|
|
||||||
setOffset(0);
|
|
||||||
setEnd(false);
|
|
||||||
}, [search, db, selectSets]);
|
|
||||||
|
|
||||||
const refreshLoader = useCallback(async () => {
|
|
||||||
setRefreshing(true);
|
|
||||||
refresh().finally(() => setRefreshing(false));
|
|
||||||
}, [setRefreshing, refresh]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refresh();
|
|
||||||
}, [search, refresh]);
|
|
||||||
|
|
||||||
const renderItem = useCallback(
|
|
||||||
({item}: {item: Set}) => (
|
|
||||||
<SetItem
|
|
||||||
setNewSet={setNewSet}
|
|
||||||
item={item}
|
|
||||||
key={item.id}
|
|
||||||
setEdit={setEdit}
|
|
||||||
onRemove={refresh}
|
|
||||||
setShowEdit={setShowEdit}
|
|
||||||
setShowNew={setShowNew}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[setEdit, refresh, setNewSet],
|
|
||||||
);
|
|
||||||
|
|
||||||
const update = useCallback(async () => {
|
|
||||||
console.log('HomePage.update', {edit});
|
|
||||||
await db.executeSql(
|
|
||||||
`UPDATE sets SET name = ?, reps = ?, weight = ?, created = ?, unit = ? WHERE id = ?`,
|
|
||||||
[
|
|
||||||
edit?.name,
|
|
||||||
edit?.reps,
|
|
||||||
edit?.weight,
|
|
||||||
edit?.created,
|
|
||||||
edit?.unit,
|
|
||||||
edit?.id,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
setShowEdit(false);
|
|
||||||
await refresh();
|
|
||||||
}, [edit, setShowEdit, refresh, db]);
|
|
||||||
|
|
||||||
const add = useCallback(async () => {
|
|
||||||
if (
|
|
||||||
newSet?.name === undefined ||
|
|
||||||
newSet?.reps === 0 ||
|
|
||||||
newSet?.weight === 0
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
await db.executeSql(
|
|
||||||
`INSERT INTO sets(name, reps, weight, created, unit) VALUES (?,?,?,?,?)`,
|
|
||||||
[
|
|
||||||
newSet?.name,
|
|
||||||
newSet?.reps,
|
|
||||||
newSet?.weight,
|
|
||||||
new Date().toISOString(),
|
|
||||||
newSet?.unit || 'kg',
|
|
||||||
],
|
|
||||||
);
|
|
||||||
setShowNew(false);
|
|
||||||
await refresh();
|
|
||||||
const enabled = await AsyncStorage.getItem('alarmEnabled');
|
|
||||||
if (enabled !== 'true') return;
|
|
||||||
const minutes = await AsyncStorage.getItem('minutes');
|
|
||||||
const seconds = await AsyncStorage.getItem('seconds');
|
|
||||||
const milliseconds = Number(minutes) * 60 * 1000 + Number(seconds) * 1000;
|
|
||||||
NativeModules.AlarmModule.timer(milliseconds);
|
|
||||||
}, [newSet, setShowNew, refresh, db]);
|
|
||||||
|
|
||||||
const next = useCallback(async () => {
|
|
||||||
if (end) return;
|
|
||||||
setRefreshing(true);
|
|
||||||
const newOffset = offset + limit;
|
|
||||||
console.log(`${HomePage.name}.next:`, {
|
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
newOffset,
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
const [result] = await db
|
|
||||||
.executeSql(selectSets, [`%${search}%`, limit, newOffset])
|
|
||||||
.finally(() => setRefreshing(false));
|
|
||||||
if (result.rows.length === 0) return setEnd(true);
|
|
||||||
if (!sets) return;
|
|
||||||
setSets([...sets, ...result.rows.raw()]);
|
|
||||||
if (result.rows.length < limit) return setEnd(true);
|
|
||||||
setOffset(newOffset);
|
|
||||||
}, [search, end, offset, sets, db, selectSets]);
|
|
||||||
|
|
||||||
const getTodaysPlan = useCallback(async (): Promise<Plan[]> => {
|
|
||||||
const today = DAYS[new Date().getDay()];
|
|
||||||
const [result] = await db.executeSql(
|
|
||||||
`SELECT * FROM plans WHERE days LIKE ? LIMIT 1`,
|
|
||||||
[`%${today}%`],
|
|
||||||
);
|
|
||||||
return result.rows.raw();
|
|
||||||
}, [db]);
|
|
||||||
|
|
||||||
const getTodaysSets = useCallback(async (): Promise<Set[]> => {
|
|
||||||
const today = new Date().toISOString().split('T')[0];
|
|
||||||
const [result] = await db.executeSql(
|
|
||||||
`SELECT * FROM sets WHERE created LIKE ? ORDER BY created DESC`,
|
|
||||||
[`${today}%`],
|
|
||||||
);
|
|
||||||
return result.rows.raw();
|
|
||||||
}, [db]);
|
|
||||||
|
|
||||||
const onAdd = useCallback(async () => {
|
|
||||||
const created = new Date().toISOString();
|
|
||||||
setNewSet({created});
|
|
||||||
setShowNew(true);
|
|
||||||
if ((await AsyncStorage.getItem('predictiveSets')) === 'false') return;
|
|
||||||
const todaysPlan = await getTodaysPlan();
|
|
||||||
if (todaysPlan.length === 0) return;
|
|
||||||
console.log(`${HomePage.name}.onAdd: todaysPlan =`, todaysPlan);
|
|
||||||
const todaysSets = await getTodaysSets();
|
|
||||||
const todaysWorkouts = todaysPlan[0].workouts.split(',');
|
|
||||||
if (todaysSets.length === 0)
|
|
||||||
return setNewSet({created, name: todaysWorkouts[0]});
|
|
||||||
console.log(`${HomePage.name}.onAdd: todaysSets =`, todaysSets);
|
|
||||||
const count = todaysSets.filter(
|
|
||||||
set => set.name === todaysSets[0].name,
|
|
||||||
).length;
|
|
||||||
console.log(`${HomePage.name}.onAdd: count =`, count);
|
|
||||||
const maxSets = await AsyncStorage.getItem('maxSets');
|
|
||||||
if (count < Number(maxSets))
|
|
||||||
return setNewSet({...todaysSets[0], id: undefined, created});
|
|
||||||
const nextWorkout =
|
|
||||||
todaysWorkouts[todaysWorkouts.indexOf(todaysSets[0].name!) + 1];
|
|
||||||
if (!nextWorkout)
|
|
||||||
return setNewSet({...todaysSets[0], id: undefined, created});
|
|
||||||
console.log(`${HomePage.name}.onAdd: nextWorkout =`, nextWorkout);
|
|
||||||
setNewSet({created, name: nextWorkout});
|
|
||||||
}, [getTodaysPlan, getTodaysSets, setNewSet, setShowNew]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<Stack.Navigator screenOptions={{headerShown: false}}>
|
||||||
<Searchbar placeholder="Search" value={search} onChangeText={setSearch} />
|
<Stack.Screen name="Sets" component={SetsPage} />
|
||||||
<FlatList
|
<Stack.Screen
|
||||||
data={sets}
|
name="EditSet"
|
||||||
style={{height: '100%'}}
|
component={EditSet}
|
||||||
ListEmptyComponent={
|
listeners={{
|
||||||
<List.Item
|
focus: () => {
|
||||||
title="No sets yet"
|
navigation.setOptions({
|
||||||
description="A set is a group of repetitions. E.g. 8 reps of Squats."
|
headerLeft: () => (
|
||||||
|
<IconButton icon="arrow-back" onPress={navigation.goBack} />
|
||||||
|
),
|
||||||
|
title: 'Set',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeRemove: () => {
|
||||||
|
navigation.setOptions({
|
||||||
|
headerLeft: () => (
|
||||||
|
<IconButton icon="menu" onPress={navigation.openDrawer} />
|
||||||
|
),
|
||||||
|
title: 'Home',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
</Stack.Navigator>
|
||||||
renderItem={renderItem}
|
|
||||||
keyExtractor={set => set.id!.toString()}
|
|
||||||
onEndReached={next}
|
|
||||||
refreshing={refreshing}
|
|
||||||
onRefresh={refreshLoader}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditSet
|
|
||||||
set={edit}
|
|
||||||
setSet={setEdit}
|
|
||||||
title={`Edit ${edit?.name}`}
|
|
||||||
saveText="Edit"
|
|
||||||
onSave={update}
|
|
||||||
show={showEdit}
|
|
||||||
setShow={setShowEdit}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditSet
|
|
||||||
set={newSet}
|
|
||||||
setSet={setNewSet}
|
|
||||||
title="Add set"
|
|
||||||
saveText="Add"
|
|
||||||
onSave={add}
|
|
||||||
show={showNew}
|
|
||||||
setShow={setShowNew}
|
|
||||||
/>
|
|
||||||
<MassiveFab onPress={onAdd} />
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flexGrow: 1,
|
|
||||||
padding: 10,
|
|
||||||
paddingBottom: '10%',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
27
SetItem.tsx
27
SetItem.tsx
|
@ -1,27 +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 {StackParams} from './HomePage';
|
||||||
import Set from './set';
|
import Set from './set';
|
||||||
|
|
||||||
export default function SetItem({
|
export default function SetItem({
|
||||||
item,
|
item,
|
||||||
setEdit,
|
|
||||||
setShowEdit,
|
|
||||||
onRemove,
|
onRemove,
|
||||||
setNewSet,
|
|
||||||
setShowNew,
|
|
||||||
}: {
|
}: {
|
||||||
item: Set;
|
item: Set;
|
||||||
setEdit: (set: Set) => void;
|
|
||||||
setNewSet: (set: Set) => void;
|
|
||||||
setShowEdit: (show: boolean) => void;
|
|
||||||
setShowNew: (show: boolean) => void;
|
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
}) {
|
}) {
|
||||||
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 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]);
|
||||||
|
@ -30,12 +25,11 @@ export default function SetItem({
|
||||||
}, [setShowMenu, db, onRemove, item.id]);
|
}, [setShowMenu, db, onRemove, item.id]);
|
||||||
|
|
||||||
const copy = useCallback(() => {
|
const copy = useCallback(() => {
|
||||||
const set = {...item};
|
const set: Set = {...item};
|
||||||
delete set.id;
|
set.created = new Date().toISOString();
|
||||||
setNewSet(set);
|
set.id = 0;
|
||||||
setShowMenu(false);
|
navigation.navigate('EditSet', {set});
|
||||||
setShowNew(true);
|
}, [navigation]);
|
||||||
}, [setNewSet, setShowMenu, item, setShowNew]);
|
|
||||||
|
|
||||||
const longPress = useCallback(
|
const longPress = useCallback(
|
||||||
(e: GestureResponderEvent) => {
|
(e: GestureResponderEvent) => {
|
||||||
|
@ -48,10 +42,7 @@ export default function SetItem({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<List.Item
|
<List.Item
|
||||||
onPress={() => {
|
onPress={() => navigation.navigate('EditSet', {set: item})}
|
||||||
setEdit(item);
|
|
||||||
setShowEdit(true);
|
|
||||||
}}
|
|
||||||
title={item.name}
|
title={item.name}
|
||||||
description={`${item.reps} x ${item.weight}${item.unit}`}
|
description={`${item.reps} x ${item.weight}${item.unit}`}
|
||||||
onLongPress={longPress}
|
onLongPress={longPress}
|
||||||
|
|
126
SetsPage.tsx
Normal file
126
SetsPage.tsx
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
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 {StackParams} from './HomePage';
|
||||||
|
import MassiveFab from './MassiveFab';
|
||||||
|
import Set from './set';
|
||||||
|
import SetItem from './SetItem';
|
||||||
|
|
||||||
|
const limit = 15;
|
||||||
|
|
||||||
|
export default function SetsPage() {
|
||||||
|
const [sets, setSets] = useState<Set[]>();
|
||||||
|
const [offset, setOffset] = useState(0);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [end, setEnd] = useState(false);
|
||||||
|
const db = useContext(DatabaseContext);
|
||||||
|
const navigation = useNavigation<NavigationProp<StackParams>>();
|
||||||
|
|
||||||
|
const selectSets = `
|
||||||
|
SELECT * from sets
|
||||||
|
WHERE name LIKE ?
|
||||||
|
ORDER BY created DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
const [result] = await db.executeSql(selectSets, [`%${search}%`, limit, 0]);
|
||||||
|
if (!result) return setSets([]);
|
||||||
|
console.log(`${SetsPage.name}.refresh:`, {search, limit});
|
||||||
|
setSets(result.rows.raw());
|
||||||
|
setOffset(0);
|
||||||
|
setEnd(false);
|
||||||
|
}, [search, db, selectSets]);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
refresh();
|
||||||
|
}, [refresh]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshLoader = useCallback(async () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
refresh().finally(() => setRefreshing(false));
|
||||||
|
}, [setRefreshing, refresh]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!search) return;
|
||||||
|
refresh();
|
||||||
|
}, [search, refresh]);
|
||||||
|
|
||||||
|
const renderItem = useCallback(
|
||||||
|
({item}: {item: Set}) => (
|
||||||
|
<SetItem item={item} key={item.id} onRemove={refresh} />
|
||||||
|
),
|
||||||
|
[refresh],
|
||||||
|
);
|
||||||
|
|
||||||
|
const next = useCallback(async () => {
|
||||||
|
if (end) return;
|
||||||
|
setRefreshing(true);
|
||||||
|
const newOffset = offset + limit;
|
||||||
|
console.log(`${SetsPage.name}.next:`, {
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
newOffset,
|
||||||
|
search,
|
||||||
|
});
|
||||||
|
const [result] = await db
|
||||||
|
.executeSql(selectSets, [`%${search}%`, limit, newOffset])
|
||||||
|
.finally(() => setRefreshing(false));
|
||||||
|
if (result.rows.length === 0) return setEnd(true);
|
||||||
|
if (!sets) return;
|
||||||
|
setSets([...sets, ...result.rows.raw()]);
|
||||||
|
if (result.rows.length < limit) return setEnd(true);
|
||||||
|
setOffset(newOffset);
|
||||||
|
}, [search, end, offset, sets, db, selectSets]);
|
||||||
|
|
||||||
|
const onAdd = useCallback(async () => {
|
||||||
|
const set: Set = {
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
name: '',
|
||||||
|
id: 0,
|
||||||
|
reps: 0,
|
||||||
|
weight: 0,
|
||||||
|
unit: 'kg',
|
||||||
|
};
|
||||||
|
navigation.navigate('EditSet', {set});
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Searchbar placeholder="Search" value={search} onChangeText={setSearch} />
|
||||||
|
<FlatList
|
||||||
|
data={sets}
|
||||||
|
style={{height: '100%'}}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<List.Item
|
||||||
|
title="No sets yet"
|
||||||
|
description="A set is a group of repetitions. E.g. 8 reps of Squats."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={set => set.id!.toString()}
|
||||||
|
onEndReached={next}
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={refreshLoader}
|
||||||
|
/>
|
||||||
|
<MassiveFab onPress={onAdd} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexGrow: 1,
|
||||||
|
padding: 10,
|
||||||
|
paddingBottom: '10%',
|
||||||
|
},
|
||||||
|
});
|
|
@ -13,7 +13,7 @@
|
||||||
"@babel/preset-env": "^7.1.6",
|
"@babel/preset-env": "^7.1.6",
|
||||||
"@react-native-async-storage/async-storage": "^1.17.7",
|
"@react-native-async-storage/async-storage": "^1.17.7",
|
||||||
"@react-native-community/datetimepicker": "^6.2.0",
|
"@react-native-community/datetimepicker": "^6.2.0",
|
||||||
"@react-navigation/material-top-tabs": "^6.2.1",
|
"@react-navigation/drawer": "^6.4.3",
|
||||||
"@react-navigation/native": "^6.0.10",
|
"@react-navigation/native": "^6.0.10",
|
||||||
"@react-navigation/native-stack": "^6.6.2",
|
"@react-navigation/native-stack": "^6.6.2",
|
||||||
"@types/d3-shape": "^3.1.0",
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
|
10
set.ts
10
set.ts
|
@ -1,8 +1,8 @@
|
||||||
export default interface Set {
|
export default interface Set {
|
||||||
id?: number;
|
id: number;
|
||||||
name?: string;
|
name: string;
|
||||||
reps?: number | string;
|
reps: number;
|
||||||
weight?: number | string;
|
weight: number;
|
||||||
created?: string;
|
created: string;
|
||||||
unit?: string;
|
unit?: string;
|
||||||
}
|
}
|
||||||
|
|
3
time.ts
3
time.ts
|
@ -23,8 +23,7 @@ export const MONTH = [
|
||||||
'December',
|
'December',
|
||||||
];
|
];
|
||||||
|
|
||||||
export function format(iso: string) {
|
export function format(date: Date) {
|
||||||
const date = new Date(iso);
|
|
||||||
const mm = MONTH[date.getMonth()];
|
const mm = MONTH[date.getMonth()];
|
||||||
const dd = date.getDate().toString();
|
const dd = date.getDate().toString();
|
||||||
const day = DAYS[date.getDay()];
|
const day = DAYS[date.getDay()];
|
||||||
|
|
32
yarn.lock
32
yarn.lock
|
@ -1636,18 +1636,24 @@
|
||||||
query-string "^7.0.0"
|
query-string "^7.0.0"
|
||||||
react-is "^16.13.0"
|
react-is "^16.13.0"
|
||||||
|
|
||||||
|
"@react-navigation/drawer@^6.4.3":
|
||||||
|
version "6.4.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@react-navigation/drawer/-/drawer-6.4.3.tgz#cc00a3d5814b62f9ab6476d2677217ba81af4ea3"
|
||||||
|
integrity sha512-qdOwZxrJe05uaDzpLPhEvXqvt0iop0AGlaGYpc3nnG44KYpzpym8/Zxoy6dIw0pn5ZPU0afvKqm+UxS2Uoz+ZQ==
|
||||||
|
dependencies:
|
||||||
|
"@react-navigation/elements" "^1.3.4"
|
||||||
|
color "^4.2.3"
|
||||||
|
warn-once "^0.1.0"
|
||||||
|
|
||||||
"@react-navigation/elements@^1.3.3":
|
"@react-navigation/elements@^1.3.3":
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.3.tgz#9f56b650a9a1a8263a271628be7342c8121d1788"
|
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.3.tgz#9f56b650a9a1a8263a271628be7342c8121d1788"
|
||||||
integrity sha512-Lv2lR7si5gNME8dRsqz57d54m4FJtrwHRjNQLOyQO546ZxO+g864cSvoLC6hQedQU0+IJnPTsZiEI2hHqfpEpw==
|
integrity sha512-Lv2lR7si5gNME8dRsqz57d54m4FJtrwHRjNQLOyQO546ZxO+g864cSvoLC6hQedQU0+IJnPTsZiEI2hHqfpEpw==
|
||||||
|
|
||||||
"@react-navigation/material-top-tabs@^6.2.1":
|
"@react-navigation/elements@^1.3.4":
|
||||||
version "6.2.1"
|
version "1.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/@react-navigation/material-top-tabs/-/material-top-tabs-6.2.1.tgz#747d17bd0138a7d50c791e9cce1adc350904f67d"
|
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.4.tgz#abb48136508c1e60862dc14a101ce02ff685a337"
|
||||||
integrity sha512-fGy2+/7cUAyrZUPUhzKUttA4avK5Z2FrNk0LRI04hRlAVqs5DYpnaQGkegGeOGwKmnxaVCjCVPr+KoEroyqduw==
|
integrity sha512-O0jICpjn3jskVo4yiWzZozmj7DZy1ZBbn3O7dbenuUjZSj/cscjwaapmZZFGcI/IMmjmx8UTKsybhCFEIbGf3g==
|
||||||
dependencies:
|
|
||||||
color "^3.1.3"
|
|
||||||
warn-once "^0.1.0"
|
|
||||||
|
|
||||||
"@react-navigation/native-stack@^6.6.2":
|
"@react-navigation/native-stack@^6.6.2":
|
||||||
version "6.6.2"
|
version "6.6.2"
|
||||||
|
@ -2879,7 +2885,7 @@ color-name@^1.0.0, color-name@~1.1.4:
|
||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||||
|
|
||||||
color-string@^1.5.2, color-string@^1.6.0:
|
color-string@^1.5.2, color-string@^1.6.0, color-string@^1.9.0:
|
||||||
version "1.9.1"
|
version "1.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
|
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
|
||||||
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
|
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
|
||||||
|
@ -2895,7 +2901,7 @@ color@^2.0.1:
|
||||||
color-convert "^1.9.1"
|
color-convert "^1.9.1"
|
||||||
color-string "^1.5.2"
|
color-string "^1.5.2"
|
||||||
|
|
||||||
color@^3.1.2, color@^3.1.3:
|
color@^3.1.2:
|
||||||
version "3.2.1"
|
version "3.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164"
|
resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164"
|
||||||
integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==
|
integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==
|
||||||
|
@ -2903,6 +2909,14 @@ color@^3.1.2, color@^3.1.3:
|
||||||
color-convert "^1.9.3"
|
color-convert "^1.9.3"
|
||||||
color-string "^1.6.0"
|
color-string "^1.6.0"
|
||||||
|
|
||||||
|
color@^4.2.3:
|
||||||
|
version "4.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
|
||||||
|
integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
|
||||||
|
dependencies:
|
||||||
|
color-convert "^2.0.1"
|
||||||
|
color-string "^1.9.0"
|
||||||
|
|
||||||
colorette@^1.0.7:
|
colorette@^1.0.7:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
|
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user