CRUDify Moves (Fix #6) #11

Merged
alexis.drai merged 13 commits from feature/improve-moves into main 2 years ago

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

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

@ -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 <Navigation/>;
// TODO Send to homescreen instead, and include a bottom bar to navigate to Moves, Pokemongs, Trainers
return (
<Provider store={store}>
<SafeAreaProvider>
<Navigation/>
</SafeAreaProvider>
</Provider>);
}

@ -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
<img src="./docs/trainers.jpg" width="540" style="margin:20px">
### Pokemongs
<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
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.

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';
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(<TypeTacticsInfoList isWeakness={true} types={types}/>);
const types = [TypeName.FIRE, TypeName.WATER, TypeName.GRASS];
const { getByText } = render(<TypeTacticsInfoList isWeakness={true} types={types}/>);
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(<TypeTacticsInfoList isWeakness={false} types={[]}/>);
const { getByText } = render(<TypeTacticsInfoList isWeakness={false} types={[]}/>);
expect(getByText('Nothing')).toBeTruthy();
});
it('renders "Nothing" when types is undefined', () => {
// @ts-ignore
const {getByText} = render(<TypeTacticsInfoList isWeakness={true} types={undefined}/>);
const { getByText } = render(<TypeTacticsInfoList isWeakness={true} types={undefined}/>);
expect(getByText('Nothing')).toBeTruthy();
});
});

@ -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 (
<ScrollView style={isWeakness ? styles.weakAgainst : styles.effectiveAgainst}>
<View style={styles.list}>
<Text>{isWeakness ? 'weak against' : 'effective against'}:</Text>
{types.map((type, index) => (
<Text key={index}>{type}</Text>
))}
</View>
</ScrollView>
<View style={isWeakness ? styles.weakAgainst : styles.effectiveAgainst}>
<Text style={styles.title}>{isWeakness ? 'Weak Against' : 'Effective Against'}:</Text>
<ScrollView>
<View style={styles.list}>
{types.map((type, index) => (
<Text key={index} style={styles.type}>{type}</Text>
))}
</View>
</ScrollView>
</View>
);
};
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,
},
});

@ -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 {
id: string | null;
name: string;
category: string;
category: MoveCategoryName;
power: number;
accuracy: number;
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 {
name: string;
name: TypeName;
weakAgainst: 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';
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<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 = () => {
// TODO replace 'Move Detail' by the move name itself
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="MoveList">
<Stack.Screen name="MoveList" component={MoveListScreen} options={{title: 'Moves'}}/>
<Stack.Screen name="MoveDetail" component={MoveDetailScreen} options={{title: 'Move Detail'}}/>
</Stack.Navigator>
<Tab.Navigator initialRouteName="Home"
screenOptions={{ headerShown: false }}>
<Tab.Screen name="Home" component={HomeScreen} options={{
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>
);
};
const styles = StyleSheet.create({
icon: {
width: 24,
height: 24,
padding: 8,
},
});
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 = {
MoveList: undefined;
MoveDetail: { move: Move };
MoveForm: { move?: Move };
};
export type RootTabParamList = {
Home: undefined;
Moves: undefined;
};

@ -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": {

@ -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:
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==

Loading…
Cancel
Save