🥅 Handle fetch error callbacks

pull/11/head
Alexis Drai 2 years ago
parent da02d7f543
commit 1063026e2d

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

@ -20,10 +20,8 @@ const MoveListItem: React.FC<MoveListItemProps> = ({ move, onPress, style }) =>
const styles = StyleSheet.create({ const styles = StyleSheet.create({
listItem: { listItem: {
backgroundColor: '#DDD', backgroundColor: '#DDD',
padding: 20, padding: 8,
marginVertical: 8, borderRadius: 8,
marginHorizontal: 16,
borderRadius: 10,
}, },
listItemText: { listItemText: {
color: '#333', color: '#333',

@ -1,71 +1,105 @@
// redux/actions/moveAction.ts // redux/actions/moveAction.ts
import { CREATE_MOVE, DELETE_MOVE, GET_MOVES, UPDATE_MOVE } from '../constants'; import { CREATE_MOVE, DELETE, DELETE_MOVE, GET, GET_MOVES, MOVE_ERROR, POST, PUT, UPDATE_MOVE } from '../constants';
import { Move } from "../../entities/Move"; import {
import { Dispatch } from "redux"; Move
import { API_BASE_URL } from "../../config"; } from "../../entities/Move";
import { Dispatch } from "redux";
import { API_BASE_URL } from "../../config";
export const createMove = (move: Move) => { export const createMove = (move: Move) => {
const verb = POST
return async (dispatch: Dispatch) => { return async (dispatch: Dispatch) => {
try { try {
const response = await fetch(`${API_BASE_URL}/move`, { const response = await fetch(`${API_BASE_URL}/move`, {
method: 'POST', method: verb,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(move), body: JSON.stringify(move),
}); });
if (!response.ok) {
throw new Error(`Failed to ${verb}: ${response.statusText}`);
}
const data = await response.json(); const data = await response.json();
dispatch({ type: CREATE_MOVE, payload: data }); dispatch({ type: CREATE_MOVE, payload: data });
} }
catch (error) { catch (error) {
console.error(error); console.error(error);
// @ts-ignore
dispatch({ type: MOVE_ERROR, payload: error.message });
} }
} }
} }
export const getMoves = () => { export const getMoves = () => {
const verb = GET
return async (dispatch: Dispatch) => { return async (dispatch: Dispatch) => {
try { try {
const response = await fetch(`${API_BASE_URL}/move`); const response = await fetch(`${API_BASE_URL}/move`);
if (!response.ok) {
throw new Error(`Failed to ${verb}: ${response.statusText}`);
}
const data = await response.json(); const data = await response.json();
dispatch({ type: GET_MOVES, payload: data }); dispatch({ type: GET_MOVES, payload: data });
} }
catch (error) { catch (error) {
console.error(error); console.error(error);
// @ts-ignore
dispatch({ type: MOVE_ERROR, payload: error.message });
} }
} }
} }
export const updateMove = (id: string, move: Move) => { export const updateMove = (id: string, move: Move) => {
const verb = PUT
return async (dispatch: Dispatch) => { return async (dispatch: Dispatch) => {
try { try {
const response = await fetch(`${API_BASE_URL}/move/${id}`, { const response = await fetch(`${API_BASE_URL}/move/${id}`, {
method: 'PUT', method: verb,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(move), body: JSON.stringify(move),
}); });
if (!response.ok) {
throw new Error(`Failed to ${verb}: ${response.statusText}`);
}
const updatedMove = await response.json(); const updatedMove = await response.json();
dispatch({ type: UPDATE_MOVE, payload: updatedMove }); dispatch({ type: UPDATE_MOVE, payload: updatedMove });
} }
catch (error) { catch (error) {
console.error(error); console.error(error);
// @ts-ignore
dispatch({ type: MOVE_ERROR, payload: error.message });
} }
} }
} }
export const deleteMove = (id: string) => { export const deleteMove = (id: string) => {
const verb = DELETE
return async (dispatch: Dispatch) => { return async (dispatch: Dispatch) => {
try { try {
await fetch(`${API_BASE_URL}/move/${id}`, { const response = await fetch(`${API_BASE_URL}/move/${id}`, {
method: 'DELETE', method: verb,
}); });
if (!response.ok) {
throw new Error(`Failed to ${verb}: ${response.statusText}`);
}
dispatch({ type: DELETE_MOVE, payload: id }); dispatch({ type: DELETE_MOVE, payload: id });
} }
catch (error) { catch (error) {
console.error(error); console.error(error);
// @ts-ignore
dispatch({ type: MOVE_ERROR, payload: error.message });
} }
} }
} }

@ -1,6 +1,12 @@
// redux/constants.ts // 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 GET_MOVES = 'GET_MOVES';
export const CREATE_MOVE = 'CREATE_MOVE'; export const CREATE_MOVE = 'CREATE_MOVE';
export const UPDATE_MOVE = 'UPDATE_MOVE'; export const UPDATE_MOVE = 'UPDATE_MOVE';
export const DELETE_MOVE = 'DELETE_MOVE'; export const DELETE_MOVE = 'DELETE_MOVE';
export const MOVE_ERROR = 'MOVE_ERROR';

@ -1,9 +1,10 @@
// redux/reducers/moveReducer.ts // redux/reducers/moveReducer.ts
import { CREATE_MOVE, DELETE_MOVE, GET_MOVES, UPDATE_MOVE } from '../constants'; import { CREATE_MOVE, DELETE_MOVE, GET_MOVES, MOVE_ERROR, UPDATE_MOVE } from '../constants';
import { Move } from "../../entities/Move"; import { Move } from "../../entities/Move";
export type MoveState = { export type MoveState = {
moves: Move[]; moves: Move[];
error: string | null;
}; };
type MoveAction = { type MoveAction = {
@ -13,27 +14,37 @@ type MoveAction = {
const initialState: MoveState = { const initialState: MoveState = {
moves: [], moves: [],
error: null
} }
export default function moveReducer(state = initialState, action: MoveAction): MoveState { export default function moveReducer(state = initialState, action: MoveAction): MoveState {
switch (action.type) { switch (action.type) {
case GET_MOVES: case GET_MOVES:
return { return {
...state, moves: action.payload as Move[] || [] ...state, moves: action.payload as Move[] || [],
error: null,
}; };
case CREATE_MOVE: case CREATE_MOVE:
return { return {
...state, moves: [...state.moves, action.payload as Move] ...state, moves: [...state.moves, action.payload as Move],
error: null,
}; };
case UPDATE_MOVE: case UPDATE_MOVE:
return { return {
...state, ...state,
moves: state.moves.map(move => move.id === (action.payload as Move).id ? action.payload as Move : move) moves: state.moves.map(move => move.id === (action.payload as Move).id ? action.payload as Move : move),
error: null,
}; };
case DELETE_MOVE: case DELETE_MOVE:
return { return {
...state, ...state,
moves: state.moves.filter(move => move.id !== action.payload) moves: state.moves.filter(move => move.id !== action.payload),
error: null,
};
case MOVE_ERROR:
return {
...state,
error: action.payload as string
}; };
default: default:
return state; return state;

@ -4,9 +4,9 @@ import React, { useState } from 'react';
import { Button, StyleSheet, Text, TextInput } from 'react-native'; import { Button, StyleSheet, Text, TextInput } from 'react-native';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import { RootStackParamList } from "../../navigation/navigationTypes"; import { RootStackParamList } from "../../navigation/navigationTypes";
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { createMove, updateMove } from '../../redux/actions/moveActions'; import { createMove, updateMove } from '../../redux/actions/moveActions';
import { AppDispatch } from "../../redux/store"; import { AppDispatch, RootState } from "../../redux/store";
import { Move } from "../../entities/Move"; import { Move } from "../../entities/Move";
import { RouteProp } from "@react-navigation/native"; import { RouteProp } from "@react-navigation/native";
import { MOVE_FORM } from "../../navigation/constants"; import { MOVE_FORM } from "../../navigation/constants";
@ -16,6 +16,8 @@ import { MoveCategoryName } from "../../entities/MoveCategory
import { TypeName } from "../../entities/TypeName"; import { TypeName } from "../../entities/TypeName";
import MultiSelect from "react-native-multiple-select"; import MultiSelect from "react-native-multiple-select";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view"; 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 MoveFormScreenNavigationProp = StackNavigationProp<RootStackParamList, typeof MOVE_FORM>;
type MoveFormScreenRouteProp = RouteProp<RootStackParamList, typeof MOVE_FORM>; type MoveFormScreenRouteProp = RouteProp<RootStackParamList, typeof MOVE_FORM>;
@ -26,6 +28,7 @@ type Props = {
}; };
const MoveFormScreen = ({ navigation, route }: Props) => { const MoveFormScreen = ({ navigation, route }: Props) => {
const error = useSelector((state: RootState) => state.move.error);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [move, setMove] = useState<Move>(route.params?.move || { const [move, setMove] = useState<Move>(route.params?.move || {
id: null, id: null,
@ -41,7 +44,6 @@ const MoveFormScreen = ({ navigation, route }: Props) => {
schemaVersion: 2 schemaVersion: 2
}); });
const [selectedWeakAgainst, setSelectedWeakAgainst] = useState<string[]>(move.type.weakAgainst); const [selectedWeakAgainst, setSelectedWeakAgainst] = useState<string[]>(move.type.weakAgainst);
const [selectedEffectiveAgainst, setSelectedEffectiveAgainst] = useState<string[]>(move.type.effectiveAgainst); const [selectedEffectiveAgainst, setSelectedEffectiveAgainst] = useState<string[]>(move.type.effectiveAgainst);
@ -69,6 +71,11 @@ const MoveFormScreen = ({ navigation, route }: Props) => {
return ( return (
<KeyboardAwareScrollView style={styles.container}> <KeyboardAwareScrollView style={styles.container}>
<AlertModal
visible={!!error}
message={error || ''}
onClose={() => dispatch({ type: MOVE_ERROR, payload: null })}
/>
<Text style={styles.label}>Name: </Text> <Text style={styles.label}>Name: </Text>
<TextInput <TextInput
value={move.name} value={move.name}

@ -11,6 +11,8 @@ import { AppDispatch } from "../../redux/store";
import MoveListItem from "../../components/MoveListItem"; import MoveListItem from "../../components/MoveListItem";
import { MOVE_DETAIL, MOVE_FORM, MOVE_LIST } from "../../navigation/constants"; import { MOVE_DETAIL, MOVE_FORM, MOVE_LIST } from "../../navigation/constants";
import { RouteProp, useFocusEffect } from "@react-navigation/native"; 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 MoveListScreenNavigationProp = StackNavigationProp<RootStackParamList, typeof MOVE_LIST>;
type MoveListScreenRouteProp = RouteProp<RootStackParamList, typeof MOVE_LIST>; type MoveListScreenRouteProp = RouteProp<RootStackParamList, typeof MOVE_LIST>;
@ -24,6 +26,7 @@ type RootState = {
}; };
const MoveListScreen = ({ navigation }: Props) => { const MoveListScreen = ({ navigation }: Props) => {
const error = useSelector((state: RootState) => state.move.error);
const dispatch: AppDispatch = useDispatch(); const dispatch: AppDispatch = useDispatch();
const moves = useSelector((state: RootState) => state.move.moves); const moves = useSelector((state: RootState) => state.move.moves);
@ -37,29 +40,36 @@ const MoveListScreen = ({ navigation }: Props) => {
); );
return ( return (
<FlatList <>
data={moves} <AlertModal
ListHeaderComponent={ visible={!!error}
<Button title="Add Move" onPress={() => navigation.navigate(MOVE_FORM, { move: undefined })}/> message={error || ''}
} onClose={() => dispatch({ type: MOVE_ERROR, payload: null })}
renderItem={({ item }) => ( />
<View style={styles.listItemContainer}> <FlatList
<MoveListItem data={moves}
move={item} ListHeaderComponent={
style={styles.moveListItem} <Button title="Add Move" onPress={() => navigation.navigate(MOVE_FORM, { move: undefined })}/>
onPress={() => navigation.navigate(MOVE_DETAIL, { move: item })} }
/> renderItem={({ item }) => (
<Button title="X" <View style={styles.listItemContainer}>
color={styles.deleteButton.backgroundColor} <MoveListItem
onPress={() => { move={item}
if (item.id) { style={styles.moveListItem}
dispatch(deleteMove(item.id)) onPress={() => navigation.navigate(MOVE_DETAIL, { move: item })}
} />
}}/> <Button title="X"
</View> color={styles.deleteButton.backgroundColor}
)} onPress={() => {
keyExtractor={(item) => item.name} if (item.id) {
/> dispatch(deleteMove(item.id))
}
}}/>
</View>
)}
keyExtractor={(item) => item.name}
/>
</>
); );
}; };
@ -71,10 +81,12 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginHorizontal: 16,
marginTop: 8,
}, },
moveListItem: { moveListItem: {
flex: 1, flex: 1,
marginRight: 10 marginRight: 8
} }
}); });

Loading…
Cancel
Save