### 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
@ -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>
|
||||
<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>);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 46 KiB |
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;
|
@ -0,0 +1,2 @@
|
||||
// config.ts
|
||||
export const API_BASE_URL = 'http://localhost:8080';
|
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;
|
||||
};
|
||||
|
@ -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;
|