diff --git a/.drone.yml b/.drone.yml
index 9f5babb..abe08db 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -1,26 +1,26 @@
-kind: pipeline
-type: docker
-name: default
-
-trigger:
- event:
- - push
-
-steps:
- - name: sonar-analysis
- image: hub.codefirst.iut.uca.fr/camille.petitalot/drone-sonarplugin-reactnative:latest
- commands:
- - npm install
- - npm run test:coverage
- - ls ./coverage
- - sonar-scanner
- -Dsonar.projectKey=AD_multiplat
- -Dsonar.sources=.
- -Dsonar.host.url=$${PLUGIN_SONAR_HOST}
- -Dsonar.login=$${PLUGIN_SONAR_TOKEN}
- -Dsonar.javascript.lcov.reportPaths=./coverage/lcov.info
- -Dsonar.exclusions=**/*.test.tsx,**/*.test.ts,**/*.spec.tsx,**/*.spec.ts,**/lcov-report/**
- settings:
- sonar_host: https://codefirst.iut.uca.fr/sonar/
- sonar_token:
- from_secret: SONAR_TOKEN
+kind: pipeline
+type: docker
+name: default
+
+trigger:
+ event:
+ - push
+
+steps:
+ - name: sonar-analysis
+ image: hub.codefirst.iut.uca.fr/camille.petitalot/drone-sonarplugin-reactnative:latest
+ commands:
+ - yarn install
+ - yarn test:coverage
+ - ls ./coverage
+ - sonar-scanner
+ -Dsonar.projectKey=AD_multiplat
+ -Dsonar.sources=.
+ -Dsonar.host.url=$${PLUGIN_SONAR_HOST}
+ -Dsonar.login=$${PLUGIN_SONAR_TOKEN}
+ -Dsonar.javascript.lcov.reportPaths=./coverage/lcov.info
+ -Dsonar.exclusions=**/*.test.tsx,**/*.test.ts,**/*.spec.tsx,**/*.spec.ts,**/lcov-report/**,**/constants.ts,config.ts,babel.config.ts
+ settings:
+ sonar_host: https://codefirst.iut.uca.fr/sonar/
+ sonar_token:
+ from_secret: SONAR_TOKEN
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..a55e7a1
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/start.xml b/.idea/runConfigurations/start.xml
index 40e4c12..7ba5005 100644
--- a/.idea/runConfigurations/start.xml
+++ b/.idea/runConfigurations/start.xml
@@ -1,13 +1,13 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/App.tsx b/App.tsx
index 53bd1cf..75d3e21 100644
--- a/App.tsx
+++ b/App.tsx
@@ -1,7 +1,16 @@
-import React from 'react';
-import Navigation from "./navigation/Navigation";
+// App.tsx
+
+import React from 'react';
+import Navigation from "./navigation/Navigation";
+import store from "./redux/store";
+import { Provider } from "react-redux";
+import { SafeAreaProvider } from "react-native-safe-area-context";
export default function App() {
- return ;
- // TODO Send to homescreen instead, and include a bottom bar to navigate to Moves, Pokemongs, Trainers
+ return (
+
+
+
+
+ );
}
diff --git a/README.md b/README.md
index 3c48434..120aacd 100644
--- a/README.md
+++ b/README.md
@@ -17,23 +17,47 @@
# AD_ReactNative
-A React Native app for education purposes. Refer
+A React Native app for educational purposes. Refer
to [instructions here](https://react-native-courses.clubinfo-clermont.fr/docs/notation).
++ [Notation checklist](#notation-checklist)
+ [Sketches](#sketches)
- - [Trainers](#trainers)
- - [Pokemongs](#pokemongs)
- - [Moves](#moves)
+ - [Pokemongs](#pokemongs)
+ - [Moves](#moves)
+ [Using the app](#using-the-app)
+## Notation checklist
+
+* [ ] Documentation (6 pts)
+ - [ ] Application sketches (4 pts)
+ - [ ] A Readme describing your project/application. (2 pts) [planned]
+* [x] Basics (20 pts)
+ - [x] Navigation (3 pts)
+ + [x] Tab bottom navigation (2 pts) AND at least one button (1 pts)
+ - [x] Redux Store (10 pts)
+ + [x] Read data from redux store (2 pts)
+ + [x] Update data to redux store with actions and reducers (slice = 0) (4 pts)
+ + [x] Update data to redux store using redux-thunk (API AND|OR AsyncStorage) (4 pts)
+ - [x] Display list of items (2 pts)
+ + [x] FlatList, VirtualizedList or SectionList
+ - [ ] ~~Display dynamic image (2 pts)~~
+ - [x] Binding child component props (1 pts)
+ - [x] Handle a TextInput correctly (2 pts)
+ + [x] Beware of keyboard management
+* [ ] Application features (14 pts)
+ - [x] Retrieve data using the Web API (6 pts)
+ + [x] Handle fetch success callback (3 pts)
+ + [x] Handle fetch error callback (3 pts)
+ - [ ] Store favorite data into phone storage (2 pts) [maybe]
+ - [x] Write Tests (6 pts)
+ + [ ] ~~all actions payload (1 pts)~~
+ + [ ] ~~all reducers case (2 pts)~~
+ + [x] one UI Component (3 pts)
+
## Sketches
This app will contain several "master/detail" tabs. They are as follows.
-### Trainers
-
-
-
### Pokemongs
@@ -44,5 +68,6 @@ This app will contain several "master/detail" tabs. They are as follows.
## Using the app
-This app is linked to a backend that is set up to accept CORS from [`http://localhost:19006`](http://localhost:19006), so please make sure you're
-not overriding that default port number when running it.
+This app is linked to a backend that is set up to accept CORS from [`http://localhost:19006`](http://localhost:19006).
+If you want to use the dedicated API, please make sure you're not overriding that default port number when running this
+app.
diff --git a/assets/favicon.png b/assets/favicon.png
index e75f697..96de913 100644
Binary files a/assets/favicon.png and b/assets/favicon.png differ
diff --git a/assets/home.png b/assets/home.png
new file mode 100644
index 0000000..b3f6a10
Binary files /dev/null and b/assets/home.png differ
diff --git a/assets/logo.png b/assets/logo.png
new file mode 100644
index 0000000..36d15eb
Binary files /dev/null and b/assets/logo.png differ
diff --git a/assets/moves.png b/assets/moves.png
new file mode 100644
index 0000000..f726cad
Binary files /dev/null and b/assets/moves.png differ
diff --git a/assets/pokemongs.png b/assets/pokemongs.png
new file mode 100644
index 0000000..d628c77
Binary files /dev/null and b/assets/pokemongs.png differ
diff --git a/assets/trainers.png b/assets/trainers.png
new file mode 100644
index 0000000..c47fe83
Binary files /dev/null and b/assets/trainers.png differ
diff --git a/components/AlertModal.tsx b/components/AlertModal.tsx
new file mode 100644
index 0000000..d0cca41
--- /dev/null
+++ b/components/AlertModal.tsx
@@ -0,0 +1,61 @@
+// components/AlertModal.tsx
+import { Button, Modal, StyleSheet, Text, View } from 'react-native';
+import React from 'react';
+
+type AlertModalProps = {
+ visible: boolean;
+ message: string;
+ onClose: () => void;
+};
+
+const AlertModal = ({ visible, message, onClose }: AlertModalProps) => {
+ return (
+
+
+
+ {message}
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ centeredView: {
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ marginTop: 16
+ },
+ modalView: {
+ margin: 16,
+ backgroundColor: "white",
+ borderRadius: 16,
+ padding: 32,
+ alignItems: "center",
+ shadowColor: "#000",
+ shadowOffset: {
+ width: 0,
+ height: 2
+ },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ elevation: 5
+ },
+ modalText: {
+ marginBottom: 16,
+ textAlign: "center"
+ }
+});
+
+export default AlertModal;
diff --git a/components/MoveListItem.tsx b/components/MoveListItem.tsx
new file mode 100644
index 0000000..874ce42
--- /dev/null
+++ b/components/MoveListItem.tsx
@@ -0,0 +1,32 @@
+// components/MoveListItem.test.ts
+
+import { Move } from "../entities/Move";
+import React from "react";
+import { StyleSheet, Text, TouchableOpacity, ViewStyle } from "react-native";
+
+type MoveListItemProps = {
+ move: Move;
+ onPress: () => void;
+ style?: ViewStyle;
+};
+
+const MoveListItem: React.FC = ({ move, onPress, style }) => (
+
+ {move.name}, {move.type.name}: {move.power}
+
+);
+
+const styles = StyleSheet.create({
+ listItem: {
+ backgroundColor: '#DDD',
+ padding: 8,
+ borderRadius: 8,
+ },
+ listItemText: {
+ color: '#333',
+ fontSize: 18,
+ },
+});
+
+export default MoveListItem;
diff --git a/components/TypeTacticsInfoList.test.tsx b/components/TypeTacticsInfoList.test.tsx
index 4a8bee7..0899a7d 100644
--- a/components/TypeTacticsInfoList.test.tsx
+++ b/components/TypeTacticsInfoList.test.tsx
@@ -1,11 +1,14 @@
-import React from 'react';
-import {render} from '@testing-library/react-native';
+// components/TypeTacticsInfoList.test.ts
+
+import React from 'react';
+import { render } from '@testing-library/react-native';
import TypeTacticsInfoList from './TypeTacticsInfoList';
+import { TypeName } from "../entities/TypeName";
describe('TypeTacticsInfoList component', () => {
it('renders types correctly', () => {
- const types = ['FIRE', 'WATER', 'GRASS'];
- const {getByText} = render();
+ const types = [TypeName.FIRE, TypeName.WATER, TypeName.GRASS];
+ const { getByText } = render();
types.forEach(type => {
expect(getByText(type)).toBeTruthy();
@@ -13,13 +16,13 @@ describe('TypeTacticsInfoList component', () => {
});
it('renders "Nothing" when types array is empty', () => {
- const {getByText} = render();
+ const { getByText } = render();
expect(getByText('Nothing')).toBeTruthy();
});
it('renders "Nothing" when types is undefined', () => {
// @ts-ignore
- const {getByText} = render();
+ const { getByText } = render();
expect(getByText('Nothing')).toBeTruthy();
});
});
diff --git a/components/TypeTacticsInfoList.tsx b/components/TypeTacticsInfoList.tsx
index 07c0068..d95b1e0 100644
--- a/components/TypeTacticsInfoList.tsx
+++ b/components/TypeTacticsInfoList.tsx
@@ -1,39 +1,56 @@
-import React from "react";
-import {StyleSheet, View, Text, ScrollView} from "react-native";
+// components/TypeTacticsInfoList.ts
+
+import React from "react";
+import { ScrollView, StyleSheet, Text, View } from "react-native";
type TypeListProps = {
isWeakness: boolean;
types: string[];
};
-const TypeTacticsInfoList = ({isWeakness, types}: TypeListProps) => {
+const TypeTacticsInfoList = ({ isWeakness, types }: TypeListProps) => {
if (!types || types.length === 0) {
- types = ['Nothing'];
+ types = ['NOTHING'];
}
return (
-
-
- {isWeakness ? 'weak against' : 'effective against'}:
- {types.map((type, index) => (
- {type}
- ))}
-
-
+
+ {isWeakness ? 'Weak Against' : 'Effective Against'}:
+
+
+ {types.map((type, index) => (
+ {type}
+ ))}
+
+
+
);
};
const styles = StyleSheet.create({
list: {
- borderRadius: 5,
padding: 10,
- marginBottom: 10,
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ marginBottom: 5,
+ },
+ type: {
+ fontSize: 16,
+ marginBottom: 5,
},
weakAgainst: {
backgroundColor: '#FF6961',
+ borderRadius: 10,
+ marginBottom: 10,
+ padding: 10,
},
effectiveAgainst: {
backgroundColor: '#77DD77',
+ borderRadius: 10,
+ marginBottom: 10,
+ padding: 10,
},
});
diff --git a/config.ts b/config.ts
new file mode 100644
index 0000000..3b99d72
--- /dev/null
+++ b/config.ts
@@ -0,0 +1,2 @@
+// config.ts
+export const API_BASE_URL = 'http://localhost:8080';
diff --git a/docs/trainers.jpg b/docs/trainers.jpg
deleted file mode 100644
index 14b8fbb..0000000
Binary files a/docs/trainers.jpg and /dev/null differ
diff --git a/entities/Move.ts b/entities/Move.ts
index d51f06e..bc81044 100644
--- a/entities/Move.ts
+++ b/entities/Move.ts
@@ -1,9 +1,14 @@
-import {Type} from "./Type";
+// entities/Move.ts
+
+import { Type } from "./Type";
+import { MoveCategoryName } from "./MoveCategoryName";
export interface Move {
+ id: string | null;
name: string;
- category: string;
+ category: MoveCategoryName;
power: number;
accuracy: number;
type: Type;
+ schemaVersion: number;
}
diff --git a/entities/MoveCategoryName.ts b/entities/MoveCategoryName.ts
new file mode 100644
index 0000000..ef12306
--- /dev/null
+++ b/entities/MoveCategoryName.ts
@@ -0,0 +1,5 @@
+export enum MoveCategoryName {
+ PHYSICAL = 'PHYSICAL',
+ SPECIAL = 'SPECIAL',
+ STATUS = 'STATUS',
+}
diff --git a/entities/Type.ts b/entities/Type.ts
index 0f44ecb..7e6642c 100644
--- a/entities/Type.ts
+++ b/entities/Type.ts
@@ -1,5 +1,9 @@
+// entities/Type.ts
+
+import { TypeName } from "./TypeName";
+
export interface Type {
- name: string;
+ name: TypeName;
weakAgainst: string[];
effectiveAgainst: string[];
}
diff --git a/entities/TypeName.ts b/entities/TypeName.ts
new file mode 100644
index 0000000..ec0bdb1
--- /dev/null
+++ b/entities/TypeName.ts
@@ -0,0 +1,20 @@
+export enum TypeName {
+ NORMAL = 'NORMAL',
+ GRASS = 'GRASS',
+ ELECTRIC = 'ELECTRIC',
+ WATER = 'WATER',
+ FIRE = 'FIRE',
+ BUG = 'BUG',
+ GHOST = 'GHOST',
+ PSYCHIC = 'PSYCHIC',
+ STEEL = 'STEEL',
+ DARK = 'DARK',
+ FLYING = 'FLYING',
+ ICE = 'ICE',
+ POISON = 'POISON',
+ GROUND = 'GROUND',
+ ROCK = 'ROCK',
+ DRAGON = 'DRAGON',
+ FIGHTING = 'FIGHTING',
+ FAIRY = 'FAIRY',
+}
diff --git a/navigation/.gitkeep b/navigation/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/navigation/Navigation.tsx b/navigation/Navigation.tsx
index 1503034..3742d5e 100644
--- a/navigation/Navigation.tsx
+++ b/navigation/Navigation.tsx
@@ -1,22 +1,59 @@
-import React from 'react';
-import {NavigationContainer} from '@react-navigation/native';
-import {createStackNavigator} from '@react-navigation/stack';
-import MoveListScreen from '../screens/MoveListScreen';
-import MoveDetailScreen from '../screens/MoveDetailScreen';
-import {RootStackParamList} from "./navigationTypes";
+// navigation/Navigation.tsx
+
+import React from 'react';
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import { NavigationContainer } from '@react-navigation/native';
+import MoveListScreen from '../screens/moves/MoveListScreen';
+import MoveDetailScreen from '../screens/moves/MoveDetailScreen';
+import HomeScreen from '../screens/HomeScreen';
+import { createStackNavigator } from '@react-navigation/stack';
+import { RootStackParamList, RootTabParamList } from "./navigationTypes";
+import { Image, StyleSheet } from 'react-native';
+import MoveFormScreen from "../screens/moves/MoveFormScreen";
const Stack = createStackNavigator();
+const Tab = createBottomTabNavigator();
+
+const MoveStack = () => {
+ return (
+
+
+ ({ title: route.params.move.name })}
+ />
+
+
+ );
+};
const Navigation = () => {
- // TODO replace 'Move Detail' by the move name itself
return (
-
-
-
-
+
+
+ }}/>
+
+ }}/>
+
);
};
+const styles = StyleSheet.create({
+ icon: {
+ width: 24,
+ height: 24,
+ padding: 8,
+ },
+});
+
export default Navigation;
diff --git a/navigation/constants.ts b/navigation/constants.ts
new file mode 100644
index 0000000..175d332
--- /dev/null
+++ b/navigation/constants.ts
@@ -0,0 +1,5 @@
+// navigation/constants.ts
+
+export const MOVE_DETAIL = 'MoveDetail';
+export const MOVE_FORM = 'MoveForm';
+export const MOVE_LIST = 'MoveList';
diff --git a/navigation/navigationTypes.ts b/navigation/navigationTypes.ts
index b676d17..8b01917 100644
--- a/navigation/navigationTypes.ts
+++ b/navigation/navigationTypes.ts
@@ -1,6 +1,14 @@
-import {Move} from "../entities/Move";
+// navigation/navigationTypes.ts
+
+import { Move } from "../entities/Move";
export type RootStackParamList = {
MoveList: undefined;
MoveDetail: { move: Move };
+ MoveForm: { move?: Move };
+};
+
+export type RootTabParamList = {
+ Home: undefined;
+ Moves: undefined;
};
diff --git a/package.json b/package.json
index 414c99f..71c55b9 100644
--- a/package.json
+++ b/package.json
@@ -36,9 +36,11 @@
},
"dependencies": {
"@expo/webpack-config": "^18.0.1",
+ "@react-native-community/picker": "^1.8.1",
"@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/native": "^6.1.6",
"@react-navigation/stack": "^6.3.16",
+ "@reduxjs/toolkit": "^1.9.5",
"@testing-library/react-native": "^12.1.2",
"@types/react": "~18.0.27",
"expo": "^48.0.0",
@@ -47,9 +49,14 @@
"react-dom": "18.2.0",
"react-native": "0.71.8",
"react-native-gesture-handler": "~2.9.0",
+ "react-native-keyboard-aware-scroll-view": "^0.9.5",
+ "react-native-multiple-select": "^0.5.12",
"react-native-safe-area-context": "4.5.0",
"react-native-screens": "~3.20.0",
"react-native-web": "~0.18.11",
+ "react-redux": "^8.0.7",
+ "redux": "^4.2.1",
+ "redux-thunk": "^2.4.2",
"typescript": "^4.9.4"
},
"devDependencies": {
diff --git a/redux/actions/.gitkeep b/redux/actions/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/redux/actions/moveActions.ts b/redux/actions/moveActions.ts
new file mode 100644
index 0000000..267155c
--- /dev/null
+++ b/redux/actions/moveActions.ts
@@ -0,0 +1,105 @@
+// redux/actions/moveAction.ts
+import { CREATE_MOVE, DELETE, DELETE_MOVE, GET, GET_MOVES, MOVE_ERROR, POST, PUT, UPDATE_MOVE } from '../constants';
+import {
+ Move
+} from "../../entities/Move";
+import { Dispatch } from "redux";
+import { API_BASE_URL } from "../../config";
+
+
+export const createMove = (move: Move) => {
+ const verb = POST
+ return async (dispatch: Dispatch) => {
+ try {
+ const response = await fetch(`${API_BASE_URL}/move`, {
+ method: verb,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(move),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to ${verb}: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ dispatch({ type: CREATE_MOVE, payload: data });
+ }
+ catch (error) {
+ console.error(error);
+ // @ts-ignore
+ dispatch({ type: MOVE_ERROR, payload: error.message });
+ }
+ }
+}
+
+export const getMoves = () => {
+ const verb = GET
+ return async (dispatch: Dispatch) => {
+ try {
+ const response = await fetch(`${API_BASE_URL}/move`);
+
+ if (!response.ok) {
+ throw new Error(`Failed to ${verb}: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ dispatch({ type: GET_MOVES, payload: data });
+ }
+ catch (error) {
+ console.error(error);
+ // @ts-ignore
+ dispatch({ type: MOVE_ERROR, payload: error.message });
+ }
+ }
+}
+
+export const updateMove = (id: string, move: Move) => {
+ const verb = PUT
+ return async (dispatch: Dispatch) => {
+ try {
+ const response = await fetch(`${API_BASE_URL}/move/${id}`, {
+ method: verb,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(move),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to ${verb}: ${response.statusText}`);
+ }
+
+ const updatedMove = await response.json();
+ dispatch({ type: UPDATE_MOVE, payload: updatedMove });
+ }
+ catch (error) {
+ console.error(error);
+ // @ts-ignore
+ dispatch({ type: MOVE_ERROR, payload: error.message });
+ }
+ }
+}
+
+export const deleteMove = (id: string) => {
+ const verb = DELETE
+ return async (dispatch: Dispatch) => {
+ try {
+ const response = await fetch(`${API_BASE_URL}/move/${id}`, {
+ method: verb,
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to ${verb}: ${response.statusText}`);
+ }
+
+ dispatch({ type: DELETE_MOVE, payload: id });
+ }
+ catch (error) {
+ console.error(error);
+ // @ts-ignore
+ dispatch({ type: MOVE_ERROR, payload: error.message });
+ }
+ }
+}
diff --git a/redux/constants.ts b/redux/constants.ts
index 8b13789..64fd849 100644
--- a/redux/constants.ts
+++ b/redux/constants.ts
@@ -1 +1,12 @@
+// redux/constants.ts
+export const GET = 'GET';
+export const PUT = 'PUT';
+export const POST = 'POST';
+export const DELETE = 'DELETE';
+
+export const GET_MOVES = 'GET_MOVES';
+export const CREATE_MOVE = 'CREATE_MOVE';
+export const UPDATE_MOVE = 'UPDATE_MOVE';
+export const DELETE_MOVE = 'DELETE_MOVE';
+export const MOVE_ERROR = 'MOVE_ERROR';
diff --git a/redux/reducers/.gitkeep b/redux/reducers/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/redux/reducers/moveReducer.ts b/redux/reducers/moveReducer.ts
new file mode 100644
index 0000000..0339c30
--- /dev/null
+++ b/redux/reducers/moveReducer.ts
@@ -0,0 +1,52 @@
+// redux/reducers/moveReducer.ts
+import { CREATE_MOVE, DELETE_MOVE, GET_MOVES, MOVE_ERROR, UPDATE_MOVE } from '../constants';
+import { Move } from "../../entities/Move";
+
+export type MoveState = {
+ moves: Move[];
+ error: string | null;
+};
+
+type MoveAction = {
+ type: string;
+ payload?: Move | Move[] | string;
+};
+
+const initialState: MoveState = {
+ moves: [],
+ error: null
+}
+
+export default function moveReducer(state = initialState, action: MoveAction): MoveState {
+ switch (action.type) {
+ case GET_MOVES:
+ return {
+ ...state, moves: action.payload as Move[] || [],
+ error: null,
+ };
+ case CREATE_MOVE:
+ return {
+ ...state, moves: [...state.moves, action.payload as Move],
+ error: null,
+ };
+ case UPDATE_MOVE:
+ return {
+ ...state,
+ moves: state.moves.map(move => move.id === (action.payload as Move).id ? action.payload as Move : move),
+ error: null,
+ };
+ case DELETE_MOVE:
+ return {
+ ...state,
+ moves: state.moves.filter(move => move.id !== action.payload),
+ error: null,
+ };
+ case MOVE_ERROR:
+ return {
+ ...state,
+ error: action.payload as string
+ };
+ default:
+ return state;
+ }
+}
diff --git a/redux/store.ts b/redux/store.ts
index e69de29..78aff04 100644
--- a/redux/store.ts
+++ b/redux/store.ts
@@ -0,0 +1,19 @@
+// redux/store.ts
+
+import { configureStore } from '@reduxjs/toolkit'
+import moveReducer, { MoveState } from './reducers/moveReducer';
+
+export type AppDispatch = typeof store.dispatch;
+
+export type RootState = {
+ move: MoveState;
+};
+
+const store = configureStore({
+ reducer: {
+ move: moveReducer
+ },
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware(),
+});
+
+export default store;
diff --git a/screens/.gitkeep b/screens/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/screens/HomeScreen.tsx b/screens/HomeScreen.tsx
new file mode 100644
index 0000000..af331b3
--- /dev/null
+++ b/screens/HomeScreen.tsx
@@ -0,0 +1,14 @@
+// screens/HomeScreen.tsx
+
+import React from 'react';
+import { Image, View } from 'react-native';
+
+const HomeScreen = () => {
+ return (
+
+
+
+ );
+};
+
+export default HomeScreen;
diff --git a/screens/MoveDetailScreen.tsx b/screens/MoveDetailScreen.tsx
deleted file mode 100644
index a72218c..0000000
--- a/screens/MoveDetailScreen.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from 'react';
-import {ScrollView, StyleSheet, Text, View} from 'react-native';
-import {RouteProp} from '@react-navigation/native';
-import {RootStackParamList} from "../navigation/navigationTypes";
-import TypeTacticsInfoList from "../components/TypeTacticsInfoList"
-
-type MoveDetailScreenRouteProp = RouteProp;
-
-type Props = {
- route: MoveDetailScreenRouteProp;
-};
-
-const MoveDetailScreen = ({route}: Props) => {
- const {move} = route.params;
-
- return (
-
- {move.name}
- Name: {move.name}
- Category: {move.category}
- Power: {move.power}
- Accuracy: {move.accuracy}
- Type: {move.type.name}
-
-
-
-
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#FFFFFF',
- padding: 20,
- },
- title: {
- fontSize: 24,
- fontWeight: 'bold',
- },
- list: {
- backgroundColor: '#F0F0F0',
- borderRadius: 5,
- padding: 10,
- marginBottom: 10,
- },
- typeListsContainer: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- },
-});
-
-export default MoveDetailScreen;
diff --git a/screens/MoveListScreen.tsx b/screens/MoveListScreen.tsx
deleted file mode 100644
index 2f2eb49..0000000
--- a/screens/MoveListScreen.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React, {useEffect, useState} from 'react';
-import {FlatList, ScrollView, Text, TouchableOpacity, View} from 'react-native';
-import {StackNavigationProp} from '@react-navigation/stack';
-import {RootStackParamList} from "../navigation/navigationTypes";
-import {Move} from "../entities/Move";
-
-type MoveListScreenNavigationProp = StackNavigationProp;
-
-type Props = {
- navigation: MoveListScreenNavigationProp;
-};
-
-const MoveListScreen = ({navigation}: Props) => {
- const [moves, setMoves] = useState([]);
-
- // FIXME LATER use a redux store to fetch the data
- useEffect(() => {
- fetch('http://localhost:8080/move')
- .then(response => response.json())
- .then(data => setMoves(data))
- .catch(error => console.error(error));
- }, []);
-
- return (
-
-
- (
- navigation.navigate('MoveDetail', {move: item})}>
- {item.name}, {item.type.name}: {item.power}
-
- )}
- keyExtractor={(item) => item.name}
- />
-
-
- );
-};
-
-export default MoveListScreen;
diff --git a/screens/moves/MoveDetailScreen.tsx b/screens/moves/MoveDetailScreen.tsx
new file mode 100644
index 0000000..0a85552
--- /dev/null
+++ b/screens/moves/MoveDetailScreen.tsx
@@ -0,0 +1,75 @@
+// screens/moves/MoveDetailScreen.tsx
+
+import React, { useEffect } from 'react';
+import { Button, ScrollView, StyleSheet, Text, View } from 'react-native';
+import { RouteProp } from '@react-navigation/native';
+import { RootStackParamList } from "../../navigation/navigationTypes";
+import TypeTacticsInfoList from "../../components/TypeTacticsInfoList"
+import { StackNavigationProp } from "@react-navigation/stack";
+import { MOVE_DETAIL, MOVE_FORM } from '../../navigation/constants';
+import { useSelector } from "react-redux";
+import { RootState } from "../../redux/store";
+import { Move } from "../../entities/Move";
+
+
+type MoveDetailScreenNavigationProp = StackNavigationProp;
+type MoveDetailScreenRouteProp = RouteProp;
+
+type Props = {
+ navigation: MoveDetailScreenNavigationProp;
+ route: MoveDetailScreenRouteProp;
+};
+
+const MoveDetailScreen = ({ navigation, route }: Props) => {
+ const move =
+ useSelector(
+ (state: RootState) => state.move.moves.find(
+ (m: Move) => m.id === route.params.move.id
+ )
+ );
+
+
+ useEffect(() => {
+ navigation.setOptions({ title: move?.name });
+ }, [move]);
+
+ return (
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#FFFFFF',
+ padding: 20,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ marginBottom: 10,
+ },
+ detail: {
+ fontSize: 18,
+ marginBottom: 5,
+ },
+ typeListsContainer: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ justifyContent: 'space-around',
+ marginTop: 10,
+ },
+});
+
+export default MoveDetailScreen;
diff --git a/screens/moves/MoveFormScreen.tsx b/screens/moves/MoveFormScreen.tsx
new file mode 100644
index 0000000..6a9e570
--- /dev/null
+++ b/screens/moves/MoveFormScreen.tsx
@@ -0,0 +1,166 @@
+// screens/moves/MoveFormScreen.tsx
+
+import React, { useState } from 'react';
+import { Button, StyleSheet, Text, TextInput } from 'react-native';
+import { StackNavigationProp } from '@react-navigation/stack';
+import { RootStackParamList } from "../../navigation/navigationTypes";
+import { useDispatch, useSelector } from 'react-redux';
+import { createMove, updateMove } from '../../redux/actions/moveActions';
+import { AppDispatch, RootState } from "../../redux/store";
+import { Move } from "../../entities/Move";
+import { RouteProp } from "@react-navigation/native";
+import { MOVE_FORM } from "../../navigation/constants";
+import { Picker } from "@react-native-community/picker";
+import { ItemValue } from "@react-native-community/picker/typings/Picker";
+import { MoveCategoryName } from "../../entities/MoveCategoryName";
+import { TypeName } from "../../entities/TypeName";
+import MultiSelect from "react-native-multiple-select";
+import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
+import AlertModal from "../../components/AlertModal";
+import { MOVE_ERROR } from "../../redux/constants";
+
+type MoveFormScreenNavigationProp = StackNavigationProp;
+type MoveFormScreenRouteProp = RouteProp;
+
+type Props = {
+ navigation: MoveFormScreenNavigationProp;
+ route: MoveFormScreenRouteProp
+};
+
+const MoveFormScreen = ({ navigation, route }: Props) => {
+ const error = useSelector((state: RootState) => state.move.error);
+ const dispatch = useDispatch();
+ const [move, setMove] = useState(route.params?.move || {
+ id: null,
+ name: '',
+ category: MoveCategoryName.PHYSICAL,
+ power: 0,
+ accuracy: 0,
+ type: {
+ name: TypeName.NORMAL,
+ weakAgainst: [],
+ effectiveAgainst: [],
+ },
+ schemaVersion: 2
+ });
+
+ const [selectedWeakAgainst, setSelectedWeakAgainst] = useState(move.type.weakAgainst);
+ const [selectedEffectiveAgainst, setSelectedEffectiveAgainst] = useState(move.type.effectiveAgainst);
+
+ const handleSelectType = (selectedTypes: string[], setTypes: React.Dispatch>, otherSelectedTypes: string[]) => {
+ const uniqueSelectedTypes = Array.from(new Set(selectedTypes));
+ const withoutDuplicatesFromOtherColumn = uniqueSelectedTypes.filter(type => !otherSelectedTypes.includes(type));
+ setTypes(withoutDuplicatesFromOtherColumn);
+ };
+
+ const handleSave = () => {
+ if (route.params?.move) {
+ (dispatch as AppDispatch)(updateMove(route.params.move.id!, {
+ ...move,
+ type: { ...move.type, weakAgainst: selectedWeakAgainst, effectiveAgainst: selectedEffectiveAgainst }
+ }));
+ }
+ else {
+ (dispatch as AppDispatch)(createMove({
+ ...move,
+ type: { ...move.type, weakAgainst: selectedWeakAgainst, effectiveAgainst: selectedEffectiveAgainst }
+ }));
+ }
+ navigation.goBack();
+ };
+
+ return (
+
+ dispatch({ type: MOVE_ERROR, payload: null })}
+ />
+ Name:
+ setMove({ ...move, name: text })}
+ style={styles.input}
+ />
+ Category:
+
+ setMove({ ...move, category: itemValue as MoveCategoryName })
+ }>
+ {Object.values(MoveCategoryName).map((value) =>
+
+ )}
+
+ Power:
+ setMove({ ...move, power: Number(text) })}
+ style={styles.input}
+ keyboardType="numeric"
+ />
+ Accuracy:
+ setMove({ ...move, accuracy: Number(text) })}
+ style={styles.input}
+ keyboardType="numeric"
+ />
+ Type:
+
+ setMove({ ...move, type: { ...move.type, name: itemValue as TypeName } })
+ }>
+ {Object.values(TypeName).map((value) =>
+
+ )}
+
+ Weak Against:
+ ({ id: value, name: value }))
+ }
+ uniqueKey="id"
+ onSelectedItemsChange={
+ (selectedItems) => handleSelectType(selectedItems, setSelectedWeakAgainst, selectedEffectiveAgainst)
+ }
+ selectedItems={selectedWeakAgainst}
+ displayKey="name"
+ />
+ Effective Against:
+ ({ id: value, name: value }))
+ }
+ uniqueKey="id"
+ onSelectedItemsChange={
+ (selectedItems) => handleSelectType(selectedItems, setSelectedEffectiveAgainst, selectedWeakAgainst)
+ }
+ selectedItems={selectedEffectiveAgainst}
+ displayKey="name"
+ />
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 16,
+ },
+ input: {
+ backgroundColor: '#CEC',
+ borderRadius: 8,
+ height: 32,
+ },
+ label: {
+ margin: 8,
+ }
+});
+
+export default MoveFormScreen;
diff --git a/screens/moves/MoveListScreen.tsx b/screens/moves/MoveListScreen.tsx
new file mode 100644
index 0000000..4f2664f
--- /dev/null
+++ b/screens/moves/MoveListScreen.tsx
@@ -0,0 +1,93 @@
+// screens/moves/MoveListScreen.tsx
+
+import React from 'react';
+import { Button, FlatList, StyleSheet, View } from 'react-native';
+import { StackNavigationProp } from '@react-navigation/stack';
+import { RootStackParamList } from "../../navigation/navigationTypes";
+import { useDispatch, useSelector } from 'react-redux';
+import { deleteMove, getMoves } from '../../redux/actions/moveActions';
+import { MoveState } from "../../redux/reducers/moveReducer";
+import { AppDispatch } from "../../redux/store";
+import MoveListItem from "../../components/MoveListItem";
+import { MOVE_DETAIL, MOVE_FORM, MOVE_LIST } from "../../navigation/constants";
+import { RouteProp, useFocusEffect } from "@react-navigation/native";
+import AlertModal from "../../components/AlertModal";
+import { MOVE_ERROR } from "../../redux/constants";
+
+type MoveListScreenNavigationProp = StackNavigationProp;
+type MoveListScreenRouteProp = RouteProp;
+
+type Props = {
+ navigation: MoveListScreenNavigationProp;
+ route: MoveListScreenRouteProp;
+};
+type RootState = {
+ move: MoveState;
+};
+
+const MoveListScreen = ({ navigation }: Props) => {
+ const error = useSelector((state: RootState) => state.move.error);
+ const dispatch: AppDispatch = useDispatch();
+ const moves = useSelector((state: RootState) => state.move.moves);
+
+ useFocusEffect(
+ React.useCallback(() => {
+ const loadMoves = async () => {
+ await (dispatch as AppDispatch)(getMoves());
+ };
+ loadMoves();
+ }, [dispatch])
+ );
+
+ return (
+ <>
+ dispatch({ type: MOVE_ERROR, payload: null })}
+ />
+ navigation.navigate(MOVE_FORM, { move: undefined })}/>
+ }
+ renderItem={({ item }) => (
+
+ navigation.navigate(MOVE_DETAIL, { move: item })}
+ />
+
+ )}
+ keyExtractor={(item) => item.name}
+ />
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ deleteButton: {
+ backgroundColor: '#FF6961',
+ },
+ listItemContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginHorizontal: 16,
+ marginTop: 8,
+ },
+ moveListItem: {
+ flex: 1,
+ marginRight: 8
+ }
+});
+
+export default MoveListScreen;
diff --git a/yarn.lock b/yarn.lock
index 563e42a..a09537a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1044,6 +1044,13 @@
dependencies:
regenerator-runtime "^0.13.11"
+"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2":
+ version "7.22.3"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.3.tgz#0a7fce51d43adbf0f7b517a71f4c3aaca92ebcbb"
+ integrity sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==
+ dependencies:
+ regenerator-runtime "^0.13.11"
+
"@babel/template@^7.0.0", "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3":
version "7.20.7"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz"
@@ -2074,6 +2081,11 @@
prompts "^2.4.0"
semver "^6.3.0"
+"@react-native-community/picker@^1.8.1":
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/@react-native-community/picker/-/picker-1.8.1.tgz#94f14f0aad98fa7592967b941be97deec95c3805"
+ integrity sha512-Sj9DzX1CSnmYiuEQ5fQhExoo4XjSKoZkqLPAAybycq6RHtCuWppf+eJXRMCOJki25BlKSSt+qVqg0fIe//ujNQ==
+
"@react-native/assets@1.0.0":
version "1.0.0"
resolved "https://registry.npmjs.org/@react-native/assets/-/assets-1.0.0.tgz"
@@ -2141,6 +2153,16 @@
color "^4.2.3"
warn-once "^0.1.0"
+"@reduxjs/toolkit@^1.9.5":
+ version "1.9.5"
+ resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4"
+ integrity sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==
+ dependencies:
+ immer "^9.0.21"
+ redux "^4.2.1"
+ redux-thunk "^2.4.2"
+ reselect "^4.1.8"
+
"@segment/loosely-validate-event@^2.0.0":
version "2.0.0"
resolved "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz"
@@ -2326,6 +2348,14 @@
resolved "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz"
integrity sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==
+"@types/hoist-non-react-statics@^3.3.1":
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
+ integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
+ dependencies:
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+
"@types/html-minifier-terser@^6.0.0":
version "6.1.0"
resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz"
@@ -2421,6 +2451,15 @@
dependencies:
"@types/react" "^17"
+"@types/react@*":
+ version "18.2.8"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.8.tgz#a77dcffe4e9af148ca4aa8000c51a1e8ed99e2c8"
+ integrity sha512-lTyWUNrd8ntVkqycEEplasWy2OxNlShj3zqS0LuB1ENUGis5HodmhM7DtCoUGbxj3VW/WsGA0DUhpG6XrM7gPA==
+ dependencies:
+ "@types/prop-types" "*"
+ "@types/scheduler" "*"
+ csstype "^3.0.2"
+
"@types/react@^17":
version "17.0.59"
resolved "https://registry.npmjs.org/@types/react/-/react-17.0.59.tgz"
@@ -2489,6 +2528,11 @@
resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz"
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
+"@types/use-sync-external-store@^0.0.3":
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
+ integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
+
"@types/ws@^8.5.1":
version "8.5.4"
resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz"
@@ -5365,7 +5409,7 @@ hermes-profile-transformer@^0.0.6:
dependencies:
source-map "^0.7.3"
-hoist-non-react-statics@^3.3.0:
+hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -5552,6 +5596,11 @@ image-size@^0.6.0:
resolved "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz"
integrity sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA==
+immer@^9.0.21:
+ version "9.0.21"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176"
+ integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==
+
import-fresh@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz"
@@ -8252,7 +8301,7 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.2, prompts@^2.4.0:
kleur "^3.0.3"
sisteransi "^1.0.5"
-prop-types@*, prop-types@^15.7.2:
+prop-types@*, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -8428,6 +8477,26 @@ react-native-gradle-plugin@^0.71.18:
resolved "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.18.tgz"
integrity sha512-7F6bD7B8Xsn3JllxcwHhFcsl9aHIig47+3eN4IHFNqfLhZr++3ElDrcqfMzugM+niWbaMi7bJ0kAkAL8eCpdWg==
+react-native-iphone-x-helper@^1.0.3:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010"
+ integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==
+
+react-native-keyboard-aware-scroll-view@^0.9.5:
+ version "0.9.5"
+ resolved "https://registry.yarnpkg.com/react-native-keyboard-aware-scroll-view/-/react-native-keyboard-aware-scroll-view-0.9.5.tgz#e2e9665d320c188e6b1f22f151b94eb358bf9b71"
+ integrity sha512-XwfRn+T/qBH9WjTWIBiJD2hPWg0yJvtaEw6RtPCa5/PYHabzBaWxYBOl0usXN/368BL1XktnZPh8C2lmTpOREA==
+ dependencies:
+ prop-types "^15.6.2"
+ react-native-iphone-x-helper "^1.0.3"
+
+react-native-multiple-select@^0.5.12:
+ version "0.5.12"
+ resolved "https://registry.yarnpkg.com/react-native-multiple-select/-/react-native-multiple-select-0.5.12.tgz#be9204f49bc1bb734c40422a89acc173959bcd70"
+ integrity sha512-lFw0u798/2qHr4TwDdxMtReRtsNOCC2SWPzWHRGKE4XcBiUll0hHhke7iqQg4xJdfo46C/h69f1ZXphDOjZY3A==
+ dependencies:
+ prop-types "^15.7.2"
+
react-native-safe-area-context@4.5.0:
version "4.5.0"
resolved "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.5.0.tgz"
@@ -8494,6 +8563,18 @@ react-native@0.71.8:
whatwg-fetch "^3.0.0"
ws "^6.2.2"
+react-redux@^8.0.7:
+ version "8.0.7"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.7.tgz#b74ef2f7ce2076e354540aa3511d3670c2b62571"
+ integrity sha512-1vRQuCQI5Y2uNmrMXg81RXKiBHY3jBzvCvNmZF437O/Z9/pZ+ba2uYHbemYXb3g8rjsacBGo+/wmfrQKzMhJsg==
+ dependencies:
+ "@babel/runtime" "^7.12.1"
+ "@types/hoist-non-react-statics" "^3.3.1"
+ "@types/use-sync-external-store" "^0.0.3"
+ hoist-non-react-statics "^3.3.2"
+ react-is "^18.0.0"
+ use-sync-external-store "^1.0.0"
+
react-refresh@^0.4.0:
version "0.4.3"
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz"
@@ -8567,6 +8648,18 @@ recast@^0.20.4:
source-map "~0.6.1"
tslib "^2.0.1"
+redux-thunk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b"
+ integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==
+
+redux@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
+ integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
+ dependencies:
+ "@babel/runtime" "^7.9.2"
+
regenerate-unicode-properties@^10.1.0:
version "10.1.0"
resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz"
@@ -8693,7 +8786,7 @@ requires-port@^1.0.0:
resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz"
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
-reselect@^4.0.0:
+reselect@^4.0.0, reselect@^4.1.8:
version "4.1.8"
resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz"
integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==