add simple timers tabada
parent
1f53fba768
commit
6aa40a3a6b
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
package.json
|
||||||
|
.eslintrc.js
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
},
|
||||||
|
extends: ['plugin:react/recommended'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
root: true,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: ['react-native', 'jest', 'simple-import-sort', '@typescript-eslint'],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/method-signature-style": [
|
||||||
|
"error",
|
||||||
|
"property"
|
||||||
|
],
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'react/no-unescaped-entities': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -33,3 +33,9 @@ yarn-error.*
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
|
||||||
|
# The following patterns were generated by expo-cli
|
||||||
|
|
||||||
|
expo-env.d.ts
|
||||||
|
# @end expo-cli
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
image: node:20
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- install
|
||||||
|
- lint
|
||||||
|
- test
|
||||||
|
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- node_modules/
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- npm install
|
||||||
|
- apt-get update
|
||||||
|
|
||||||
|
install_dependencies:
|
||||||
|
stage: install
|
||||||
|
script:
|
||||||
|
- npm install
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- node_modules/
|
||||||
|
expire_in: 1 week
|
||||||
|
|
||||||
|
lint:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- npm run lint
|
||||||
|
- npm run types
|
||||||
|
only:
|
||||||
|
- branches
|
||||||
|
- merge_requests
|
||||||
|
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- npm test
|
||||||
|
only:
|
||||||
|
- branches
|
||||||
|
- merge_requests
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
npm run types && npm run lint
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
|
||||||
import { Link, Tabs } from 'expo-router';
|
|
||||||
import { Pressable } from 'react-native';
|
|
||||||
|
|
||||||
import Colors from '@/constants/Colors';
|
|
||||||
import { useColorScheme } from '@/components/useColorScheme';
|
|
||||||
import { useClientOnlyValue } from '@/components/useClientOnlyValue';
|
|
||||||
|
|
||||||
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
|
|
||||||
function TabBarIcon(props: {
|
|
||||||
name: React.ComponentProps<typeof FontAwesome>['name'];
|
|
||||||
color: string;
|
|
||||||
}) {
|
|
||||||
return <FontAwesome size={28} style={{ marginBottom: -3 }} {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TabLayout() {
|
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tabs
|
|
||||||
screenOptions={{
|
|
||||||
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
|
|
||||||
// Disable the static render of the header on web
|
|
||||||
// to prevent a hydration error in React Navigation v6.
|
|
||||||
headerShown: useClientOnlyValue(false, true),
|
|
||||||
}}>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="index"
|
|
||||||
options={{
|
|
||||||
title: 'Tab One',
|
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
|
||||||
headerRight: () => (
|
|
||||||
<Link href="/modal" asChild>
|
|
||||||
<Pressable>
|
|
||||||
{({ pressed }) => (
|
|
||||||
<FontAwesome
|
|
||||||
name="info-circle"
|
|
||||||
size={25}
|
|
||||||
color={Colors[colorScheme ?? 'light'].text}
|
|
||||||
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="two"
|
|
||||||
options={{
|
|
||||||
title: 'Tab Two',
|
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { StyleSheet } from 'react-native';
|
|
||||||
|
|
||||||
import EditScreenInfo from '@/components/EditScreenInfo';
|
|
||||||
import { Text, View } from '@/components/Themed';
|
|
||||||
|
|
||||||
export default function TabOneScreen() {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text style={styles.title}>Tab One</Text>
|
|
||||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
|
|
||||||
<EditScreenInfo path="app/(tabs)/index.tsx" />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
separator: {
|
|
||||||
marginVertical: 30,
|
|
||||||
height: 1,
|
|
||||||
width: '80%',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { StyleSheet } from 'react-native';
|
|
||||||
|
|
||||||
import EditScreenInfo from '@/components/EditScreenInfo';
|
|
||||||
import { Text, View } from '@/components/Themed';
|
|
||||||
|
|
||||||
export default function TabTwoScreen() {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text style={styles.title}>Tab Two</Text>
|
|
||||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
|
|
||||||
<EditScreenInfo path="app/(tabs)/two.tsx" />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
separator: {
|
|
||||||
marginVertical: 30,
|
|
||||||
height: 1,
|
|
||||||
width: '80%',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Link, Stack } from 'expo-router';
|
import { Link, Stack } from 'expo-router';
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
|
|
||||||
import { Text, View } from '@/components/Themed';
|
import { Text, View } from '@/components/shared/Themed';
|
||||||
|
|
||||||
export default function NotFoundScreen() {
|
export default function NotFoundScreen() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -9,10 +9,6 @@ export default function NotFoundScreen() {
|
||||||
<Stack.Screen options={{ title: 'Oops!' }} />
|
<Stack.Screen options={{ title: 'Oops!' }} />
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.title}>This screen doesn't exist.</Text>
|
<Text style={styles.title}>This screen doesn't exist.</Text>
|
||||||
|
|
||||||
<Link href="/" style={styles.link}>
|
|
||||||
<Text style={styles.linkText}>Go to home screen!</Text>
|
|
||||||
</Link>
|
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export type RootStackParamList = {
|
||||||
|
dashboard: undefined;
|
||||||
|
timer: { reps: number, restTime: number, workTime: number};
|
||||||
|
};
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
import FontAwesome from '@expo/vector-icons/FontAwesome';
|
||||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
import { ThemeProvider } from '@emotion/react';
|
||||||
|
import { theme } from '@/app/shared/theme';
|
||||||
import { useFonts } from 'expo-font';
|
import { useFonts } from 'expo-font';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import * as SplashScreen from 'expo-splash-screen';
|
import * as SplashScreen from 'expo-splash-screen';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import 'react-native-reanimated';
|
import 'react-native-reanimated';
|
||||||
|
|
||||||
import { useColorScheme } from '@/components/useColorScheme';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
// Catch any errors thrown by the Layout component.
|
// Catch any errors thrown by the Layout component.
|
||||||
ErrorBoundary,
|
ErrorBoundary,
|
||||||
|
|
@ -15,7 +14,7 @@ export {
|
||||||
|
|
||||||
export const unstable_settings = {
|
export const unstable_settings = {
|
||||||
// Ensure that reloading on `/modal` keeps a back button present.
|
// Ensure that reloading on `/modal` keeps a back button present.
|
||||||
initialRouteName: '(tabs)',
|
initialRouteName: 'dashboard',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||||
|
|
@ -46,13 +45,11 @@ export default function RootLayout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function RootLayoutNav() {
|
function RootLayoutNav() {
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
<Stack.Screen name="dashboard" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
|
<Stack.Screen name="timer" options={{ headerShown: false }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { Text } from '@/components/shared/Themed';
|
||||||
|
import Card from '@/components/shared/Card';
|
||||||
|
import styled from '@emotion/native';
|
||||||
|
import { formatTime } from '@/components/shared/business/timeHelpers';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
|
import { RootStackParamList } from '@/app/RootStackParamList';
|
||||||
|
import { VerticalSpacer } from '@/components/shared/Spacers';
|
||||||
|
import { TimerPickerModal } from 'react-native-timer-picker';
|
||||||
|
import Button from '@/components/shared/Button';
|
||||||
|
import NumberSelector from '@/components/shared/NumberSelector';
|
||||||
|
|
||||||
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'timer'>;
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const navigation = useNavigation<NavigationProp>();
|
||||||
|
|
||||||
|
const [reps, setReps] = useState<number>(1);
|
||||||
|
const [workTime, setWorkTime] = useState<number>(0);
|
||||||
|
const [restTime, setRestTime] = useState<number>(0);
|
||||||
|
const [showWorkTimePicker, setShowWorkTimePicker] = useState<boolean>(false);
|
||||||
|
const [showRestTimePicker, setShowRestTimePicker] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const totalWorkTime: string = formatTime((workTime + restTime) * reps);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Title>Boxons</Title>
|
||||||
|
|
||||||
|
<VerticalSpacer heightUnits={8} />
|
||||||
|
|
||||||
|
<CardContainer>
|
||||||
|
<Card backgroundColor="black">
|
||||||
|
<CardTextContainer>
|
||||||
|
<CustomText>Reps.</CustomText>
|
||||||
|
<NumberSelector reps={reps} setReps={setReps} />
|
||||||
|
</CardTextContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card backgroundColor="red" onPress={() => setShowWorkTimePicker(true)}>
|
||||||
|
<CardTextContainer>
|
||||||
|
<CustomText>Fight</CustomText>
|
||||||
|
<CustomText>{formatTime(workTime)}</CustomText>
|
||||||
|
</CardTextContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<TimerPickerModal
|
||||||
|
hideHours
|
||||||
|
visible={showWorkTimePicker}
|
||||||
|
setIsVisible={setShowWorkTimePicker}
|
||||||
|
onConfirm={(pickedDuration: any) => {
|
||||||
|
setWorkTime(pickedDuration.minutes * 60 + pickedDuration.seconds);
|
||||||
|
setShowWorkTimePicker(false);
|
||||||
|
}}
|
||||||
|
modalTitle="Set Round Time"
|
||||||
|
onCancel={() => setShowWorkTimePicker(false)}
|
||||||
|
closeOnOverlayPress
|
||||||
|
styles={{
|
||||||
|
theme: "dark",
|
||||||
|
}}
|
||||||
|
modalProps={{
|
||||||
|
overlayOpacity: 0.2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card backgroundColor="green" onPress={() => setShowRestTimePicker(true)}>
|
||||||
|
<CardTextContainer>
|
||||||
|
<CustomText>Repos</CustomText>
|
||||||
|
<CustomText>{formatTime(restTime)}</CustomText>
|
||||||
|
</CardTextContainer>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<TimerPickerModal
|
||||||
|
hideHours
|
||||||
|
visible={showRestTimePicker}
|
||||||
|
setIsVisible={setShowRestTimePicker}
|
||||||
|
onConfirm={(pickedDuration: any) => {
|
||||||
|
setRestTime(pickedDuration.minutes * 60 + pickedDuration.seconds);
|
||||||
|
setShowRestTimePicker(false);
|
||||||
|
}}
|
||||||
|
modalTitle="Set Rest Time"
|
||||||
|
onCancel={() => setShowRestTimePicker(false)}
|
||||||
|
closeOnOverlayPress
|
||||||
|
styles={{
|
||||||
|
theme: "dark",
|
||||||
|
}}
|
||||||
|
modalProps={{
|
||||||
|
overlayOpacity: 0.2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CardContainer>
|
||||||
|
|
||||||
|
<VerticalSpacer heightUnits={5} />
|
||||||
|
<Text>Temps total de travail : {totalWorkTime}</Text>
|
||||||
|
|
||||||
|
<VerticalSpacer heightUnits={5} />
|
||||||
|
|
||||||
|
<Button label="Commencer" onPress={() => navigation.navigate('timer', { reps, restTime, workTime})} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Title = styled(Text)(({ theme }) => ({
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 40,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: theme.colors.black
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Container = styled.View({
|
||||||
|
padding: 20
|
||||||
|
})
|
||||||
|
|
||||||
|
const CardTextContainer = styled.View({
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center'
|
||||||
|
})
|
||||||
|
|
||||||
|
const CardContainer = styled.View({
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
const CustomText = styled(Text)(({ theme }) => ({
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: theme.colors.white
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { StatusBar } from 'expo-status-bar';
|
|
||||||
import { Platform, StyleSheet } from 'react-native';
|
|
||||||
|
|
||||||
import EditScreenInfo from '@/components/EditScreenInfo';
|
|
||||||
import { Text, View } from '@/components/Themed';
|
|
||||||
|
|
||||||
export default function ModalScreen() {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text style={styles.title}>Modal</Text>
|
|
||||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
|
|
||||||
<EditScreenInfo path="app/modal.tsx" />
|
|
||||||
|
|
||||||
{/* Use a light status bar on iOS to account for the black space above the modal */}
|
|
||||||
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
separator: {
|
|
||||||
marginVertical: 30,
|
|
||||||
height: 1,
|
|
||||||
width: '80%',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const colors = {
|
||||||
|
black: '#000000',
|
||||||
|
white: '#FFFFFF',
|
||||||
|
red: '#FF4141',
|
||||||
|
green: '#3AC26E',
|
||||||
|
grey: '#F9F9F9',
|
||||||
|
} as const;
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { colors } from './colors';
|
||||||
|
import { layout } from './layout';
|
||||||
|
|
||||||
|
export const theme = {
|
||||||
|
colors,
|
||||||
|
layout
|
||||||
|
};
|
||||||
|
|
||||||
|
declare module '@emotion/react' {
|
||||||
|
export interface Theme {
|
||||||
|
colors: typeof colors;
|
||||||
|
layout: typeof layout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Theme = typeof theme;
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Dimensions, StyleSheet } from 'react-native';
|
||||||
|
|
||||||
|
const width = Dimensions.get('window').width;
|
||||||
|
const height = Dimensions.get('window').height;
|
||||||
|
|
||||||
|
export const layout = {
|
||||||
|
window: {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
},
|
||||||
|
isSmallDevice: width < 375, // 375 but with added 5 px
|
||||||
|
isRegularDevice: width >= 375 && width <= 420,
|
||||||
|
isLargeDevice: width > 420,
|
||||||
|
gridUnit: 4,
|
||||||
|
iconSize: {
|
||||||
|
xxl: 80,
|
||||||
|
larger: 40,
|
||||||
|
large: 32,
|
||||||
|
medium: 24,
|
||||||
|
small: 16,
|
||||||
|
xsmall: 10
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRoute } from '@react-navigation/native';
|
||||||
|
|
||||||
|
import { View } from '@/components/shared/Themed';
|
||||||
|
import styled from '@emotion/native';
|
||||||
|
import TimerContent from '@/components/useCases/timer/view/TimerContent';
|
||||||
|
import FinishContent from '@/components/useCases/timer/view/FinishContent';
|
||||||
|
import { TimerBgColor } from '@/components/useCases/timer/business/type';
|
||||||
|
|
||||||
|
interface TimerProps {
|
||||||
|
reps: number;
|
||||||
|
restTime: number;
|
||||||
|
workTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Timer() {
|
||||||
|
const route = useRoute();
|
||||||
|
const { reps, restTime, workTime } = route.params as TimerProps;
|
||||||
|
|
||||||
|
const [currentRep, setCurrentRep] = useState<number>(0);
|
||||||
|
const [timeLeft, setTimeLeft] = useState<number>(0);
|
||||||
|
const [isWorkPhase, setIsWorkPhase] = useState<boolean>(true);
|
||||||
|
const [isRunning, setIsRunning] = useState<boolean>(false);
|
||||||
|
const [isFinish, setIsFinish] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleStart();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
if (isRunning && timeLeft > 0) {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
setTimeLeft(timeLeft - 1);
|
||||||
|
}, 1000);
|
||||||
|
} else if (isRunning && timeLeft === 0) {
|
||||||
|
if (currentRep < reps) {
|
||||||
|
if (isWorkPhase) {
|
||||||
|
// If the work phase is over, move on to the rest phase
|
||||||
|
setIsWorkPhase(false);
|
||||||
|
setTimeLeft(restTime);
|
||||||
|
} else {
|
||||||
|
// If the rest phase is complete, move on to the next work phase
|
||||||
|
setIsWorkPhase(true);
|
||||||
|
setTimeLeft(workTime);
|
||||||
|
setCurrentRep(currentRep + 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Done
|
||||||
|
setIsRunning(false);
|
||||||
|
setIsFinish(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isRunning, timeLeft, currentRep, isWorkPhase, reps, workTime, restTime]);
|
||||||
|
|
||||||
|
const handleStart = () => {
|
||||||
|
setCurrentRep(1);
|
||||||
|
setIsWorkPhase(true);
|
||||||
|
setTimeLeft(workTime);
|
||||||
|
setIsRunning(true);
|
||||||
|
setIsFinish(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContine = () => {
|
||||||
|
setIsRunning(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStop = () => {
|
||||||
|
setIsRunning(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBgColor: () => TimerBgColor = () => {
|
||||||
|
if (isFinish) return 'black';
|
||||||
|
if (isWorkPhase) return 'red';
|
||||||
|
|
||||||
|
return 'green';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container bgColor={renderBgColor()}>
|
||||||
|
{isFinish && (
|
||||||
|
<FinishContent handleStart={handleStart} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isFinish && (
|
||||||
|
<TimerContent
|
||||||
|
isWorkPhase={isWorkPhase}
|
||||||
|
timeLeft={timeLeft}
|
||||||
|
reps={reps}
|
||||||
|
bgColor={renderBgColor()}
|
||||||
|
currentRep={currentRep}
|
||||||
|
isRunning={isRunning}
|
||||||
|
handleStop={handleStop}
|
||||||
|
handleContine={handleContine}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled(View)<{ bgColor: TimerBgColor }>(({ theme, bgColor }) => ({
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: theme.colors[bgColor]
|
||||||
|
}));
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
module.exports = function (api) {
|
module.exports = function (api) {
|
||||||
api.cache(true);
|
api.cache(true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
presets: ['babel-preset-expo']
|
presets: ['babel-preset-expo']
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { StyleSheet } from 'react-native';
|
|
||||||
|
|
||||||
import { ExternalLink } from './ExternalLink';
|
|
||||||
import { MonoText } from './StyledText';
|
|
||||||
import { Text, View } from './Themed';
|
|
||||||
|
|
||||||
import Colors from '@/constants/Colors';
|
|
||||||
|
|
||||||
export default function EditScreenInfo({ path }: { path: string }) {
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<View style={styles.getStartedContainer}>
|
|
||||||
<Text
|
|
||||||
style={styles.getStartedText}
|
|
||||||
lightColor="rgba(0,0,0,0.8)"
|
|
||||||
darkColor="rgba(255,255,255,0.8)">
|
|
||||||
Open up the code for this screen:
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[styles.codeHighlightContainer, styles.homeScreenFilename]}
|
|
||||||
darkColor="rgba(255,255,255,0.05)"
|
|
||||||
lightColor="rgba(0,0,0,0.05)">
|
|
||||||
<MonoText>{path}</MonoText>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
style={styles.getStartedText}
|
|
||||||
lightColor="rgba(0,0,0,0.8)"
|
|
||||||
darkColor="rgba(255,255,255,0.8)">
|
|
||||||
Change any of the text, save the file, and your app will automatically update.
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.helpContainer}>
|
|
||||||
<ExternalLink
|
|
||||||
style={styles.helpLink}
|
|
||||||
href="https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet">
|
|
||||||
<Text style={styles.helpLinkText} lightColor={Colors.light.tint}>
|
|
||||||
Tap here if your app doesn't automatically update after making changes
|
|
||||||
</Text>
|
|
||||||
</ExternalLink>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
getStartedContainer: {
|
|
||||||
alignItems: 'center',
|
|
||||||
marginHorizontal: 50,
|
|
||||||
},
|
|
||||||
homeScreenFilename: {
|
|
||||||
marginVertical: 7,
|
|
||||||
},
|
|
||||||
codeHighlightContainer: {
|
|
||||||
borderRadius: 3,
|
|
||||||
paddingHorizontal: 4,
|
|
||||||
},
|
|
||||||
getStartedText: {
|
|
||||||
fontSize: 17,
|
|
||||||
lineHeight: 24,
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
helpContainer: {
|
|
||||||
marginTop: 15,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
helpLink: {
|
|
||||||
paddingVertical: 15,
|
|
||||||
},
|
|
||||||
helpLinkText: {
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { Link } from 'expo-router';
|
|
||||||
import * as WebBrowser from 'expo-web-browser';
|
|
||||||
import React from 'react';
|
|
||||||
import { Platform } from 'react-native';
|
|
||||||
|
|
||||||
export function ExternalLink(
|
|
||||||
props: Omit<React.ComponentProps<typeof Link>, 'href'> & { href: string }
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
target="_blank"
|
|
||||||
{...props}
|
|
||||||
// @ts-expect-error: External URLs are not typed.
|
|
||||||
href={props.href}
|
|
||||||
onPress={(e) => {
|
|
||||||
if (Platform.OS !== 'web') {
|
|
||||||
// Prevent the default behavior of linking to the default browser on native.
|
|
||||||
e.preventDefault();
|
|
||||||
// Open the link in an in-app browser.
|
|
||||||
WebBrowser.openBrowserAsync(props.href as string);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { Text, TextProps } from './Themed';
|
|
||||||
|
|
||||||
export function MonoText(props: TextProps) {
|
|
||||||
return <Text {...props} style={[props.style, { fontFamily: 'SpaceMono' }]} />;
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import renderer from 'react-test-renderer';
|
|
||||||
|
|
||||||
import { MonoText } from '../StyledText';
|
|
||||||
|
|
||||||
it(`renders correctly`, () => {
|
|
||||||
const tree = renderer.create(<MonoText>Snapshot test!</MonoText>).toJSON();
|
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
|
import styled from '@emotion/native';
|
||||||
|
|
||||||
|
import { HorizontalSpacer } from '@/components/shared/Spacers';
|
||||||
|
import { Text } from '@/components/shared/Themed';
|
||||||
|
|
||||||
|
type ButtonColor = 'green' | 'red' | 'grey' | 'white';
|
||||||
|
type LabelColor = ButtonColor | 'black';
|
||||||
|
type IconLocation = 'left' | 'right';
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
label?: string;
|
||||||
|
color?: ButtonColor;
|
||||||
|
labelColor?: LabelColor;
|
||||||
|
status?: 'ready' | 'disabled' | 'loading';
|
||||||
|
onPress?: () => void;
|
||||||
|
secondary?: boolean;
|
||||||
|
iconLocation?: IconLocation;
|
||||||
|
icon?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button: React.FC<ButtonProps> = ({
|
||||||
|
label,
|
||||||
|
color = 'white',
|
||||||
|
labelColor = 'black',
|
||||||
|
status = 'ready',
|
||||||
|
onPress,
|
||||||
|
secondary = false,
|
||||||
|
icon = undefined,
|
||||||
|
iconLocation = 'right'
|
||||||
|
}: ButtonProps) => {
|
||||||
|
const safeOnPress = useCallback(() => onPress?.(), [onPress]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
secondary={secondary}
|
||||||
|
bgColor={color}
|
||||||
|
onPress={safeOnPress}
|
||||||
|
disabled={status !== 'ready'}
|
||||||
|
>
|
||||||
|
{icon && iconLocation === 'left' && (
|
||||||
|
<>
|
||||||
|
<IconContainer>{icon}</IconContainer>
|
||||||
|
{label && <HorizontalSpacer widthUnits={2} />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{secondary ? (
|
||||||
|
<SecondaryLabel bgColor={color}>{label}</SecondaryLabel>
|
||||||
|
) : (
|
||||||
|
<Label color={labelColor}>{label}</Label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{icon && iconLocation === 'right' && (
|
||||||
|
<>
|
||||||
|
{label && <HorizontalSpacer widthUnits={2} />}
|
||||||
|
<IconContainer>{icon}</IconContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
|
|
||||||
|
const Container = styled.TouchableOpacity<{
|
||||||
|
bgColor: ButtonColor;
|
||||||
|
disabled: boolean;
|
||||||
|
secondary: boolean;
|
||||||
|
}>(({ theme, bgColor, secondary }) => ({
|
||||||
|
backgroundColor: theme.colors[bgColor],
|
||||||
|
borderWidth: secondary ? 1 : 0,
|
||||||
|
borderColor: theme.colors[bgColor],
|
||||||
|
borderRadius: 6,
|
||||||
|
height: theme.layout.gridUnit * 12,
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingHorizontal: theme.layout.gridUnit * 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'row',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Label = styled(Text)<{ color: LabelColor }>(({ theme, color }) => ({
|
||||||
|
color: theme.colors[color],
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'semibold',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const SecondaryLabel = styled(Text)<{ bgColor: ButtonColor }>(({ theme, bgColor }) => ({
|
||||||
|
color: theme.colors[bgColor],
|
||||||
|
fontWeight: 'semibold',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const IconContainer = styled.View(({ theme }) => ({
|
||||||
|
height: theme.layout.iconSize.medium,
|
||||||
|
aspectRatio: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import styled from '@emotion/native';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { TouchableOpacity } from 'react-native';
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
backgroundColor: CardBackgroundColor;
|
||||||
|
testID?: string;
|
||||||
|
onPress?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CardBackgroundColor = 'red' | 'green' | 'grey' | 'black';
|
||||||
|
|
||||||
|
const Card: React.FC<CardProps> = ({
|
||||||
|
children,
|
||||||
|
backgroundColor = 'red',
|
||||||
|
testID = undefined,
|
||||||
|
onPress
|
||||||
|
}) => {
|
||||||
|
const safeOnPress = useCallback(() => onPress?.(), [onPress]);
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<Container bgColor={backgroundColor} testID={testID}>
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
const touchableContent = (
|
||||||
|
<TouchableOpacity onPress={safeOnPress}>
|
||||||
|
{content}
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
onPress ? touchableContent : content
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Container = styled.View<{
|
||||||
|
bgColor: CardBackgroundColor;
|
||||||
|
}>(({ theme, bgColor }) => ({
|
||||||
|
padding: 30,
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: 10,
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: theme.colors[bgColor]
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default Card;
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Text, View } from '@/components/shared/Themed';
|
||||||
|
import styled from '@emotion/native';
|
||||||
|
|
||||||
|
import { HorizontalSpacer } from './Spacers';
|
||||||
|
|
||||||
|
type FinishContentProps = {
|
||||||
|
reps: number;
|
||||||
|
setReps: (reps: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NumberSelector({
|
||||||
|
setReps,
|
||||||
|
reps
|
||||||
|
}: FinishContentProps) {
|
||||||
|
const addReps: () => void = () => {
|
||||||
|
setReps(reps + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeReps: () => void = () => {
|
||||||
|
if (reps === 0) return;
|
||||||
|
|
||||||
|
setReps(reps - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<CustomText onPress={removeReps}>-</CustomText>
|
||||||
|
<HorizontalSpacer widthUnits={5} />
|
||||||
|
<CustomText>{reps}</CustomText>
|
||||||
|
<HorizontalSpacer widthUnits={5} />
|
||||||
|
<CustomText onPress={addReps}>+</CustomText>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Row = styled(View)(() => ({
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: 'black'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const CustomText = styled(Text)(({ theme }) => ({
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: theme.colors.white
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import styled from '@emotion/native';
|
||||||
|
import { View, ViewProps } from 'react-native';
|
||||||
|
|
||||||
|
const Spacer = (props: Omit<ViewProps, 'pointerEvents'>) => (
|
||||||
|
<View pointerEvents="none" {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const VerticalSpacer = styled(Spacer)<{ heightUnits: number }>(({ theme, heightUnits }) => ({
|
||||||
|
height: theme.layout.gridUnit * heightUnits,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const HorizontalSpacer = styled(Spacer)<{ widthUnits: number }>(({ theme, widthUnits }) => ({
|
||||||
|
width: theme.layout.gridUnit * widthUnits,
|
||||||
|
}));
|
||||||
|
|
@ -6,17 +6,17 @@
|
||||||
import { Text as DefaultText, View as DefaultView } from 'react-native';
|
import { Text as DefaultText, View as DefaultView } from 'react-native';
|
||||||
|
|
||||||
import Colors from '@/constants/Colors';
|
import Colors from '@/constants/Colors';
|
||||||
import { useColorScheme } from './useColorScheme';
|
import { useColorScheme } from '@/components/useColorScheme';
|
||||||
|
|
||||||
type ThemeProps = {
|
type ThemeProps = {
|
||||||
lightColor?: string;
|
lightColor?: string;
|
||||||
darkColor?: string;
|
darkColor?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TextProps = ThemeProps & DefaultText['props'];
|
type TextProps = ThemeProps & DefaultText['props'];
|
||||||
export type ViewProps = ThemeProps & DefaultView['props'];
|
type ViewProps = ThemeProps & DefaultView['props'];
|
||||||
|
|
||||||
export function useThemeColor(
|
function useThemeColor(
|
||||||
props: { light?: string; dark?: string },
|
props: { light?: string; dark?: string },
|
||||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||||
) {
|
) {
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { fireEvent } from '@testing-library/react-native';
|
||||||
|
|
||||||
|
import { render } from '@/components/testUtils';
|
||||||
|
import Button from '@/components/shared/Button';
|
||||||
|
|
||||||
|
const renderComponent = ({ status, onPress } = {}) => {
|
||||||
|
const base = render(<Button label="Press me" status={status} onPress={onPress} />);
|
||||||
|
|
||||||
|
return base;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('[Component] Button', () => {
|
||||||
|
describe('Ready state', () => {
|
||||||
|
test('renders correctly', () => {
|
||||||
|
const button = renderComponent();
|
||||||
|
|
||||||
|
expect(button).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('executes an action when pressing it', () => {
|
||||||
|
const action = jest.fn();
|
||||||
|
const button = renderComponent({ onPress: action });
|
||||||
|
|
||||||
|
fireEvent.press(button.getByText('Press me'));
|
||||||
|
expect(action).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Disabled state', () => {
|
||||||
|
test('renders correctly', () => {
|
||||||
|
const button = renderComponent({ status: 'disabled' });
|
||||||
|
|
||||||
|
expect(button).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not do anything when pressing it', () => {
|
||||||
|
const action = jest.fn();
|
||||||
|
const button = renderComponent({ status: 'disabled', onPress: action });
|
||||||
|
|
||||||
|
fireEvent.press(button.getByText('Press me'));
|
||||||
|
expect(action).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Loading state', () => {
|
||||||
|
test('renders correctly', () => {
|
||||||
|
const button = renderComponent({ status: 'loading' });
|
||||||
|
|
||||||
|
expect(button).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not do anything when pressing it', () => {
|
||||||
|
const action = jest.fn();
|
||||||
|
const button = renderComponent({ status: 'loading', onPress: action });
|
||||||
|
|
||||||
|
fireEvent.press(button.getByText('Press me'));
|
||||||
|
expect(action).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { fireEvent } from '@testing-library/react-native';
|
||||||
|
|
||||||
|
import { render } from '@/components/testUtils';
|
||||||
|
import Card from '@/components/shared/Card';
|
||||||
|
import { Text } from 'react-native';
|
||||||
|
import { theme } from '@/app/shared/theme';
|
||||||
|
|
||||||
|
const renderComponent = ({ backgroundColor, onPress = undefined, content = 'content'} = {}) => {
|
||||||
|
const base = render(
|
||||||
|
<Card backgroundColor={backgroundColor} onPress={onPress} testID='card'>
|
||||||
|
<Text>{content}</Text>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
return base;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('[Component] Card', () => {
|
||||||
|
test('renders correctly', () => {
|
||||||
|
const card = renderComponent({ backgroundColor: 'red' });
|
||||||
|
|
||||||
|
expect(card).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders children correctly', () => {
|
||||||
|
const { getByText } = renderComponent({ backgroundColor: 'red', content: 'Test Content'});
|
||||||
|
|
||||||
|
expect(getByText('Test Content')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('applies the correct background color', () => {
|
||||||
|
const { getByTestId } = renderComponent({ backgroundColor: 'green'});
|
||||||
|
|
||||||
|
const card = getByTestId('card');
|
||||||
|
expect(card?.props.style[0].backgroundColor).toEqual(theme.colors.green);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not render as TouchableOpacity when onPress is not provided', () => {
|
||||||
|
const { queryByTestId } = renderComponent({ backgroundColor: 'red' });
|
||||||
|
|
||||||
|
// TouchableOpacity ne doit pas exister
|
||||||
|
expect(queryByTestId('card').props.onPress).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls onPress when pressed', () => {
|
||||||
|
const onPressMock = jest.fn();
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
backgroundColor: 'green',
|
||||||
|
onPress: onPressMock,
|
||||||
|
content: 'Press me'
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.press(getByTestId('card'));
|
||||||
|
expect(onPressMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { fireEvent } from '@testing-library/react-native';
|
||||||
|
import { render } from '@/components/testUtils';
|
||||||
|
import NumberSelector from '@/components/shared/NumberSelector';
|
||||||
|
|
||||||
|
const renderComponent = ({ reps = 5, setReps = jest.fn() } = {}) => {
|
||||||
|
const base = render(
|
||||||
|
<NumberSelector reps={reps} setReps={setReps} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return base;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('[Component] NumberSelect', () => {
|
||||||
|
test('renders correctly', () => {
|
||||||
|
const component = renderComponent();
|
||||||
|
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders the correct initial reps value', () => {
|
||||||
|
const { getByText } = renderComponent()
|
||||||
|
|
||||||
|
expect(getByText('5')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('increments reps when "+" is pressed', () => {
|
||||||
|
const setRepsMock = jest.fn();
|
||||||
|
const { getByText } = renderComponent({ setReps: setRepsMock });
|
||||||
|
|
||||||
|
fireEvent.press(getByText('+'));
|
||||||
|
|
||||||
|
expect(setRepsMock).toHaveBeenCalledWith(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decrements reps when "-" is pressed', () => {
|
||||||
|
const setRepsMock = jest.fn();
|
||||||
|
const { getByText } = renderComponent({ setReps: setRepsMock });
|
||||||
|
|
||||||
|
fireEvent.press(getByText('-'));
|
||||||
|
|
||||||
|
expect(setRepsMock).toHaveBeenCalledWith(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not decrement reps below 0', () => {
|
||||||
|
const setRepsMock = jest.fn();
|
||||||
|
const { getByText } = renderComponent({ reps: 0, setReps: setRepsMock });
|
||||||
|
|
||||||
|
fireEvent.press(getByText('-'));
|
||||||
|
|
||||||
|
expect(setRepsMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays the reps correctly after increment and decrement', () => {
|
||||||
|
let repsValue = 5;
|
||||||
|
const setRepsMock = jest.fn((newReps) => {
|
||||||
|
repsValue = newReps;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getByText, rerender } = renderComponent({ reps: repsValue, setReps: setRepsMock });
|
||||||
|
|
||||||
|
fireEvent.press(getByText('+'));
|
||||||
|
rerender(<NumberSelector reps={repsValue} setReps={setRepsMock} />);
|
||||||
|
|
||||||
|
expect(getByText('6')).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.press(getByText('-'));
|
||||||
|
rerender(<NumberSelector reps={repsValue} setReps={setRepsMock} />);
|
||||||
|
|
||||||
|
expect(getByText('5')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from '@/components/testUtils';
|
||||||
|
import { HorizontalSpacer, VerticalSpacer } from '@/components/shared/Spacers';
|
||||||
|
import { theme } from '@/app/shared/theme';
|
||||||
|
|
||||||
|
describe('[Component] Spacers', () => {
|
||||||
|
test('renders Vertical correctly', () => {
|
||||||
|
const component = render(
|
||||||
|
<VerticalSpacer heightUnits={3} testID="vertical-spacer" />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders HorizontalSpacer correctly', () => {
|
||||||
|
const component = render(
|
||||||
|
<HorizontalSpacer widthUnits={3} testID="horizontal-spacer" />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('applies correct height for VerticalSpacer based on heightUnits', () => {
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<VerticalSpacer heightUnits={3} testID="vertical-spacer" />
|
||||||
|
);
|
||||||
|
|
||||||
|
const spacer = getByTestId('vertical-spacer');
|
||||||
|
expect(spacer).toHaveStyle({ height: theme.layout.gridUnit * 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('applies correct width for HorizontalSpacer based on widthUnits', () => {
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<HorizontalSpacer widthUnits={4} testID="horizontal-spacer" />
|
||||||
|
);
|
||||||
|
|
||||||
|
const spacer = getByTestId('horizontal-spacer');
|
||||||
|
expect(spacer).toHaveStyle({ width: theme.layout.gridUnit * 4 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`[Component] Button Disabled state renders correctly 1`] = `
|
||||||
|
<View
|
||||||
|
accessibilityState={
|
||||||
|
{
|
||||||
|
"busy": undefined,
|
||||||
|
"checked": undefined,
|
||||||
|
"disabled": true,
|
||||||
|
"expanded": undefined,
|
||||||
|
"selected": undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accessibilityValue={
|
||||||
|
{
|
||||||
|
"max": undefined,
|
||||||
|
"min": undefined,
|
||||||
|
"now": undefined,
|
||||||
|
"text": undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accessible={true}
|
||||||
|
collapsable={false}
|
||||||
|
focusable={true}
|
||||||
|
onClick={[Function]}
|
||||||
|
onResponderGrant={[Function]}
|
||||||
|
onResponderMove={[Function]}
|
||||||
|
onResponderRelease={[Function]}
|
||||||
|
onResponderTerminate={[Function]}
|
||||||
|
onResponderTerminationRequest={[Function]}
|
||||||
|
onStartShouldSetResponder={[Function]}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"alignItems": "center",
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"borderColor": "#FFFFFF",
|
||||||
|
"borderRadius": 6,
|
||||||
|
"borderWidth": 0,
|
||||||
|
"flexDirection": "row",
|
||||||
|
"height": 48,
|
||||||
|
"justifyContent": "center",
|
||||||
|
"opacity": 1,
|
||||||
|
"paddingHorizontal": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
color="black"
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "#000",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "#000000",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": "semibold",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Press me
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`[Component] Button Loading state renders correctly 1`] = `
|
||||||
|
<View
|
||||||
|
accessibilityState={
|
||||||
|
{
|
||||||
|
"busy": undefined,
|
||||||
|
"checked": undefined,
|
||||||
|
"disabled": true,
|
||||||
|
"expanded": undefined,
|
||||||
|
"selected": undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accessibilityValue={
|
||||||
|
{
|
||||||
|
"max": undefined,
|
||||||
|
"min": undefined,
|
||||||
|
"now": undefined,
|
||||||
|
"text": undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accessible={true}
|
||||||
|
collapsable={false}
|
||||||
|
focusable={true}
|
||||||
|
onClick={[Function]}
|
||||||
|
onResponderGrant={[Function]}
|
||||||
|
onResponderMove={[Function]}
|
||||||
|
onResponderRelease={[Function]}
|
||||||
|
onResponderTerminate={[Function]}
|
||||||
|
onResponderTerminationRequest={[Function]}
|
||||||
|
onStartShouldSetResponder={[Function]}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"alignItems": "center",
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"borderColor": "#FFFFFF",
|
||||||
|
"borderRadius": 6,
|
||||||
|
"borderWidth": 0,
|
||||||
|
"flexDirection": "row",
|
||||||
|
"height": 48,
|
||||||
|
"justifyContent": "center",
|
||||||
|
"opacity": 1,
|
||||||
|
"paddingHorizontal": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
color="black"
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "#000",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "#000000",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": "semibold",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Press me
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`[Component] Button Ready state renders correctly 1`] = `
|
||||||
|
<View
|
||||||
|
accessibilityState={
|
||||||
|
{
|
||||||
|
"busy": undefined,
|
||||||
|
"checked": undefined,
|
||||||
|
"disabled": false,
|
||||||
|
"expanded": undefined,
|
||||||
|
"selected": undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accessibilityValue={
|
||||||
|
{
|
||||||
|
"max": undefined,
|
||||||
|
"min": undefined,
|
||||||
|
"now": undefined,
|
||||||
|
"text": undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accessible={true}
|
||||||
|
collapsable={false}
|
||||||
|
focusable={true}
|
||||||
|
onClick={[Function]}
|
||||||
|
onResponderGrant={[Function]}
|
||||||
|
onResponderMove={[Function]}
|
||||||
|
onResponderRelease={[Function]}
|
||||||
|
onResponderTerminate={[Function]}
|
||||||
|
onResponderTerminationRequest={[Function]}
|
||||||
|
onStartShouldSetResponder={[Function]}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"alignItems": "center",
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"borderColor": "#FFFFFF",
|
||||||
|
"borderRadius": 6,
|
||||||
|
"borderWidth": 0,
|
||||||
|
"flexDirection": "row",
|
||||||
|
"height": 48,
|
||||||
|
"justifyContent": "center",
|
||||||
|
"opacity": 1,
|
||||||
|
"paddingHorizontal": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
color="black"
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "#000",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "#000000",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": "semibold",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Press me
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`[Component] Card renders correctly 1`] = `
|
||||||
|
<View
|
||||||
|
bgColor="red"
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"backgroundColor": "#FF4141",
|
||||||
|
"borderRadius": 10,
|
||||||
|
"overflow": "hidden",
|
||||||
|
"padding": 30,
|
||||||
|
"width": "100%",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
testID="card"
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
content
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`[Component] NumberSelect renders correctly 1`] = `
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"backgroundColor": "#fff",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"backgroundColor": "black",
|
||||||
|
"flexDirection": "row",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
onPress={[Function]}
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "#000",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "#FFFFFF",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"textAlign": "center",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
pointerEvents="none"
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"width": 20,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
widthUnits={5}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "#000",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "#FFFFFF",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"textAlign": "center",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
5
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
pointerEvents="none"
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"width": 20,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
widthUnits={5}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
onPress={[Function]}
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "#000",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "#FFFFFF",
|
||||||
|
"fontSize": 20,
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"textAlign": "center",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`[Component] Spacers renders HorizontalSpacer correctly 1`] = `
|
||||||
|
<View
|
||||||
|
pointerEvents="none"
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"width": 12,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
testID="horizontal-spacer"
|
||||||
|
widthUnits={3}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`[Component] Spacers renders Vertical correctly 1`] = `
|
||||||
|
<View
|
||||||
|
heightUnits={3}
|
||||||
|
pointerEvents="none"
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"height": 12,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
testID="vertical-spacer"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { formatTime } from '../timeHelpers';
|
||||||
|
|
||||||
|
describe('formatTime', () => {
|
||||||
|
test('should format 0 seconds as "00:00"', () => {
|
||||||
|
expect(formatTime(0)).toBe('00:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should format seconds less than a minute correctly', () => {
|
||||||
|
expect(formatTime(45)).toBe('00:45');
|
||||||
|
expect(formatTime(9)).toBe('00:09');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should format exactly one minute correctly', () => {
|
||||||
|
expect(formatTime(60)).toBe('01:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should format minutes and seconds correctly', () => {
|
||||||
|
expect(formatTime(75)).toBe('01:15');
|
||||||
|
expect(formatTime(125)).toBe('02:05');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should format larger time values correctly', () => {
|
||||||
|
expect(formatTime(3600)).toBe('60:00');
|
||||||
|
expect(formatTime(3661)).toBe('61:01');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export function formatTime(seconds: number): string {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
const formattedSeconds = remainingSeconds < 10 ? `0${remainingSeconds}` : remainingSeconds;
|
||||||
|
const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes;
|
||||||
|
|
||||||
|
return `${formattedMinutes}:${formattedSeconds}`;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ThemeProvider } from '@emotion/react';
|
||||||
|
import { render as rntlRender, RenderAPI } from '@testing-library/react-native';
|
||||||
|
|
||||||
|
import { theme } from '@/app/shared/theme/index';
|
||||||
|
|
||||||
|
export const render = (
|
||||||
|
component: React.ReactElement<unknown>
|
||||||
|
): RenderAPI => {
|
||||||
|
const TestProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
return rntlRender(component, { wrapper: TestProvider });
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export type TimerBgColor = 'red' | 'green' | 'black';
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Text, View } from '@/components/shared/Themed';
|
||||||
|
import styled from '@emotion/native';
|
||||||
|
import { useNavigation } from 'expo-router';
|
||||||
|
import { NativeStackNavigationProp } from 'react-native-screens/lib/typescript/native-stack/types';
|
||||||
|
|
||||||
|
import Button from '@/components/shared/Button';
|
||||||
|
import { HorizontalSpacer, VerticalSpacer } from '@/components/shared/Spacers';
|
||||||
|
import { RootStackParamList } from '@/app/RootStackParamList';
|
||||||
|
|
||||||
|
type FinishContentProps = {
|
||||||
|
handleStart: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'dashboard'>;
|
||||||
|
|
||||||
|
export default function FinishContent({
|
||||||
|
handleStart
|
||||||
|
}: FinishContentProps) {
|
||||||
|
const navigation = useNavigation<NavigationProp>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Time>FIN</Time>
|
||||||
|
|
||||||
|
<VerticalSpacer heightUnits={8} />
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Button label="Recommencer" onPress={handleStart} />
|
||||||
|
|
||||||
|
<HorizontalSpacer widthUnits={3} />
|
||||||
|
|
||||||
|
<Button label="Retour" onPress={() => navigation.navigate('dashboard')} />
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Row = styled(View)(() => ({
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: 'black'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Time = styled(Text)(({ theme }) => ({
|
||||||
|
fontSize: 100,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: theme.colors.white
|
||||||
|
}));
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { Text, View } from '@/components/shared/Themed';
|
||||||
|
import styled from '@emotion/native';
|
||||||
|
import { formatTime } from '@/components/shared/business/timeHelpers';
|
||||||
|
import Button from '@/components/shared/Button';
|
||||||
|
import { VerticalSpacer } from '@/components/shared/Spacers';
|
||||||
|
import { TimerBgColor } from '@/components/useCases/timer/business/type';
|
||||||
|
import { useNavigation } from 'expo-router';
|
||||||
|
import { NativeStackNavigationProp } from 'react-native-screens/lib/typescript/native-stack/types';
|
||||||
|
import { RootStackParamList } from '@/app/RootStackParamList';
|
||||||
|
|
||||||
|
interface TimerContentProps {
|
||||||
|
isWorkPhase: boolean;
|
||||||
|
timeLeft: number;
|
||||||
|
reps: number;
|
||||||
|
currentRep: number;
|
||||||
|
isRunning: boolean;
|
||||||
|
handleStop: () => void;
|
||||||
|
handleContine: () => void;
|
||||||
|
bgColor: TimerBgColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NavigationProp = NativeStackNavigationProp<RootStackParamList, 'dashboard'>;
|
||||||
|
|
||||||
|
export default function TimerContent({
|
||||||
|
isWorkPhase,
|
||||||
|
timeLeft,
|
||||||
|
reps,
|
||||||
|
currentRep,
|
||||||
|
isRunning,
|
||||||
|
handleStop,
|
||||||
|
handleContine,
|
||||||
|
bgColor,
|
||||||
|
}: TimerContentProps ) {
|
||||||
|
const navigation = useNavigation<NavigationProp>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title>{isWorkPhase ? 'Fight' : 'Repos'}</Title>
|
||||||
|
|
||||||
|
<VerticalSpacer heightUnits={8} />
|
||||||
|
|
||||||
|
<Time>
|
||||||
|
{formatTime(timeLeft)}
|
||||||
|
</Time>
|
||||||
|
|
||||||
|
<VerticalSpacer heightUnits={8} />
|
||||||
|
|
||||||
|
<Reps>{currentRep} / {reps}</Reps>
|
||||||
|
|
||||||
|
<VerticalSpacer heightUnits={8} />
|
||||||
|
|
||||||
|
<ButtonContainer bgColor={bgColor}>
|
||||||
|
{isRunning ? (
|
||||||
|
<Button label="Stop" onPress={handleStop} />
|
||||||
|
) : (
|
||||||
|
<Button label="Start" onPress={handleContine} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<VerticalSpacer heightUnits={3} />
|
||||||
|
|
||||||
|
<Button label="Retour" onPress={() => navigation.navigate('dashboard')} />
|
||||||
|
</ButtonContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonContainer = styled(View)<{ bgColor: TimerBgColor }>(({ theme, bgColor }) => ({
|
||||||
|
backgroundColor: theme.colors[bgColor]
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Title = styled(Text)(({ theme }) => ({
|
||||||
|
fontSize: 50,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: theme.colors.white
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Time = styled(Text)(({ theme }) => ({
|
||||||
|
fontSize: 100,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: theme.colors.white
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Reps = styled(Text)(({ theme }) => ({
|
||||||
|
fontSize: 30,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: theme.colors.white
|
||||||
|
}));
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
// This function is web-only as native doesn't currently support server (or build-time) rendering.
|
|
||||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
// `useEffect` is not invoked during server rendering, meaning
|
|
||||||
// we can use this to determine if we're on the server or not.
|
|
||||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
|
|
||||||
const [value, setValue] = React.useState<S | C>(server);
|
|
||||||
React.useEffect(() => {
|
|
||||||
setValue(client);
|
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
// NOTE: The default React Native styling doesn't support server rendering.
|
|
||||||
// Server rendered styles should not change between the first render of the HTML
|
|
||||||
// and the first render on the client. Typically, web developers will use CSS media queries
|
|
||||||
// to render different styles on the client and server, these aren't directly supported in React Native
|
|
||||||
// but can be achieved using a styling library like Nativewind.
|
|
||||||
export function useColorScheme() {
|
|
||||||
return 'light';
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import '@testing-library/jest-native/extend-expect';
|
||||||
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
|
|
@ -7,15 +7,29 @@
|
||||||
"android": "expo start --android",
|
"android": "expo start --android",
|
||||||
"ios": "expo start --ios",
|
"ios": "expo start --ios",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
"test": "jest --watchAll"
|
"test:watch": "jest --watchAll",
|
||||||
|
"test": "jest",
|
||||||
|
"types": "tsc --noEmit",
|
||||||
|
"lint": "eslint --ext .ts,.tsx .",
|
||||||
|
"lint:fix": "eslint --fix --ext .ts,.tsx .",
|
||||||
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "jest-expo"
|
"preset": "jest-expo",
|
||||||
|
"setupFilesAfterEnv": [
|
||||||
|
"<rootDir>/jest.setup.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"babel-plugin-react-compiler": "0.0.0-experimental-734b737-20241003"
|
||||||
},
|
},
|
||||||
"overrides": { "babel-plugin-react-compiler": "0.0.0-experimental-734b737-20241003" },
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/native": "^11.11.0",
|
||||||
|
"@emotion/react": "^11.13.3",
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
|
"@react-native-picker/picker": "^2.8.1",
|
||||||
"@react-navigation/native": "^6.0.2",
|
"@react-navigation/native": "^6.0.2",
|
||||||
|
"@testing-library/react-native": "^12.7.2",
|
||||||
"expo": "~51.0.28",
|
"expo": "~51.0.28",
|
||||||
"expo-font": "~12.0.9",
|
"expo-font": "~12.0.9",
|
||||||
"expo-linking": "~6.3.1",
|
"expo-linking": "~6.3.1",
|
||||||
|
|
@ -27,14 +41,27 @@
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-native": "0.74.5",
|
"react-native": "0.74.5",
|
||||||
|
"react-native-linear-gradient": "^2.8.3",
|
||||||
"react-native-reanimated": "~3.10.1",
|
"react-native-reanimated": "~3.10.1",
|
||||||
"react-native-safe-area-context": "4.10.5",
|
"react-native-safe-area-context": "4.10.5",
|
||||||
"react-native-screens": "3.31.1",
|
"react-native-screens": "3.31.1",
|
||||||
|
"react-native-sound": "^0.11.2",
|
||||||
|
"react-native-timer-picker": "^1.10.3",
|
||||||
"react-native-web": "~0.19.10"
|
"react-native-web": "~0.19.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
|
"@testing-library/jest-native": "^5.4.3",
|
||||||
"@types/react": "~18.2.45",
|
"@types/react": "~18.2.45",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
|
"@typescript-eslint/parser": "^7.2.0",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-standard": "^17.1.0",
|
||||||
|
"eslint-plugin-jest": "^28.8.3",
|
||||||
|
"eslint-plugin-react": "^7.37.1",
|
||||||
|
"eslint-plugin-react-native": "^4.1.0",
|
||||||
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
|
"husky": "^9.1.6",
|
||||||
"jest": "^29.2.1",
|
"jest": "^29.2.1",
|
||||||
"jest-expo": "~51.0.3",
|
"jest-expo": "~51.0.3",
|
||||||
"react-test-renderer": "18.2.0",
|
"react-test-renderer": "18.2.0",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue