From 1e9cfad86a5496133025baf37638302890591dd6 Mon Sep 17 00:00:00 2001 From: Torpenn Date: Sun, 13 Oct 2024 22:33:44 +0200 Subject: [PATCH 1/3] doc: Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f02fca6..0cb6764 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Roadmap -- [] Create a simple timer (number of repetitions, rest, and exercise) with simple sound design. Button Skip Last reps +- [x] Create a simple timer (number of repetitions, rest, and exercise) with simple sound design. - [] Configure your own sounds - [] Be able to create exercise presets with a name - [] Have a history in the form of a list or diary From 397c14d590a3a9475fac58f46fa924619fef9467 Mon Sep 17 00:00:00 2001 From: Torpenn Date: Sun, 13 Oct 2024 23:17:48 +0200 Subject: [PATCH 2/3] feat: add AsyncStorage package and helpers --- components/shared/business/AsyncStorage.ts | 21 ++++++ .../business/__tests__/AsyncStorage-test.js | 64 +++++++++++++++++++ package-lock.json | 34 ++++++++++ package.json | 1 + 4 files changed, 120 insertions(+) create mode 100644 components/shared/business/AsyncStorage.ts create mode 100644 components/shared/business/__tests__/AsyncStorage-test.js diff --git a/components/shared/business/AsyncStorage.ts b/components/shared/business/AsyncStorage.ts new file mode 100644 index 0000000..f456d33 --- /dev/null +++ b/components/shared/business/AsyncStorage.ts @@ -0,0 +1,21 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +export const saveUserSettings = async (key: string, value: string) => { + try { + await AsyncStorage.setItem(key, value); + } catch (e) { + throw new Error('Failed to load the data from AsyncStorage'); + } +}; + +export const loadUserSettings = async (key: string) => { + try { + const value = await AsyncStorage.getItem(key); + + if (value !== null) { + return value; + } + } catch (e) { + throw new Error('Failed to load the data from AsyncStorage'); + } +}; diff --git a/components/shared/business/__tests__/AsyncStorage-test.js b/components/shared/business/__tests__/AsyncStorage-test.js new file mode 100644 index 0000000..fff529b --- /dev/null +++ b/components/shared/business/__tests__/AsyncStorage-test.js @@ -0,0 +1,64 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { saveUserSettings, loadUserSettings } from '../AsyncStorage'; + +// Mock de AsyncStorage +jest.mock('@react-native-async-storage/async-storage', () => ({ + setItem: jest.fn(), + getItem: jest.fn(), +})); + +describe('UserSettings functions', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('saveUserSettings', () => { + test('should save data to AsyncStorage', async () => { + const key = 'theme'; + const value = 'dark'; + + await saveUserSettings(key, value); + + expect(AsyncStorage.setItem).toHaveBeenCalledWith(key, value); + }); + + test('should throw an error if AsyncStorage.setItem fails', async () => { + AsyncStorage.setItem.mockRejectedValueOnce(new Error('AsyncStorage error')); + + await expect(saveUserSettings('theme', 'dark')).rejects.toThrow( + 'Failed to load the data from AsyncStorage' + ); + }); + }); + + describe('loadUserSettings', () => { + test('should load data from AsyncStorage', async () => { + const key = 'theme'; + const value = 'dark'; + + AsyncStorage.getItem.mockResolvedValueOnce(value); + + const result = await loadUserSettings(key); + + expect(result).toBe(value); + + expect(AsyncStorage.getItem).toHaveBeenCalledWith(key); + }); + + test('should return null if the value does not exist in AsyncStorage', async () => { + AsyncStorage.getItem.mockResolvedValueOnce(null); + + const result = await loadUserSettings('nonexistent_key'); + + expect(result).toBeUndefined(); + }); + + test('should throw an error if AsyncStorage.getItem fails', async () => { + AsyncStorage.getItem.mockRejectedValueOnce(new Error('AsyncStorage error')); + + await expect(loadUserSettings('theme')).rejects.toThrow( + 'Failed to load the data from AsyncStorage' + ); + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 78781bf..f963a94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@emotion/native": "^11.11.0", "@emotion/react": "^11.13.3", "@expo/vector-icons": "^14.0.2", + "@react-native-async-storage/async-storage": "^2.0.0", "@react-native-picker/picker": "2.7.5", "@react-navigation/native": "^6.0.2", "@testing-library/react-native": "^12.7.2", @@ -5085,6 +5086,18 @@ "react": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.0.0.tgz", + "integrity": "sha512-af6H9JjfL6G/PktBfUivvexoiFKQTJGQCtSWxMdivLzNIY94mu9DdiY0JqCSg/LyPCLGKhHPUlRQhNvpu3/KVA==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, "node_modules/@react-native-community/cli": { "version": "13.6.9", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-13.6.9.tgz", @@ -14133,6 +14146,15 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -17793,6 +17815,18 @@ "integrity": "sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==", "license": "BSD-2-Clause" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/package.json b/package.json index 2c39fac..dc59615 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@emotion/native": "^11.11.0", "@emotion/react": "^11.13.3", "@expo/vector-icons": "^14.0.2", + "@react-native-async-storage/async-storage": "^2.0.0", "@react-native-picker/picker": "2.7.5", "@react-navigation/native": "^6.0.2", "@testing-library/react-native": "^12.7.2", From 2a633cb3f4735665d2c5e1769fa2a591a346487f Mon Sep 17 00:00:00 2001 From: Torpenn Date: Sun, 13 Oct 2024 23:18:08 +0200 Subject: [PATCH 3/3] feat: add settings page --- app/+not-found.tsx | 2 +- app/RootStackParamList.ts | 5 +- app/Settings.tsx | 80 +++++++++++++++++++ app/_layout.tsx | 11 +-- app/dashboard.tsx | 8 +- app/timer.tsx | 21 ++++- .../useCases/timer/view/FinishContent.tsx | 4 +- .../useCases/timer/view/TimerContent.tsx | 4 +- 8 files changed, 118 insertions(+), 17 deletions(-) create mode 100644 app/Settings.tsx diff --git a/app/+not-found.tsx b/app/+not-found.tsx index 45c0b68..068a869 100644 --- a/app/+not-found.tsx +++ b/app/+not-found.tsx @@ -1,4 +1,4 @@ -import { Link, Stack } from 'expo-router'; +import { Stack } from 'expo-router'; import { StyleSheet } from 'react-native'; import { Text, View } from '@/components/shared/Themed'; diff --git a/app/RootStackParamList.ts b/app/RootStackParamList.ts index 309d108..fe7d722 100644 --- a/app/RootStackParamList.ts +++ b/app/RootStackParamList.ts @@ -1,4 +1,5 @@ export type RootStackParamList = { - dashboard: undefined; - timer: { reps: number, restTime: number, workTime: number}; + Dashboard: undefined; + Timer: { reps: number, restTime: number, workTime: number}; + Settings: undefined; }; diff --git a/app/Settings.tsx b/app/Settings.tsx new file mode 100644 index 0000000..9753fb4 --- /dev/null +++ b/app/Settings.tsx @@ -0,0 +1,80 @@ +import React, { useEffect, useState } from 'react'; + +import { Text } from '@/components/shared/Themed'; +import styled from '@emotion/native'; +import { useNavigation } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { RootStackParamList } from '@/app/RootStackParamList'; +import { loadUserSettings, saveUserSettings } from '@/components/shared/business/AsyncStorage'; +import { Switch } from 'react-native'; +import { VerticalSpacer } from '@/components/shared/Spacers'; +import Button from '@/components/shared/Button'; + +type NavigationProp = NativeStackNavigationProp; + +export default function Dashboard() { + const navigation = useNavigation(); + const [soundEnabled, setSoundEnabled] = useState(false); + + useEffect(() => { + const init = async () => { + try { + const soundEnabledLocal = await loadUserSettings('soundEnabled'); + console.log('soundEnabledLocal', soundEnabledLocal); + + if (soundEnabledLocal === null) { + console.log('soundEnabledLocal is null'); + setSoundEnabled(true); + } else { + console.log('soundEnabledLocal is present', ); + setSoundEnabled(Boolean(Number(soundEnabledLocal))); + } + } catch (error) { + throw new Error('Erreur lors du chargement des paramètres utilisateur'); + } + }; + + init(); + }, []); + + useEffect(() => { + saveUserSettings('soundEnabled', String(Number(soundEnabled))); + }, [soundEnabled]); + + return ( + + Preferences + +