CRUDify Moves (Fix #6) (#11)

### Contents
🗃️ Use redux to fetch data
🧭 Add nav options
 Handle API 2xx responses
🥅 Handle API errors

Co-authored-by: Alexis DRAI <alexis.drai@etu.uca.fr>
Reviewed-on: #11
Alexis Drai 2 years ago committed by Alexis Drai
parent 6bb5f81f5f
commit b28088e987

@ -10,8 +10,8 @@ steps:
- name: sonar-analysis - name: sonar-analysis
image: hub.codefirst.iut.uca.fr/camille.petitalot/drone-sonarplugin-reactnative:latest image: hub.codefirst.iut.uca.fr/camille.petitalot/drone-sonarplugin-reactnative:latest
commands: commands:
- npm install - yarn install
- npm run test:coverage - yarn test:coverage
- ls ./coverage - ls ./coverage
- sonar-scanner - sonar-scanner
-Dsonar.projectKey=AD_multiplat -Dsonar.projectKey=AD_multiplat
@ -19,7 +19,7 @@ steps:
-Dsonar.host.url=$${PLUGIN_SONAR_HOST} -Dsonar.host.url=$${PLUGIN_SONAR_HOST}
-Dsonar.login=$${PLUGIN_SONAR_TOKEN} -Dsonar.login=$${PLUGIN_SONAR_TOKEN}
-Dsonar.javascript.lcov.reportPaths=./coverage/lcov.info -Dsonar.javascript.lcov.reportPaths=./coverage/lcov.info
-Dsonar.exclusions=**/*.test.tsx,**/*.test.ts,**/*.spec.tsx,**/*.spec.ts,**/lcov-report/** -Dsonar.exclusions=**/*.test.tsx,**/*.test.ts,**/*.spec.tsx,**/*.spec.ts,**/lcov-report/**,**/constants.ts,config.ts,babel.config.ts
settings: settings:
sonar_host: https://codefirst.iut.uca.fr/sonar/ sonar_host: https://codefirst.iut.uca.fr/sonar/
sonar_token: sonar_token:

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

@ -1,13 +1,13 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="start" type="js.build_tools.npm" nameIsGenerated="true"> <configuration default="false" name="start" type="js.build_tools.npm" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" /> <package-json value="$PROJECT_DIR$/package.json"/>
<command value="run" /> <command value="run"/>
<scripts> <scripts>
<script value="start" /> <script value="start"/>
</scripts> </scripts>
<node-interpreter value="project" /> <node-interpreter value="project"/>
<package-manager value="C:\Users\draia\AppData\Roaming\npm\node_modules\npm" /> <package-manager value="yarn"/>
<envs /> <envs/>
<method v="2" /> <method v="2"/>
</configuration> </configuration>
</component> </component>

@ -1,7 +1,16 @@
import React from 'react'; // App.tsx
import Navigation from "./navigation/Navigation";
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() { export default function App() {
return <Navigation/>; return (
// TODO Send to homescreen instead, and include a bottom bar to navigate to Moves, Pokemongs, Trainers <Provider store={store}>
<SafeAreaProvider>
<Navigation/>
</SafeAreaProvider>
</Provider>);
} }

@ -17,23 +17,47 @@
# AD_ReactNative # 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). to [instructions here](https://react-native-courses.clubinfo-clermont.fr/docs/notation).
+ [Notation checklist](#notation-checklist)
+ [Sketches](#sketches) + [Sketches](#sketches)
- [Trainers](#trainers) - [Pokemongs](#pokemongs)
- [Pokemongs](#pokemongs) - [Moves](#moves)
- [Moves](#moves)
+ [Using the app](#using-the-app) + [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 ## Sketches
This app will contain several "master/detail" tabs. They are as follows. This app will contain several "master/detail" tabs. They are as follows.
### Trainers
<img src="./docs/trainers.jpg" width="540" style="margin:20px">
### Pokemongs ### Pokemongs
<img src="./docs/pokemongs.jpg" width="540" style="margin:20px"> <img src="./docs/pokemongs.jpg" width="540" style="margin:20px">
@ -44,5 +68,6 @@ This app will contain several "master/detail" tabs. They are as follows.
## Using the app ## 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 This app is linked to a backend that is set up to accept CORS from [`http://localhost:19006`](http://localhost:19006).
not overriding that default port number when running it. If you want to use the dedicated API, please make sure you're not overriding that default port number when running this
app.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

@ -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 (
<Modal
animationType="slide"
transparent={true}
visible={visible}
onRequestClose={onClose}
>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<Text style={styles.modalText}>{message}</Text>
<Button
title="Close"
onPress={onClose}
/>
</View>
</View>
</Modal>
);
};
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;

@ -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<MoveListItemProps> = ({ move, onPress, style }) => (
<TouchableOpacity style={[styles.listItem, style]} onPress={onPress}>
<Text style={styles.listItemText} numberOfLines={1}
ellipsizeMode="tail">{move.name}, {move.type.name}: {move.power}</Text>
</TouchableOpacity>
);
const styles = StyleSheet.create({
listItem: {
backgroundColor: '#DDD',
padding: 8,
borderRadius: 8,
},
listItemText: {
color: '#333',
fontSize: 18,
},
});
export default MoveListItem;

@ -1,11 +1,14 @@
import React from 'react'; // components/TypeTacticsInfoList.test.ts
import {render} from '@testing-library/react-native';
import React from 'react';
import { render } from '@testing-library/react-native';
import TypeTacticsInfoList from './TypeTacticsInfoList'; import TypeTacticsInfoList from './TypeTacticsInfoList';
import { TypeName } from "../entities/TypeName";
describe('TypeTacticsInfoList component', () => { describe('TypeTacticsInfoList component', () => {
it('renders types correctly', () => { it('renders types correctly', () => {
const types = ['FIRE', 'WATER', 'GRASS']; const types = [TypeName.FIRE, TypeName.WATER, TypeName.GRASS];
const {getByText} = render(<TypeTacticsInfoList isWeakness={true} types={types}/>); const { getByText } = render(<TypeTacticsInfoList isWeakness={true} types={types}/>);
types.forEach(type => { types.forEach(type => {
expect(getByText(type)).toBeTruthy(); expect(getByText(type)).toBeTruthy();
@ -13,13 +16,13 @@ describe('TypeTacticsInfoList component', () => {
}); });
it('renders "Nothing" when types array is empty', () => { it('renders "Nothing" when types array is empty', () => {
const {getByText} = render(<TypeTacticsInfoList isWeakness={false} types={[]}/>); const { getByText } = render(<TypeTacticsInfoList isWeakness={false} types={[]}/>);
expect(getByText('Nothing')).toBeTruthy(); expect(getByText('Nothing')).toBeTruthy();
}); });
it('renders "Nothing" when types is undefined', () => { it('renders "Nothing" when types is undefined', () => {
// @ts-ignore // @ts-ignore
const {getByText} = render(<TypeTacticsInfoList isWeakness={true} types={undefined}/>); const { getByText } = render(<TypeTacticsInfoList isWeakness={true} types={undefined}/>);
expect(getByText('Nothing')).toBeTruthy(); expect(getByText('Nothing')).toBeTruthy();
}); });
}); });

@ -1,39 +1,56 @@
import React from "react"; // components/TypeTacticsInfoList.ts
import {StyleSheet, View, Text, ScrollView} from "react-native";
import React from "react";
import { ScrollView, StyleSheet, Text, View } from "react-native";
type TypeListProps = { type TypeListProps = {
isWeakness: boolean; isWeakness: boolean;
types: string[]; types: string[];
}; };
const TypeTacticsInfoList = ({isWeakness, types}: TypeListProps) => { const TypeTacticsInfoList = ({ isWeakness, types }: TypeListProps) => {
if (!types || types.length === 0) { if (!types || types.length === 0) {
types = ['Nothing']; types = ['NOTHING'];
} }
return ( return (
<ScrollView style={isWeakness ? styles.weakAgainst : styles.effectiveAgainst}> <View style={isWeakness ? styles.weakAgainst : styles.effectiveAgainst}>
<View style={styles.list}> <Text style={styles.title}>{isWeakness ? 'Weak Against' : 'Effective Against'}:</Text>
<Text>{isWeakness ? 'weak against' : 'effective against'}:</Text> <ScrollView>
{types.map((type, index) => ( <View style={styles.list}>
<Text key={index}>{type}</Text> {types.map((type, index) => (
))} <Text key={index} style={styles.type}>{type}</Text>
</View> ))}
</ScrollView> </View>
</ScrollView>
</View>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
list: { list: {
borderRadius: 5,
padding: 10, padding: 10,
marginBottom: 10, },
title: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 5,
},
type: {
fontSize: 16,
marginBottom: 5,
}, },
weakAgainst: { weakAgainst: {
backgroundColor: '#FF6961', backgroundColor: '#FF6961',
borderRadius: 10,
marginBottom: 10,
padding: 10,
}, },
effectiveAgainst: { effectiveAgainst: {
backgroundColor: '#77DD77', backgroundColor: '#77DD77',
borderRadius: 10,
marginBottom: 10,
padding: 10,
}, },
}); });

@ -0,0 +1,2 @@
// config.ts
export const API_BASE_URL = 'http://localhost:8080';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

@ -1,9 +1,14 @@
import {Type} from "./Type"; // entities/Move.ts
import { Type } from "./Type";
import { MoveCategoryName } from "./MoveCategoryName";
export interface Move { export interface Move {
id: string | null;
name: string; name: string;
category: string; category: MoveCategoryName;
power: number; power: number;
accuracy: number; accuracy: number;
type: Type; type: Type;
schemaVersion: number;
} }

@ -0,0 +1,5 @@
export enum MoveCategoryName {
PHYSICAL = 'PHYSICAL',
SPECIAL = 'SPECIAL',
STATUS = 'STATUS',
}

@ -1,5 +1,9 @@
// entities/Type.ts
import { TypeName } from "./TypeName";
export interface Type { export interface Type {
name: string; name: TypeName;
weakAgainst: string[]; weakAgainst: string[];
effectiveAgainst: string[]; effectiveAgainst: string[];
} }

@ -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',
}

@ -1,22 +1,59 @@
import React from 'react'; // navigation/Navigation.tsx
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack'; import React from 'react';
import MoveListScreen from '../screens/MoveListScreen'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import MoveDetailScreen from '../screens/MoveDetailScreen'; import { NavigationContainer } from '@react-navigation/native';
import {RootStackParamList} from "./navigationTypes"; 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<RootStackParamList>(); const Stack = createStackNavigator<RootStackParamList>();
const Tab = createBottomTabNavigator<RootTabParamList>();
const MoveStack = () => {
return (
<Stack.Navigator initialRouteName="MoveList">
<Stack.Screen name="MoveList" component={MoveListScreen} options={{ title: 'Moves' }}/>
<Stack.Screen
name="MoveDetail"
component={MoveDetailScreen}
options={({ route }) => ({ title: route.params.move.name })}
/>
<Stack.Screen name="MoveForm" component={MoveFormScreen}/>
</Stack.Navigator>
);
};
const Navigation = () => { const Navigation = () => {
// TODO replace 'Move Detail' by the move name itself
return ( return (
<NavigationContainer> <NavigationContainer>
<Stack.Navigator initialRouteName="MoveList"> <Tab.Navigator initialRouteName="Home"
<Stack.Screen name="MoveList" component={MoveListScreen} options={{title: 'Moves'}}/> screenOptions={{ headerShown: false }}>
<Stack.Screen name="MoveDetail" component={MoveDetailScreen} options={{title: 'Move Detail'}}/> <Tab.Screen name="Home" component={HomeScreen} options={{
</Stack.Navigator> title: 'Home',
tabBarIcon: () => <Image source={require('../assets/home.png')}
style={styles.icon}/>
}}/>
<Tab.Screen name="Moves" component={MoveStack} options={{
title: 'Moves',
tabBarIcon: () => <Image source={require('../assets/moves.png')}
style={styles.icon}/>
}}/>
</Tab.Navigator>
</NavigationContainer> </NavigationContainer>
); );
}; };
const styles = StyleSheet.create({
icon: {
width: 24,
height: 24,
padding: 8,
},
});
export default Navigation; export default Navigation;

@ -0,0 +1,5 @@
// navigation/constants.ts
export const MOVE_DETAIL = 'MoveDetail';
export const MOVE_FORM = 'MoveForm';
export const MOVE_LIST = 'MoveList';

@ -1,6 +1,14 @@
import {Move} from "../entities/Move"; // navigation/navigationTypes.ts
import { Move } from "../entities/Move";
export type RootStackParamList = { export type RootStackParamList = {
MoveList: undefined; MoveList: undefined;
MoveDetail: { move: Move }; MoveDetail: { move: Move };
MoveForm: { move?: Move };
};
export type RootTabParamList = {
Home: undefined;
Moves: undefined;
}; };

@ -36,9 +36,11 @@
}, },
"dependencies": { "dependencies": {
"@expo/webpack-config": "^18.0.1", "@expo/webpack-config": "^18.0.1",
"@react-native-community/picker": "^1.8.1",
"@react-navigation/bottom-tabs": "^6.5.7", "@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/native": "^6.1.6", "@react-navigation/native": "^6.1.6",
"@react-navigation/stack": "^6.3.16", "@react-navigation/stack": "^6.3.16",
"@reduxjs/toolkit": "^1.9.5",
"@testing-library/react-native": "^12.1.2", "@testing-library/react-native": "^12.1.2",
"@types/react": "~18.0.27", "@types/react": "~18.0.27",
"expo": "^48.0.0", "expo": "^48.0.0",
@ -47,9 +49,14 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-native": "0.71.8", "react-native": "0.71.8",
"react-native-gesture-handler": "~2.9.0", "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-safe-area-context": "4.5.0",
"react-native-screens": "~3.20.0", "react-native-screens": "~3.20.0",
"react-native-web": "~0.18.11", "react-native-web": "~0.18.11",
"react-redux": "^8.0.7",
"redux": "^4.2.1",
"redux-thunk": "^2.4.2",
"typescript": "^4.9.4" "typescript": "^4.9.4"
}, },
"devDependencies": { "devDependencies": {

@ -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 });
}
}
}

@ -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';

@ -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;
}
}

@ -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;

@ -0,0 +1,14 @@
// screens/HomeScreen.tsx
import React from 'react';
import { Image, View } from 'react-native';
const HomeScreen = () => {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Image source={require('../assets/logo.png')} style={{ width: 500, height: 500 }}/>
</View>
);
};
export default HomeScreen;

@ -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<RootStackParamList, 'MoveDetail'>;
type Props = {
route: MoveDetailScreenRouteProp;
};
const MoveDetailScreen = ({route}: Props) => {
const {move} = route.params;
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>{move.name}</Text>
<Text>Name: {move.name}</Text>
<Text>Category: {move.category}</Text>
<Text>Power: {move.power}</Text>
<Text>Accuracy: {move.accuracy}</Text>
<Text>Type: {move.type.name}</Text>
<View style={styles.typeListsContainer}>
<TypeTacticsInfoList isWeakness={true} types={move.type.weakAgainst}/>
<TypeTacticsInfoList isWeakness={false} types={move.type.effectiveAgainst}/>
</View>
</ScrollView>
);
};
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;

@ -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<RootStackParamList, 'MoveList'>;
type Props = {
navigation: MoveListScreenNavigationProp;
};
const MoveListScreen = ({navigation}: Props) => {
const [moves, setMoves] = useState<Move[]>([]);
// 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 (
<ScrollView>
<View>
<FlatList
data={moves}
renderItem={({item}) => (
<TouchableOpacity onPress={() => navigation.navigate('MoveDetail', {move: item})}>
<Text>{item.name}, {item.type.name}: {item.power}</Text>
</TouchableOpacity>
)}
keyExtractor={(item) => item.name}
/>
</View>
</ScrollView>
);
};
export default MoveListScreen;

@ -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<RootStackParamList, typeof MOVE_DETAIL>;
type MoveDetailScreenRouteProp = RouteProp<RootStackParamList, typeof MOVE_DETAIL>;
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 (
<ScrollView style={styles.container}>
<Button title="Edit Move" onPress={() => navigation.navigate(MOVE_FORM, { move: move })}/>
<Text style={styles.title}>Name: {move?.name}</Text>
<Text style={styles.detail}>Category: {move?.category}</Text>
<Text style={styles.detail}>Power: {move?.power}</Text>
<Text style={styles.detail}>Accuracy: {move?.accuracy}</Text>
<Text style={styles.detail}>Type: {move?.type.name}</Text>
<View style={styles.typeListsContainer}>
<TypeTacticsInfoList isWeakness={true} types={move?.type.weakAgainst || []}/>
<TypeTacticsInfoList isWeakness={false} types={move?.type.effectiveAgainst || []}/>
</View>
</ScrollView>
);
};
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;

@ -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<RootStackParamList, typeof MOVE_FORM>;
type MoveFormScreenRouteProp = RouteProp<RootStackParamList, typeof MOVE_FORM>;
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<Move>(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<string[]>(move.type.weakAgainst);
const [selectedEffectiveAgainst, setSelectedEffectiveAgainst] = useState<string[]>(move.type.effectiveAgainst);
const handleSelectType = (selectedTypes: string[], setTypes: React.Dispatch<React.SetStateAction<string[]>>, 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 (
<KeyboardAwareScrollView style={styles.container}>
<AlertModal
visible={!!error}
message={error || ''}
onClose={() => dispatch({ type: MOVE_ERROR, payload: null })}
/>
<Text style={styles.label}>Name: </Text>
<TextInput
value={move.name}
onChangeText={(text) => setMove({ ...move, name: text })}
style={styles.input}
/>
<Text style={styles.label}>Category: </Text>
<Picker
selectedValue={move.category}
style={styles.input}
onValueChange={(itemValue: ItemValue) =>
setMove({ ...move, category: itemValue as MoveCategoryName })
}>
{Object.values(MoveCategoryName).map((value) =>
<Picker.Item key={value} label={value} value={value}/>
)}
</Picker>
<Text style={styles.label}>Power: </Text>
<TextInput
value={move.power.toString()}
onChangeText={(text) => setMove({ ...move, power: Number(text) })}
style={styles.input}
keyboardType="numeric"
/>
<Text style={styles.label}>Accuracy: </Text>
<TextInput
value={move.accuracy.toString()}
onChangeText={(text) => setMove({ ...move, accuracy: Number(text) })}
style={styles.input}
keyboardType="numeric"
/>
<Text style={styles.label}>Type: </Text>
<Picker
selectedValue={move.type.name}
style={styles.input}
onValueChange={(itemValue: ItemValue) =>
setMove({ ...move, type: { ...move.type, name: itemValue as TypeName } })
}>
{Object.values(TypeName).map((value) =>
<Picker.Item key={value} label={value} value={value}/>
)}
</Picker>
<Text style={styles.label}>Weak Against: </Text>
<MultiSelect
items={
Object.values(TypeName).map((value) => ({ id: value, name: value }))
}
uniqueKey="id"
onSelectedItemsChange={
(selectedItems) => handleSelectType(selectedItems, setSelectedWeakAgainst, selectedEffectiveAgainst)
}
selectedItems={selectedWeakAgainst}
displayKey="name"
/>
<Text style={styles.label}>Effective Against: </Text>
<MultiSelect
items={
Object.values(TypeName).map((value) => ({ id: value, name: value }))
}
uniqueKey="id"
onSelectedItemsChange={
(selectedItems) => handleSelectType(selectedItems, setSelectedEffectiveAgainst, selectedWeakAgainst)
}
selectedItems={selectedEffectiveAgainst}
displayKey="name"
/>
<Button title="Save" onPress={handleSave}/>
</KeyboardAwareScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
input: {
backgroundColor: '#CEC',
borderRadius: 8,
height: 32,
},
label: {
margin: 8,
}
});
export default MoveFormScreen;

@ -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<RootStackParamList, typeof MOVE_LIST>;
type MoveListScreenRouteProp = RouteProp<RootStackParamList, typeof MOVE_LIST>;
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 (
<>
<AlertModal
visible={!!error}
message={error || ''}
onClose={() => dispatch({ type: MOVE_ERROR, payload: null })}
/>
<FlatList
data={moves}
ListHeaderComponent={
<Button title="Add Move" onPress={() => navigation.navigate(MOVE_FORM, { move: undefined })}/>
}
renderItem={({ item }) => (
<View style={styles.listItemContainer}>
<MoveListItem
move={item}
style={styles.moveListItem}
onPress={() => navigation.navigate(MOVE_DETAIL, { move: item })}
/>
<Button title="X"
color={styles.deleteButton.backgroundColor}
onPress={() => {
if (item.id) {
dispatch(deleteMove(item.id))
}
}}/>
</View>
)}
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;

@ -1044,6 +1044,13 @@
dependencies: dependencies:
regenerator-runtime "^0.13.11" 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": "@babel/template@^7.0.0", "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3":
version "7.20.7" version "7.20.7"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz" resolved "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz"
@ -2074,6 +2081,11 @@
prompts "^2.4.0" prompts "^2.4.0"
semver "^6.3.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": "@react-native/assets@1.0.0":
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/@react-native/assets/-/assets-1.0.0.tgz" resolved "https://registry.npmjs.org/@react-native/assets/-/assets-1.0.0.tgz"
@ -2141,6 +2153,16 @@
color "^4.2.3" color "^4.2.3"
warn-once "^0.1.0" 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": "@segment/loosely-validate-event@^2.0.0":
version "2.0.0" version "2.0.0"
resolved "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz" 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" resolved "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz"
integrity sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA== 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": "@types/html-minifier-terser@^6.0.0":
version "6.1.0" version "6.1.0"
resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz"
@ -2421,6 +2451,15 @@
dependencies: dependencies:
"@types/react" "^17" "@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": "@types/react@^17":
version "17.0.59" version "17.0.59"
resolved "https://registry.npmjs.org/@types/react/-/react-17.0.59.tgz" 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" resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz"
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== 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": "@types/ws@^8.5.1":
version "8.5.4" version "8.5.4"
resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz" resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz"
@ -5365,7 +5409,7 @@ hermes-profile-transformer@^0.0.6:
dependencies: dependencies:
source-map "^0.7.3" 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" version "3.3.2"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== 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" resolved "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz"
integrity sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA== 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: import-fresh@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz" 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" kleur "^3.0.3"
sisteransi "^1.0.5" 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" version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== 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" resolved "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.18.tgz"
integrity sha512-7F6bD7B8Xsn3JllxcwHhFcsl9aHIig47+3eN4IHFNqfLhZr++3ElDrcqfMzugM+niWbaMi7bJ0kAkAL8eCpdWg== 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: react-native-safe-area-context@4.5.0:
version "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" 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" whatwg-fetch "^3.0.0"
ws "^6.2.2" 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: react-refresh@^0.4.0:
version "0.4.3" version "0.4.3"
resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz" 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" source-map "~0.6.1"
tslib "^2.0.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: regenerate-unicode-properties@^10.1.0:
version "10.1.0" version "10.1.0"
resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz" 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" resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz"
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
reselect@^4.0.0: reselect@^4.0.0, reselect@^4.1.8:
version "4.1.8" version "4.1.8"
resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz" resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz"
integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==

Loading…
Cancel
Save