Merge branch 'add-simple-tabada-timer' into 'main'
add simple timers tabada See merge request torpenn/boxons!1merge-requests/10/head
commit
1e2376fac7
|
|
@ -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
|
||||
*.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 { StyleSheet } from 'react-native';
|
||||
|
||||
import { Text, View } from '@/components/Themed';
|
||||
import { Text, View } from '@/components/shared/Themed';
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
return (
|
||||
|
|
@ -9,10 +9,6 @@ export default function NotFoundScreen() {
|
|||
<Stack.Screen options={{ title: 'Oops!' }} />
|
||||
<View style={styles.container}>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { theme } from '@/app/shared/theme';
|
||||
import { useFonts } from 'expo-font';
|
||||
import { Stack } from 'expo-router';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { useEffect } from 'react';
|
||||
import 'react-native-reanimated';
|
||||
|
||||
import { useColorScheme } from '@/components/useColorScheme';
|
||||
|
||||
export {
|
||||
// Catch any errors thrown by the Layout component.
|
||||
ErrorBoundary,
|
||||
|
|
@ -15,7 +14,7 @@ export {
|
|||
|
||||
export const unstable_settings = {
|
||||
// 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.
|
||||
|
|
@ -46,13 +45,11 @@ export default function RootLayout() {
|
|||
}
|
||||
|
||||
function RootLayoutNav() {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
return (
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
|
||||
<Stack.Screen name="dashboard" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="timer" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</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) {
|
||||
api.cache(true);
|
||||
|
||||
return {
|
||||
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 Colors from '@/constants/Colors';
|
||||
import { useColorScheme } from './useColorScheme';
|
||||
import { useColorScheme } from '@/components/useColorScheme';
|
||||
|
||||
type ThemeProps = {
|
||||
lightColor?: string;
|
||||
darkColor?: string;
|
||||
};
|
||||
|
||||
export type TextProps = ThemeProps & DefaultText['props'];
|
||||
export type ViewProps = ThemeProps & DefaultView['props'];
|
||||
type TextProps = ThemeProps & DefaultText['props'];
|
||||
type ViewProps = ThemeProps & DefaultView['props'];
|
||||
|
||||
export function useThemeColor(
|
||||
function useThemeColor(
|
||||
props: { light?: string; dark?: string },
|
||||
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",
|
||||
"ios": "expo start --ios",
|
||||
"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": {
|
||||
"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": {
|
||||
"@emotion/native": "^11.11.0",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@expo/vector-icons": "^14.0.2",
|
||||
"@react-native-picker/picker": "^2.8.1",
|
||||
"@react-navigation/native": "^6.0.2",
|
||||
"@testing-library/react-native": "^12.7.2",
|
||||
"expo": "~51.0.28",
|
||||
"expo-font": "~12.0.9",
|
||||
"expo-linking": "~6.3.1",
|
||||
|
|
@ -27,14 +41,27 @@
|
|||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.74.5",
|
||||
"react-native-linear-gradient": "^2.8.3",
|
||||
"react-native-reanimated": "~3.10.1",
|
||||
"react-native-safe-area-context": "4.10.5",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@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-expo": "~51.0.3",
|
||||
"react-test-renderer": "18.2.0",
|
||||
|
|
|
|||
Loading…
Reference in New Issue