chore: initial commit
This commit is contained in:
commit
edd662ce44
11
.editorconfig
Normal file
11
.editorconfig
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# https://editorconfig.org/
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
43
.eslintrc.json
Normal file
43
.eslintrc.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"conventions",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"ignorePatterns": ["jest.setup.ts"],
|
||||||
|
"plugins": ["prettier"],
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"react/self-closing-comp": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"html": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/void-dom-elements-no-children": "error",
|
||||||
|
"react/jsx-boolean-value": "error"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"parser": "@typescript-eslint/parser"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
.npm/
|
||||||
|
|
||||||
|
# Expo
|
||||||
|
.expo/
|
||||||
|
dist/
|
||||||
|
web-build/
|
||||||
|
|
||||||
|
# Native
|
||||||
|
*.orig.*
|
||||||
|
*.jks
|
||||||
|
*.p8
|
||||||
|
*.p12
|
||||||
|
*.key
|
||||||
|
*.mobileprovision
|
||||||
|
|
||||||
|
# Metro
|
||||||
|
.metro-health-check*
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.*
|
||||||
|
yarn-debug.*
|
||||||
|
yarn-error.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# tests
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
|
||||||
|
# The following patterns were generated by expo-cli
|
||||||
|
|
||||||
|
expo-env.d.ts
|
||||||
|
# @end expo-cli
|
3
.prettierrc.json
Normal file
3
.prettierrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"semi": false
|
||||||
|
}
|
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint"
|
||||||
|
]
|
||||||
|
}
|
14
.vscode/settings.json
vendored
Normal file
14
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"editor.bracketPairColorization.enabled": true,
|
||||||
|
"prettier.configPath": ".prettierrc.json",
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": "explicit"
|
||||||
|
},
|
||||||
|
"eslint.options": {
|
||||||
|
"ignorePath": ".gitignore"
|
||||||
|
},
|
||||||
|
"prettier.ignorePath": ".gitignore"
|
||||||
|
}
|
39
README.md
Normal file
39
README.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# P61 - Projet
|
||||||
|
|
||||||
|
## À propos
|
||||||
|
|
||||||
|
Application mobile en React Native pour le projet du module P61 Développement avancé.
|
||||||
|
|
||||||
|
[Sujet](./SUJET.md)
|
||||||
|
|
||||||
|
## Membres du Groupe 7
|
||||||
|
|
||||||
|
- [Théo LUDWIG](https://git.unistra.fr/t.ludwig)
|
||||||
|
- [Haoxuan LI](https://git.unistra.fr/haoxuan.li)
|
||||||
|
- [Maxime RUMPLER](https://git.unistra.fr/m.rumpler)
|
||||||
|
- [Maxime RICHARD](https://git.unistra.fr/maximerichard)
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) >= 20.0.0
|
||||||
|
- [npm](https://www.npmjs.com/) >= 10.0.0
|
||||||
|
- [Expo Go](https://expo.io/client)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm clean-install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Run the application
|
||||||
|
npm run start
|
||||||
|
|
||||||
|
# Lint and Test
|
||||||
|
npm run lint:prettier
|
||||||
|
npm run lint:eslint
|
||||||
|
npm run lint:typescript
|
||||||
|
npm run test
|
||||||
|
```
|
35
app.json
Normal file
35
app.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"name": "p61-project",
|
||||||
|
"slug": "p61-project",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icon": "./assets/images/icon.png",
|
||||||
|
"scheme": "myapp",
|
||||||
|
"userInterfaceStyle": "automatic",
|
||||||
|
"splash": {
|
||||||
|
"image": "./assets/images/splashscreen.jpg",
|
||||||
|
"resizeMode": "cover",
|
||||||
|
"backgroundColor": "#74b6cb"
|
||||||
|
},
|
||||||
|
"assetBundlePatterns": ["**/*"],
|
||||||
|
"ios": {
|
||||||
|
"supportsTablet": true
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"adaptiveIcon": {
|
||||||
|
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||||
|
"backgroundColor": "#ffffff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"bundler": "metro",
|
||||||
|
"output": "static",
|
||||||
|
"favicon": "./assets/images/favicon.png"
|
||||||
|
},
|
||||||
|
"plugins": ["expo-router"],
|
||||||
|
"experiments": {
|
||||||
|
"typedRoutes": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
app/(pages)/__tests__/__snapshots__/index.test.tsx.snap
Normal file
34
app/(pages)/__tests__/__snapshots__/index.test.tsx.snap
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<HomePage /> renders correctly 1`] = `
|
||||||
|
<RNCSafeAreaView
|
||||||
|
edges={
|
||||||
|
{
|
||||||
|
"bottom": "additive",
|
||||||
|
"left": "additive",
|
||||||
|
"right": "additive",
|
||||||
|
"top": "additive",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"alignItems": "center",
|
||||||
|
"flex": 1,
|
||||||
|
"justifyContent": "center",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"color": "#006CFF",
|
||||||
|
"fontFamily": "Georama",
|
||||||
|
"fontSize": 36,
|
||||||
|
"marginVertical": 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
P61 Project
|
||||||
|
</Text>
|
||||||
|
</RNCSafeAreaView>
|
||||||
|
`;
|
14
app/(pages)/__tests__/index.test.tsx
Normal file
14
app/(pages)/__tests__/index.test.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import renderer from "react-test-renderer"
|
||||||
|
|
||||||
|
import HomePage from "@/app/(pages)/index"
|
||||||
|
|
||||||
|
describe("<HomePage />", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("renders correctly", () => {
|
||||||
|
const tree = renderer.create(<HomePage />).toJSON()
|
||||||
|
expect(tree).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
37
app/(pages)/_layout.tsx
Normal file
37
app/(pages)/_layout.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import FontAwesome from "@expo/vector-icons/FontAwesome"
|
||||||
|
import { Tabs } from "expo-router"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://icons.expo.fyi/
|
||||||
|
* @param props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const TabBarIcon: React.FC<{
|
||||||
|
name: React.ComponentProps<typeof FontAwesome>["name"]
|
||||||
|
color: string
|
||||||
|
}> = (props) => {
|
||||||
|
return <FontAwesome size={28} style={{ marginBottom: -3 }} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabLayout: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
screenOptions={{
|
||||||
|
headerShown: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="index"
|
||||||
|
options={{
|
||||||
|
title: "Home",
|
||||||
|
tabBarIcon: ({ color }) => {
|
||||||
|
return <TabBarIcon name="home" color={color} />
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TabLayout
|
29
app/(pages)/index.tsx
Normal file
29
app/(pages)/index.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { StatusBar } from "expo-status-bar"
|
||||||
|
import { StyleSheet, Text } from "react-native"
|
||||||
|
import { SafeAreaView } from "react-native-safe-area-context"
|
||||||
|
|
||||||
|
const HomePage: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<Text style={styles.title}>P61 Project</Text>
|
||||||
|
|
||||||
|
<StatusBar style="auto" />
|
||||||
|
</SafeAreaView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontFamily: "Canterbury",
|
||||||
|
fontSize: 36,
|
||||||
|
color: "#006CFF",
|
||||||
|
marginVertical: 20,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default HomePage
|
19
app/+html.tsx
Normal file
19
app/+html.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { ScrollViewStyleReset } from "expo-router/html"
|
||||||
|
|
||||||
|
const Root: React.FC<React.PropsWithChildren> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||||
|
/>
|
||||||
|
<ScrollViewStyleReset />
|
||||||
|
</head>
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Root
|
56
app/_layout.tsx
Normal file
56
app/_layout.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useFonts } from "expo-font"
|
||||||
|
import { Stack } from "expo-router"
|
||||||
|
import * as SplashScreen from "expo-splash-screen"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
|
import CanterburyFont from "../assets/fonts/Canterbury.ttf"
|
||||||
|
import GeoramFont from "../assets/fonts/Georama-Black.ttf"
|
||||||
|
import SpaceMonoFont from "../assets/fonts/SpaceMono-Regular.ttf"
|
||||||
|
|
||||||
|
export { ErrorBoundary } from "expo-router"
|
||||||
|
|
||||||
|
export const unstableSettings = {
|
||||||
|
initialRouteName: "index",
|
||||||
|
}
|
||||||
|
|
||||||
|
SplashScreen.preventAutoHideAsync().catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
const RootLayout: React.FC = () => {
|
||||||
|
const [loaded, error] = useFonts({
|
||||||
|
Georama: GeoramFont,
|
||||||
|
SpaceMono: SpaceMonoFont,
|
||||||
|
Canterbury: CanterburyFont,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error != null) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}, [error])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loaded) {
|
||||||
|
SplashScreen.hideAsync().catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [loaded])
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
screenOptions={{
|
||||||
|
headerShown: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack.Screen name="(pages)" />
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RootLayout
|
BIN
assets/fonts/Canterbury.ttf
Executable file
BIN
assets/fonts/Canterbury.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/Georama-Black.ttf
Executable file
BIN
assets/fonts/Georama-Black.ttf
Executable file
Binary file not shown.
BIN
assets/fonts/SpaceMono-Regular.ttf
Executable file
BIN
assets/fonts/SpaceMono-Regular.ttf
Executable file
Binary file not shown.
BIN
assets/images/Universite-Strasbourg.png
Normal file
BIN
assets/images/Universite-Strasbourg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
assets/images/adaptive-icon.png
Normal file
BIN
assets/images/adaptive-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
assets/images/favicon.png
Normal file
BIN
assets/images/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/images/icon.png
Normal file
BIN
assets/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
assets/images/splash.png
Normal file
BIN
assets/images/splash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
BIN
assets/images/splashscreen.jpg
Executable file
BIN
assets/images/splashscreen.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 97 KiB |
6
babel.config.js
Normal file
6
babel.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = function (api) {
|
||||||
|
api.cache(true)
|
||||||
|
return {
|
||||||
|
presets: ["babel-preset-expo"],
|
||||||
|
}
|
||||||
|
}
|
34
components/ButtonCustom.tsx
Normal file
34
components/ButtonCustom.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import type { StyleProp, ViewStyle } from "react-native"
|
||||||
|
import { Pressable, StyleSheet, Text } from "react-native"
|
||||||
|
|
||||||
|
export interface ButtonCustomProps
|
||||||
|
extends React.ComponentProps<typeof Pressable> {
|
||||||
|
children: React.ReactNode
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonCustom: React.FC<ButtonCustomProps> = (props) => {
|
||||||
|
const { children, style, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable style={[styles.button, style]} {...rest}>
|
||||||
|
<Text style={styles.text}>{children}</Text>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
button: {
|
||||||
|
padding: 10,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "black",
|
||||||
|
borderRadius: 10,
|
||||||
|
marginTop: 10,
|
||||||
|
backgroundColor: "#152B5D",
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
color: "white",
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: 15,
|
||||||
|
},
|
||||||
|
})
|
27
components/ExternalLink.tsx
Normal file
27
components/ExternalLink.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Link } from "expo-router"
|
||||||
|
import * as WebBrowser from "expo-web-browser"
|
||||||
|
import { Platform } from "react-native"
|
||||||
|
|
||||||
|
type LinkProps = React.ComponentProps<typeof Link>
|
||||||
|
|
||||||
|
export const ExternalLink: React.FC<
|
||||||
|
Omit<LinkProps, "href"> & {
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
> = (props) => {
|
||||||
|
const { href, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href={href as unknown as LinkProps["href"]}
|
||||||
|
{...rest}
|
||||||
|
onPress={async (event) => {
|
||||||
|
if (Platform.OS !== "web") {
|
||||||
|
event.preventDefault()
|
||||||
|
await WebBrowser.openBrowserAsync(href)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
13
components/MonoText.tsx
Normal file
13
components/MonoText.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { StyleSheet, Text } from "react-native"
|
||||||
|
|
||||||
|
export const MonoText: React.FC<Text["props"]> = (props) => {
|
||||||
|
const { style, ...rest } = props
|
||||||
|
|
||||||
|
return <Text style={[style, styles.text]} {...rest} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
text: {
|
||||||
|
fontFamily: "SpaceMono",
|
||||||
|
},
|
||||||
|
})
|
12
components/__tests__/ButtonCustom.test.tsx
Normal file
12
components/__tests__/ButtonCustom.test.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import renderer from "react-test-renderer"
|
||||||
|
|
||||||
|
import { ButtonCustom } from "@/components/ButtonCustom"
|
||||||
|
|
||||||
|
describe("<ButtonCustom />", () => {
|
||||||
|
it("renders correctly", () => {
|
||||||
|
const tree = renderer
|
||||||
|
.create(<ButtonCustom>Awesome Button!</ButtonCustom>)
|
||||||
|
.toJSON()
|
||||||
|
expect(tree).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
16
components/__tests__/ExternalLink.test.tsx
Normal file
16
components/__tests__/ExternalLink.test.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import renderer from "react-test-renderer"
|
||||||
|
|
||||||
|
import { ExternalLink } from "@/components/ExternalLink"
|
||||||
|
|
||||||
|
describe("<ExternalLink />", () => {
|
||||||
|
it("renders correctly", () => {
|
||||||
|
const tree = renderer
|
||||||
|
.create(
|
||||||
|
<ExternalLink href="https://www.unistra.fr/">
|
||||||
|
Awesome Link!
|
||||||
|
</ExternalLink>,
|
||||||
|
)
|
||||||
|
.toJSON()
|
||||||
|
expect(tree).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
10
components/__tests__/MonoText.test.tsx
Normal file
10
components/__tests__/MonoText.test.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import renderer from "react-test-renderer"
|
||||||
|
|
||||||
|
import { MonoText } from "@/components/MonoText"
|
||||||
|
|
||||||
|
describe("<MonoText />", () => {
|
||||||
|
it("renders correctly", () => {
|
||||||
|
const tree = renderer.create(<MonoText>Awesome text!</MonoText>).toJSON()
|
||||||
|
expect(tree).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,60 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<ButtonCustom /> renders correctly 1`] = `
|
||||||
|
<View
|
||||||
|
accessibilityState={
|
||||||
|
{
|
||||||
|
"busy": undefined,
|
||||||
|
"checked": undefined,
|
||||||
|
"disabled": undefined,
|
||||||
|
"expanded": undefined,
|
||||||
|
"selected": undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accessibilityValue={
|
||||||
|
{
|
||||||
|
"max": undefined,
|
||||||
|
"min": undefined,
|
||||||
|
"now": undefined,
|
||||||
|
"text": undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accessible={true}
|
||||||
|
collapsable={false}
|
||||||
|
focusable={true}
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onResponderGrant={[Function]}
|
||||||
|
onResponderMove={[Function]}
|
||||||
|
onResponderRelease={[Function]}
|
||||||
|
onResponderTerminate={[Function]}
|
||||||
|
onResponderTerminationRequest={[Function]}
|
||||||
|
onStartShouldSetResponder={[Function]}
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"backgroundColor": "#152B5D",
|
||||||
|
"borderColor": "black",
|
||||||
|
"borderRadius": 10,
|
||||||
|
"borderWidth": 1,
|
||||||
|
"marginTop": 10,
|
||||||
|
"padding": 10,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"color": "white",
|
||||||
|
"fontSize": 15,
|
||||||
|
"textAlign": "center",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Awesome Button!
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
`;
|
@ -0,0 +1,11 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<ExternalLink /> renders correctly 1`] = `
|
||||||
|
<Text
|
||||||
|
href="https://www.unistra.fr/"
|
||||||
|
onPress={[Function]}
|
||||||
|
role="link"
|
||||||
|
>
|
||||||
|
Awesome Link!
|
||||||
|
</Text>
|
||||||
|
`;
|
16
components/__tests__/__snapshots__/MonoText.test.tsx.snap
Normal file
16
components/__tests__/__snapshots__/MonoText.test.tsx.snap
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<MonoText /> renders correctly 1`] = `
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
"fontFamily": "SpaceMono",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Awesome text!
|
||||||
|
</Text>
|
||||||
|
`;
|
61
hooks/__tests__/useBoolean.test.ts
Normal file
61
hooks/__tests__/useBoolean.test.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { act, renderHook } from "@testing-library/react-native"
|
||||||
|
|
||||||
|
import { useBoolean } from "@/hooks/useBoolean"
|
||||||
|
|
||||||
|
describe("hooks/useBoolean", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialValues = [true, false]
|
||||||
|
|
||||||
|
for (const initialValue of initialValues) {
|
||||||
|
it(`should set the initial value to ${initialValue}`, () => {
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
return useBoolean({ initialValue })
|
||||||
|
})
|
||||||
|
expect(result.current.value).toBe(initialValue)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should by default set the initial value to false", () => {
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
return useBoolean()
|
||||||
|
})
|
||||||
|
expect(result.current.value).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should toggle the value", async () => {
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
return useBoolean({ initialValue: false })
|
||||||
|
})
|
||||||
|
await act(() => {
|
||||||
|
return result.current.toggle()
|
||||||
|
})
|
||||||
|
expect(result.current.value).toBe(true)
|
||||||
|
await act(() => {
|
||||||
|
return result.current.toggle()
|
||||||
|
})
|
||||||
|
expect(result.current.value).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should set the value to true", async () => {
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
return useBoolean({ initialValue: false })
|
||||||
|
})
|
||||||
|
await act(() => {
|
||||||
|
return result.current.setTrue()
|
||||||
|
})
|
||||||
|
expect(result.current.value).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should set the value to false", async () => {
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
return useBoolean({ initialValue: true })
|
||||||
|
})
|
||||||
|
await act(() => {
|
||||||
|
return result.current.setFalse()
|
||||||
|
})
|
||||||
|
expect(result.current.value).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
93
hooks/__tests__/useLocalStorage.test.ts
Normal file
93
hooks/__tests__/useLocalStorage.test.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { act, renderHook, waitFor } from "@testing-library/react-native"
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage"
|
||||||
|
|
||||||
|
import { useLocalStorage } from "@/hooks/useLocalStorage"
|
||||||
|
|
||||||
|
describe("hooks/useLocalStorage", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
await AsyncStorage.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should get the default value", () => {
|
||||||
|
const key = "key"
|
||||||
|
const givenDefaultValue = { key: "value" }
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
return useLocalStorage(key, givenDefaultValue)
|
||||||
|
})
|
||||||
|
const [actualValue] = result.current
|
||||||
|
expect(actualValue).toEqual(givenDefaultValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should get the value from the storage", async () => {
|
||||||
|
const key = "key"
|
||||||
|
const givenDefaultValue = { someValue: "value" }
|
||||||
|
const storedValue = { someValue: "value" }
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
return useLocalStorage(key, givenDefaultValue)
|
||||||
|
})
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(AsyncStorage.getItem).toHaveBeenCalledWith(key)
|
||||||
|
})
|
||||||
|
const [actualValue] = result.current
|
||||||
|
expect(actualValue).toEqual(storedValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should set the value to the storage", async () => {
|
||||||
|
const key = "key"
|
||||||
|
const givenDefaultValue = { someValue: "value" }
|
||||||
|
const storedValue = { someValue: "value" }
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
return useLocalStorage(key, givenDefaultValue)
|
||||||
|
})
|
||||||
|
const [, setValue] = result.current
|
||||||
|
await act(() => {
|
||||||
|
setValue(storedValue)
|
||||||
|
})
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
||||||
|
key,
|
||||||
|
JSON.stringify(storedValue),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should get default value if storage value is not valid JSON", async () => {
|
||||||
|
console.error = jest.fn()
|
||||||
|
const key = "key"
|
||||||
|
const givenDefaultValue = { someValue: "value" }
|
||||||
|
const storedValue = "{not valid JSON"
|
||||||
|
await AsyncStorage.setItem(key, storedValue)
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
return useLocalStorage(key, givenDefaultValue)
|
||||||
|
})
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(AsyncStorage.getItem).toHaveBeenCalledWith(key)
|
||||||
|
})
|
||||||
|
const [actualValue] = result.current
|
||||||
|
expect(actualValue).toEqual(givenDefaultValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should catch the error when setting the value to the storage", async () => {
|
||||||
|
console.error = jest.fn()
|
||||||
|
const key = "key"
|
||||||
|
const givenDefaultValue = { someValue: "value" }
|
||||||
|
const storedValue = { someValue: "value" }
|
||||||
|
const error = new Error("error")
|
||||||
|
;(AsyncStorage.setItem as jest.Mock).mockRejectedValue(error)
|
||||||
|
const { result } = renderHook(() => {
|
||||||
|
return useLocalStorage(key, givenDefaultValue)
|
||||||
|
})
|
||||||
|
const [, setValue] = result.current
|
||||||
|
await act(() => {
|
||||||
|
setValue(storedValue)
|
||||||
|
})
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
||||||
|
key,
|
||||||
|
JSON.stringify(storedValue),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
expect(console.error).toHaveBeenCalledWith(error)
|
||||||
|
})
|
||||||
|
})
|
50
hooks/useBoolean.ts
Normal file
50
hooks/useBoolean.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
export interface UseBooleanResult {
|
||||||
|
value: boolean
|
||||||
|
toggle: () => void
|
||||||
|
setTrue: () => void
|
||||||
|
setFalse: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseBooleanOptions {
|
||||||
|
/**
|
||||||
|
* The initial value of the boolean.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
initialValue?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to manage a boolean state.
|
||||||
|
* @param options
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const useBoolean = (
|
||||||
|
options: UseBooleanOptions = {},
|
||||||
|
): UseBooleanResult => {
|
||||||
|
const { initialValue = false } = options
|
||||||
|
|
||||||
|
const [value, setValue] = useState(initialValue)
|
||||||
|
|
||||||
|
const toggle = (): void => {
|
||||||
|
setValue((old) => {
|
||||||
|
return !old
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTrue = (): void => {
|
||||||
|
setValue(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFalse = (): void => {
|
||||||
|
setValue(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
toggle,
|
||||||
|
setTrue,
|
||||||
|
setFalse,
|
||||||
|
}
|
||||||
|
}
|
43
hooks/useLocalStorage.ts
Normal file
43
hooks/useLocalStorage.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage"
|
||||||
|
|
||||||
|
type UseLocalStorageResult<T> = [T, React.Dispatch<React.SetStateAction<T>>]
|
||||||
|
|
||||||
|
export const useLocalStorage = <T extends unknown>(
|
||||||
|
key: string,
|
||||||
|
defaultValue: T,
|
||||||
|
): UseLocalStorageResult<T> => {
|
||||||
|
const hasSaved = useRef(false)
|
||||||
|
const [value, setValue] = useState<T>(defaultValue)
|
||||||
|
|
||||||
|
const getFromLocalStorage = useCallback(async (): Promise<T> => {
|
||||||
|
const value = await AsyncStorage.getItem(key)
|
||||||
|
hasSaved.current = true
|
||||||
|
if (value == null) {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return JSON.parse(value) as T
|
||||||
|
}, [key, defaultValue])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasSaved.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AsyncStorage.setItem(key, JSON.stringify(value)).catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
})
|
||||||
|
}, [key, value])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getFromLocalStorage()
|
||||||
|
.then((value) => {
|
||||||
|
setValue(value)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setValue(defaultValue)
|
||||||
|
console.error(error)
|
||||||
|
})
|
||||||
|
}, [defaultValue, getFromLocalStorage])
|
||||||
|
|
||||||
|
return [value, setValue]
|
||||||
|
}
|
19
jest.config.json
Normal file
19
jest.config.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"preset": "jest-expo",
|
||||||
|
"roots": ["./"],
|
||||||
|
"setupFilesAfterEnv": ["<rootDir>/tests/jest.setup.ts"],
|
||||||
|
"fakeTimers": {
|
||||||
|
"enableGlobally": true
|
||||||
|
},
|
||||||
|
"collectCoverage": true,
|
||||||
|
"coverageDirectory": "coverage",
|
||||||
|
"coverageReporters": ["text", "text-summary", "cobertura"],
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"<rootDir>/**/*.{ts,tsx}",
|
||||||
|
"!<rootDir>/components/ExternalLink.tsx",
|
||||||
|
"!<rootDir>/.expo",
|
||||||
|
"!<rootDir>/app/+html.tsx",
|
||||||
|
"!<rootDir>/app/**/_layout.tsx",
|
||||||
|
"!**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
20466
package-lock.json
generated
Normal file
20466
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
60
package.json
Normal file
60
package.json
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"name": "p61-project",
|
||||||
|
"private": true,
|
||||||
|
"main": "expo-router/entry",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"start": "expo start",
|
||||||
|
"android": "expo start --android",
|
||||||
|
"ios": "expo start --ios",
|
||||||
|
"web": "expo start --web",
|
||||||
|
"lint:prettier": "prettier . --check",
|
||||||
|
"lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives --ignore-path .gitignore",
|
||||||
|
"lint:typescript": "tsc --noEmit",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/vector-icons": "14.0.0",
|
||||||
|
"@react-native-async-storage/async-storage": "1.21.0",
|
||||||
|
"@react-navigation/native": "6.1.10",
|
||||||
|
"expo": "50.0.6",
|
||||||
|
"expo-font": "11.10.2",
|
||||||
|
"expo-linking": "6.2.2",
|
||||||
|
"expo-router": "3.4.7",
|
||||||
|
"expo-splash-screen": "0.26.4",
|
||||||
|
"expo-status-bar": "1.11.1",
|
||||||
|
"expo-system-ui": "2.9.3",
|
||||||
|
"expo-web-browser": "12.8.2",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-native": "0.73.4",
|
||||||
|
"react-native-safe-area-context": "4.8.2",
|
||||||
|
"react-native-screens": "3.29.0",
|
||||||
|
"react-native-web": "0.19.10"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "7.23.9",
|
||||||
|
"@testing-library/react-native": "12.4.3",
|
||||||
|
"@total-typescript/ts-reset": "0.5.1",
|
||||||
|
"@tsconfig/strictest": "2.0.3",
|
||||||
|
"@types/jest": "29.5.12",
|
||||||
|
"@types/react": "18.2.55",
|
||||||
|
"@types/react-test-renderer": "18.0.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "7.0.1",
|
||||||
|
"@typescript-eslint/parser": "7.0.1",
|
||||||
|
"eslint": "8.56.0",
|
||||||
|
"eslint-config-conventions": "14.1.0",
|
||||||
|
"eslint-config-prettier": "9.1.0",
|
||||||
|
"eslint-plugin-import": "2.29.1",
|
||||||
|
"eslint-plugin-prettier": "5.1.3",
|
||||||
|
"eslint-plugin-promise": "6.1.1",
|
||||||
|
"eslint-plugin-react": "7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
|
"eslint-plugin-react-native": "4.1.0",
|
||||||
|
"eslint-plugin-unicorn": "51.0.1",
|
||||||
|
"jest": "29.7.0",
|
||||||
|
"jest-expo": "50.0.2",
|
||||||
|
"react-test-renderer": "18.2.0",
|
||||||
|
"typescript": "5.3.3"
|
||||||
|
}
|
||||||
|
}
|
5
react-app-env.d.ts
vendored
Normal file
5
react-app-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
declare module "*.png"
|
||||||
|
declare module "*.svg"
|
||||||
|
declare module "*.jpeg"
|
||||||
|
declare module "*.jpg"
|
||||||
|
declare module "*.ttf"
|
5
tests/jest.setup.ts
Normal file
5
tests/jest.setup.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import "@testing-library/react-native/extend-expect"
|
||||||
|
|
||||||
|
jest.mock("@react-native-async-storage/async-storage", () => {
|
||||||
|
return require("@react-native-async-storage/async-storage/jest/async-storage-mock")
|
||||||
|
})
|
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": ["@tsconfig/strictest/tsconfig.json", "expo/tsconfig.base"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"exactOptionalPropertyTypes": false,
|
||||||
|
"allowJs": false,
|
||||||
|
"checkJs": false,
|
||||||
|
"types": [
|
||||||
|
"@total-typescript/ts-reset",
|
||||||
|
"jest",
|
||||||
|
"@testing-library/react-native/extend-expect"
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
|
||||||
|
}
|
Reference in New Issue
Block a user