### 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">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="start" type="js.build_tools.npm" nameIsGenerated="true">
|
<configuration default="false" name="start" type="js.build_tools.npm" nameIsGenerated="true">
|
||||||
<package-json value="$PROJECT_DIR$/package.json" />
|
<package-json value="$PROJECT_DIR$/package.json"/>
|
||||||
<command value="run" />
|
<command value="run"/>
|
||||||
<scripts>
|
<scripts>
|
||||||
<script value="start" />
|
<script value="start"/>
|
||||||
</scripts>
|
</scripts>
|
||||||
<node-interpreter value="project" />
|
<node-interpreter value="project"/>
|
||||||
<package-manager value="C:\Users\draia\AppData\Roaming\npm\node_modules\npm" />
|
<package-manager value="yarn"/>
|
||||||
<envs />
|
<envs/>
|
||||||
<method v="2" />
|
<method v="2"/>
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
@ -1,7 +1,16 @@
|
|||||||
import React from 'react';
|
// App.tsx
|
||||||
import Navigation from "./navigation/Navigation";
|
|
||||||
|
import React from 'react';
|
||||||
|
import Navigation from "./navigation/Navigation";
|
||||||
|
import store from "./redux/store";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return <Navigation/>;
|
return (
|
||||||
// TODO Send to homescreen instead, and include a bottom bar to navigate to Moves, Pokemongs, Trainers
|
<Provider store={store}>
|
||||||
|
<SafeAreaProvider>
|
||||||
|
<Navigation/>
|
||||||
|
</SafeAreaProvider>
|
||||||
|
</Provider>);
|
||||||
}
|
}
|
||||||
|
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 {
|
export interface Move {
|
||||||
|
id: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
category: MoveCategoryName;
|
||||||
power: number;
|
power: number;
|
||||||
accuracy: number;
|
accuracy: number;
|
||||||
type: Type;
|
type: Type;
|
||||||
|
schemaVersion: number;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
export enum MoveCategoryName {
|
||||||
|
PHYSICAL = 'PHYSICAL',
|
||||||
|
SPECIAL = 'SPECIAL',
|
||||||
|
STATUS = 'STATUS',
|
||||||
|
}
|
@ -1,5 +1,9 @@
|
|||||||
|
// entities/Type.ts
|
||||||
|
|
||||||
|
import { TypeName } from "./TypeName";
|
||||||
|
|
||||||
export interface Type {
|
export interface Type {
|
||||||
name: string;
|
name: TypeName;
|
||||||
weakAgainst: string[];
|
weakAgainst: string[];
|
||||||
effectiveAgainst: string[];
|
effectiveAgainst: string[];
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
export enum TypeName {
|
||||||
|
NORMAL = 'NORMAL',
|
||||||
|
GRASS = 'GRASS',
|
||||||
|
ELECTRIC = 'ELECTRIC',
|
||||||
|
WATER = 'WATER',
|
||||||
|
FIRE = 'FIRE',
|
||||||
|
BUG = 'BUG',
|
||||||
|
GHOST = 'GHOST',
|
||||||
|
PSYCHIC = 'PSYCHIC',
|
||||||
|
STEEL = 'STEEL',
|
||||||
|
DARK = 'DARK',
|
||||||
|
FLYING = 'FLYING',
|
||||||
|
ICE = 'ICE',
|
||||||
|
POISON = 'POISON',
|
||||||
|
GROUND = 'GROUND',
|
||||||
|
ROCK = 'ROCK',
|
||||||
|
DRAGON = 'DRAGON',
|
||||||
|
FIGHTING = 'FIGHTING',
|
||||||
|
FAIRY = 'FAIRY',
|
||||||
|
}
|
@ -1,22 +1,59 @@
|
|||||||
import React from 'react';
|
// navigation/Navigation.tsx
|
||||||
import {NavigationContainer} from '@react-navigation/native';
|
|
||||||
import {createStackNavigator} from '@react-navigation/stack';
|
import React from 'react';
|
||||||
import MoveListScreen from '../screens/MoveListScreen';
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
import MoveDetailScreen from '../screens/MoveDetailScreen';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import {RootStackParamList} from "./navigationTypes";
|
import MoveListScreen from '../screens/moves/MoveListScreen';
|
||||||
|
import MoveDetailScreen from '../screens/moves/MoveDetailScreen';
|
||||||
|
import HomeScreen from '../screens/HomeScreen';
|
||||||
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
|
import { RootStackParamList, RootTabParamList } from "./navigationTypes";
|
||||||
|
import { Image, StyleSheet } from 'react-native';
|
||||||
|
import MoveFormScreen from "../screens/moves/MoveFormScreen";
|
||||||
|
|
||||||
const Stack = createStackNavigator<RootStackParamList>();
|
const Stack = createStackNavigator<RootStackParamList>();
|
||||||
|
const Tab = createBottomTabNavigator<RootTabParamList>();
|
||||||
|
|
||||||
|
const MoveStack = () => {
|
||||||
|
return (
|
||||||
|
<Stack.Navigator initialRouteName="MoveList">
|
||||||
|
<Stack.Screen name="MoveList" component={MoveListScreen} options={{ title: 'Moves' }}/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="MoveDetail"
|
||||||
|
component={MoveDetailScreen}
|
||||||
|
options={({ route }) => ({ title: route.params.move.name })}
|
||||||
|
/>
|
||||||
|
<Stack.Screen name="MoveForm" component={MoveFormScreen}/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Navigation = () => {
|
const Navigation = () => {
|
||||||
// TODO replace 'Move Detail' by the move name itself
|
|
||||||
return (
|
return (
|
||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
<Stack.Navigator initialRouteName="MoveList">
|
<Tab.Navigator initialRouteName="Home"
|
||||||
<Stack.Screen name="MoveList" component={MoveListScreen} options={{title: 'Moves'}}/>
|
screenOptions={{ headerShown: false }}>
|
||||||
<Stack.Screen name="MoveDetail" component={MoveDetailScreen} options={{title: 'Move Detail'}}/>
|
<Tab.Screen name="Home" component={HomeScreen} options={{
|
||||||
</Stack.Navigator>
|
title: 'Home',
|
||||||
|
tabBarIcon: () => <Image source={require('../assets/home.png')}
|
||||||
|
style={styles.icon}/>
|
||||||
|
}}/>
|
||||||
|
<Tab.Screen name="Moves" component={MoveStack} options={{
|
||||||
|
title: 'Moves',
|
||||||
|
tabBarIcon: () => <Image source={require('../assets/moves.png')}
|
||||||
|
style={styles.icon}/>
|
||||||
|
}}/>
|
||||||
|
</Tab.Navigator>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
icon: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default Navigation;
|
export default Navigation;
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
// navigation/constants.ts
|
||||||
|
|
||||||
|
export const MOVE_DETAIL = 'MoveDetail';
|
||||||
|
export const MOVE_FORM = 'MoveForm';
|
||||||
|
export const MOVE_LIST = 'MoveList';
|
@ -1,6 +1,14 @@
|
|||||||
import {Move} from "../entities/Move";
|
// navigation/navigationTypes.ts
|
||||||
|
|
||||||
|
import { Move } from "../entities/Move";
|
||||||
|
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
MoveList: undefined;
|
MoveList: undefined;
|
||||||
MoveDetail: { move: Move };
|
MoveDetail: { move: Move };
|
||||||
|
MoveForm: { move?: Move };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RootTabParamList = {
|
||||||
|
Home: undefined;
|
||||||
|
Moves: undefined;
|
||||||
};
|
};
|
||||||
|
@ -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;
|