CRUDify Moves

but still leaving a few todos for later
pull/11/head
Alexis Drai 2 years ago
parent bb986a8929
commit 495bdb9d3b

@ -3,10 +3,11 @@
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 types = [TypeName.FIRE, TypeName.WATER, TypeName.GRASS];
const { getByText } = render(<TypeTacticsInfoList isWeakness={true} types={types}/>);
types.forEach(type => {

@ -1,11 +1,14 @@
// entities/Move.ts
import { Type } from "./Type";
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,7 +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',
}

@ -9,6 +9,7 @@ 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>();
@ -22,6 +23,7 @@ const MoveStack = () => {
component={MoveDetailScreen}
options={({ route }) => ({ title: route.params.move.name })}
/>
<Stack.Screen name="MoveForm" component={MoveFormScreen}/>
</Stack.Navigator>
);
};

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

@ -5,6 +5,7 @@ import { Move } from "../entities/Move";
export type RootStackParamList = {
MoveList: undefined;
MoveDetail: { move: Move };
MoveForm: { move?: Move };
};
export type RootTabParamList = {

@ -36,6 +36,7 @@
},
"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",

@ -4,6 +4,7 @@ import { FETCH_MOVES } from '../constants';
import { Move } from "../../entities/Move";
import { Dispatch } from "redux";
import { API_BASE_URL } from "../../config";
import { RootState } from "../store";
export const setMoves = (moves: Move[]) => {
return {
@ -12,6 +13,25 @@ export const setMoves = (moves: Move[]) => {
};
}
export const createMove = (move: Move) => {
return async (dispatch: Dispatch) => {
try {
const response = await fetch(`${API_BASE_URL}/move`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(move),
});
const data = await response.json();
dispatch(setMoves(data));
}
catch (error) {
console.error(error);
}
}
}
export const getMoves = () => {
return async (dispatch: Dispatch) => {
try {
@ -24,3 +44,39 @@ export const getMoves = () => {
}
}
}
export const updateMove = (id: string, move: Move) => {
return async (dispatch: Dispatch, getState: () => RootState) => {
try {
const response = await fetch(`${API_BASE_URL}/move/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(move),
});
const updatedMove = await response.json();
const moves = getState().move.moves.map((m: Move) => m.id === id ? updatedMove : m);
dispatch(setMoves(moves));
}
catch (error) {
console.error(error);
}
}
}
export const deleteMove = (id: string) => {
return async (dispatch: Dispatch, getState: () => RootState) => {
try {
await fetch(`${API_BASE_URL}/move/${id}`, {
method: 'DELETE',
});
const moves = getState().move.moves.filter((m: Move) => m.id !== id);
dispatch(setMoves(moves));
}
catch (error) {
console.error(error);
}
}
}

@ -1,10 +1,14 @@
// redux/store.ts
import { configureStore } from '@reduxjs/toolkit'
import moveReducer from './reducers/moveReducer';
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

@ -1,30 +1,49 @@
// screens/moves/MoveDetailScreen.tsx
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"
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 MoveDetailScreenRouteProp = RouteProp<RootStackParamList, 'MoveDetail'>;
type MoveDetailScreenNavigationProp = StackNavigationProp<RootStackParamList, typeof MOVE_DETAIL>;
type MoveDetailScreenRouteProp = RouteProp<RootStackParamList, typeof MOVE_DETAIL>;
type Props = {
navigation: MoveDetailScreenNavigationProp;
route: MoveDetailScreenRouteProp;
};
const MoveDetailScreen = ({ route }: Props) => {
const { move } = route.params;
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}>
<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>
<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}/>
<TypeTacticsInfoList isWeakness={true} types={move?.type.weakAgainst || []}/>
<TypeTacticsInfoList isWeakness={false} types={move?.type.effectiveAgainst || []}/>
</View>
</ScrollView>
);

@ -0,0 +1,111 @@
// screens/moves/MoveFormScreen.tsx
import React, { useState } from 'react';
import { Button, StyleSheet, TextInput, View } from 'react-native';
import { StackNavigationProp } from '@react-navigation/stack';
import { RootStackParamList } from "../../navigation/navigationTypes";
import { useDispatch } from 'react-redux';
import { createMove, updateMove } from '../../redux/actions/moveActions';
import { AppDispatch } 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";
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 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 handleSave = () => {
if (route.params?.move) {
(dispatch as AppDispatch)(updateMove(route.params.move.id!, move));
}
else {
(dispatch as AppDispatch)(createMove(move));
}
navigation.goBack();
};
// TODO add labels and remove placeholders
return (
<View style={styles.container}>
<TextInput
value={move.name}
onChangeText={(text) => setMove({ ...move, name: text })}
placeholder="Name"
style={styles.input}
/>
<Picker
selectedValue={move.category}
onValueChange={(itemValue: ItemValue) =>
setMove({ ...move, category: itemValue as MoveCategoryName })
}>
{Object.values(MoveCategoryName).map((value) =>
<Picker.Item key={value} label={value} value={value}/>
)}
</Picker>
<TextInput
value={move.power.toString()}
onChangeText={(text) => setMove({ ...move, power: Number(text) })}
placeholder="Power"
style={styles.input}
keyboardType="numeric"
/>
<TextInput
value={move.accuracy.toString()}
onChangeText={(text) => setMove({ ...move, accuracy: Number(text) })}
placeholder="Accuracy"
style={styles.input}
keyboardType="numeric"
/>
<Picker
selectedValue={move.type.name}
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>
{/*TODO add weakAgainst and effectiveAgainst columns... Two pickers for that, and the ability to keep adding types in each column.. but user can't put the same type in each column or twice in a column*/}
<Button title="Save" onPress={handleSave}/>
</View>
);
};
// TODO improve styles a bit
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
input: {
backgroundColor: '#CEC',
borderRadius: 8,
},
});
export default MoveFormScreen;

@ -1,26 +1,30 @@
// screens/moves/MoveListScreen.tsx
import React, { useEffect } from 'react';
import { FlatList, ScrollView, View } from 'react-native';
import { StackNavigationProp } from '@react-navigation/stack';
import { RootStackParamList } from "../../navigation/navigationTypes";
import { useDispatch, useSelector } from 'react-redux';
import { getMoves } from '../../redux/actions/moveActions';
import { MoveState } from "../../redux/reducers/moveReducer";
import { AppDispatch } from "../../redux/store";
import MoveListItem from "../../components/MoveListItem";
type MoveListScreenNavigationProp = StackNavigationProp<RootStackParamList, 'MoveList'>;
import React, { useEffect } from 'react';
import { Button, FlatList, ScrollView, 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 } from "@react-navigation/native";
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 dispatch = useDispatch();
const dispatch: AppDispatch = useDispatch();
const moves = useSelector((state: RootState) => state.move.moves);
useEffect(() => {
@ -28,18 +32,24 @@ const MoveListScreen = ({ navigation }: Props) => {
await (dispatch as AppDispatch)(getMoves());
};
loadMoves();
}, [dispatch]);
}, [dispatch, moves]);
return (
<ScrollView>
<Button title="Add Move" onPress={() => navigation.navigate(MOVE_FORM, { move: undefined })}/>
<View>
<FlatList
data={moves}
renderItem={({ item }) => (
<MoveListItem
move={item}
onPress={() => navigation.navigate('MoveDetail', { move: item })}
/>
<View style={styles.listItemContainer}>
<MoveListItem
move={item}
onPress={() => navigation.navigate(MOVE_DETAIL, { move: item })}
/>
<Button title="Delete"
color={styles.deleteButton.backgroundColor}
onPress={() => dispatch(deleteMove(item.id!))}/>
</View>
)}
keyExtractor={(item) => item.name}
/>
@ -48,4 +58,15 @@ const MoveListScreen = ({ navigation }: Props) => {
);
};
const styles = StyleSheet.create({
deleteButton: {
backgroundColor: '#FF6961',
},
listItemContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
});
export default MoveListScreen;

@ -2081,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"

Loading…
Cancel
Save