Compare commits
	
		
			2 Commits
		
	
	
		
			7aa9dde5a9
			...
			61b0cd51ae
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 61b0cd51ae | |||
| c28097d443 | 
| @@ -1,18 +1,100 @@ | ||||
| import { useGameRepairMutation, useGameResetMutation, useGameStartMutation, useGameStopMutation, useGameSwitchPlayerMutation } from '@/hooks/mutations/useGameMutation' | ||||
| import { useAuth } from '@/hooks/useAuth' | ||||
| import { useGame, useUpdateGameState } from '@/hooks/useGame' | ||||
| import { useRouter } from 'expo-router' | ||||
| import { FAB, List, Surface } from 'react-native-paper' | ||||
| import { useState } from 'react' | ||||
| import { Button, Dialog, FAB, List, Portal, Surface, Text } from 'react-native-paper' | ||||
|  | ||||
| export default function HistoryScreen() { | ||||
|   const router = useRouter() | ||||
|   const auth = useAuth() | ||||
|   const game = useGame() | ||||
|   const updateGameState = useUpdateGameState() | ||||
|  | ||||
|   const gameStartMutation = useGameStartMutation({ | ||||
|     auth, | ||||
|     updateGameState, | ||||
|   }) | ||||
|   const gameStopMutation = useGameStopMutation({ | ||||
|     auth, | ||||
|     updateGameState, | ||||
|   }) | ||||
|   const gameSwitchMutation = useGameSwitchPlayerMutation({ | ||||
|     auth, | ||||
|     updateGameState, | ||||
|   }) | ||||
|   const gameRepairMutation = useGameRepairMutation({ | ||||
|     auth, | ||||
|     updateGameState, | ||||
|   }) | ||||
|   const gameResetMutation = useGameResetMutation({ | ||||
|     auth, | ||||
|     updateGameState, | ||||
|   }) | ||||
|  | ||||
|   const [resetConfirmVisible, setResetConfirmVisible] = useState(false) | ||||
|  | ||||
|   return ( | ||||
|     <Surface | ||||
|       style={{ flex: 1 }}> | ||||
|       <List.Item | ||||
|           title="Connexion au serveur" | ||||
|           description={auth.loggedIn ? "Vous êtes déjà connecté⋅e" : "Vous n'êtes pas connecté⋅e"} | ||||
|           right={() => <FAB icon="login" size="small" onPress={() => router.navigate('/login')} />} | ||||
|           onPress={() => router.navigate('/login')} /> | ||||
|       <List.Section title={"Paramètres"}> | ||||
|         <List.Item | ||||
|             key={"login"} | ||||
|             title="Connexion au serveur" | ||||
|             description={auth.loggedIn ? "Vous êtes déjà connecté⋅e" : "Vous n'êtes pas connecté⋅e"} | ||||
|             right={() => <FAB icon="login" size="small" onPress={() => router.navigate('/login')} />} | ||||
|             onPress={() => router.navigate('/login')} /> | ||||
|       </List.Section> | ||||
|       <List.Section title={"Gestion du jeu"}> | ||||
|         <List.Item | ||||
|             key={"start"} | ||||
|             title="Démarrer le jeu" | ||||
|             disabled={game.gameStarted} | ||||
|             right={() => <FAB icon="play" size="small" disabled={game.gameStarted} />} | ||||
|             onPress={() => gameStartMutation.mutate()} /> | ||||
|         <List.Item | ||||
|             key={"stop"} | ||||
|             title="Arrêter le jeu" | ||||
|             disabled={!game.gameStarted} | ||||
|             right={() => <FAB icon="stop" size="small" disabled={!game.gameStarted} />} | ||||
|             onPress={() => gameStopMutation.mutate()} /> | ||||
|         <List.Item | ||||
|             key={"switch"} | ||||
|             title="Changer de joueur⋅se en course" | ||||
|             description="À utiliser après une capture" | ||||
|             disabled={!game.gameStarted} | ||||
|             right={() => <FAB icon="exit-run" size="small" disabled={!game.gameStarted} />} | ||||
|             onPress={() => gameSwitchMutation.mutate()} /> | ||||
|       </List.Section> | ||||
|       <List.Section title={"Avancé"}> | ||||
|         <List.Item | ||||
|             key={"repair"} | ||||
|             title="Réparer" | ||||
|             description="Permet de réparer les soldes des joueur⋅ses à partir des défis réalisés et des trains emprunter. À manipuler avec précaution." | ||||
|             right={() => <FAB icon="reload-alert" size="small" variant={'tertiary'} />} | ||||
|             onPress={() => gameRepairMutation.mutate()} /> | ||||
|         <List.Item | ||||
|             key={"reset"} | ||||
|             title="Réinitialiser les données de jeu" | ||||
|             description="Permet de détruire toutes les données. À manipuler avec précaution." | ||||
|             right={() => <FAB icon="reload-alert" size="small" variant={'tertiary'} />} | ||||
|             onPress={() => setResetConfirmVisible(true)} /> | ||||
|       </List.Section> | ||||
|       <Portal> | ||||
|         <Dialog key="confirmReset" visible={resetConfirmVisible} onDismiss={() => setResetConfirmVisible(false)}> | ||||
|           <Dialog.Title>Confirmer</Dialog.Title> | ||||
|           <Dialog.Content> | ||||
|             <Text variant="bodyMedium"> | ||||
|               Cette action va réinitialiser TOUTES les données de jeu : l'historique des positions, les défis réalisés et les trains empruntés. | ||||
|               Êtes-vous réellement sûr⋅e de vouloir tout supprimer ? | ||||
|             </Text> | ||||
|           </Dialog.Content> | ||||
|           <Dialog.Actions> | ||||
|             <Button onPress={() => setResetConfirmVisible(false)}>Annuler</Button> | ||||
|             <Button onPress={() => { setResetConfirmVisible(false); gameResetMutation.mutate() }}>Confirmer</Button> | ||||
|           </Dialog.Actions> | ||||
|         </Dialog> | ||||
|       </Portal> | ||||
|     </Surface> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import store from '@/utils/store' | ||||
| import { useStartBackgroundFetchServiceEffect } from '@/utils/background' | ||||
| import LoginProvider from '@/components/LoginProvider' | ||||
| import GeolocationProvider from '@/components/GeolocationProvider' | ||||
| import GameProvider from '@/components/GameProvider' | ||||
|  | ||||
| const queryClient = new QueryClient({ | ||||
|   defaultOptions: { | ||||
| @@ -45,16 +46,18 @@ export default function RootLayout() { | ||||
|           onSuccess={() => queryClient.resumePausedMutations().then(() => queryClient.invalidateQueries())}> | ||||
|         <LoginProvider loginRedirect={'/login'}> | ||||
|           <GeolocationProvider> | ||||
|             <PaperProvider theme={colorScheme === 'dark' ? MD3DarkTheme : MD3LightTheme}> | ||||
|               <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> | ||||
|                 <Stack> | ||||
|                   <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> | ||||
|                   <Stack.Screen name="login" options={{ headerShown: false }} /> | ||||
|                   <Stack.Screen name="+not-found" /> | ||||
|                 </Stack> | ||||
|                 <StatusBar style="auto" /> | ||||
|               </ThemeProvider> | ||||
|             </PaperProvider> | ||||
|             <GameProvider> | ||||
|               <PaperProvider theme={colorScheme === 'dark' ? MD3DarkTheme : MD3LightTheme}> | ||||
|                 <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> | ||||
|                   <Stack> | ||||
|                     <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> | ||||
|                     <Stack.Screen name="login" options={{ headerShown: false }} /> | ||||
|                     <Stack.Screen name="+not-found" /> | ||||
|                   </Stack> | ||||
|                   <StatusBar style="auto" /> | ||||
|                 </ThemeProvider> | ||||
|               </PaperProvider> | ||||
|             </GameProvider> | ||||
|           </GeolocationProvider> | ||||
|         </LoginProvider> | ||||
|       </PersistQueryClientProvider> | ||||
|   | ||||
							
								
								
									
										26
									
								
								client/components/GameProvider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								client/components/GameProvider.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import { useAuth } from '@/hooks/useAuth' | ||||
| import { useUpdateGameState } from '@/hooks/useGame' | ||||
| import { useQuery } from '@tanstack/react-query' | ||||
| import { ReactNode, useEffect } from 'react' | ||||
|  | ||||
| export default function GameProvider({ children }: { children: ReactNode }) { | ||||
|   const auth = useAuth() | ||||
|   const updateGameState = useUpdateGameState() | ||||
|   const gameQuery = useQuery({ | ||||
|     queryKey: ['update-game'], | ||||
|     queryFn: () => fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/game/`, { | ||||
|       headers: { "Authorization": `Bearer ${auth.token}` }} | ||||
|     ).then(resp => resp.json()), | ||||
|     enabled: auth.loggedIn, | ||||
|     refetchInterval: 5000, | ||||
|   }) | ||||
|   const game = gameQuery.data | ||||
|   useEffect(() => { | ||||
|     if (game) | ||||
|       updateGameState(game) | ||||
|   }, [game]) | ||||
|  | ||||
|   return <> | ||||
|     {children} | ||||
|   </> | ||||
| } | ||||
							
								
								
									
										159
									
								
								client/hooks/mutations/useGameMutation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								client/hooks/mutations/useGameMutation.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| import { AuthState } from "@/utils/features/auth/authSlice" | ||||
| import { GamePayload, GameState } from "@/utils/features/game/gameSlice" | ||||
| import { useMutation } from "@tanstack/react-query" | ||||
|  | ||||
| type ErrorResponse = { | ||||
|   error: string | ||||
|   message: string | ||||
|   statusCode: number | ||||
| } | ||||
|  | ||||
| type onPostSuccessFunc = () => void | ||||
| type ErrorFuncProps = { response?: ErrorResponse, error?: Error } | ||||
| type onErrorFunc = (props: ErrorFuncProps) => void | ||||
|  | ||||
| type GameProps = { | ||||
|   updateGameState: (payload: GamePayload) => { payload: GamePayload, type: "game/updateGameState" } | ||||
|   auth: AuthState | ||||
|   onPostSuccess?: onPostSuccessFunc | ||||
|   onError?: onErrorFunc | ||||
| } | ||||
|  | ||||
| export const useGameStartMutation = ({ auth, updateGameState, onPostSuccess, onError }: GameProps) => { | ||||
|   return useMutation({ | ||||
|     mutationFn: async () => { | ||||
|       return fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/game/start/`, { | ||||
|         method: "POST", | ||||
|         headers: { | ||||
|           "Authorization": `Bearer ${auth.token}`, | ||||
|           "Content-Type": "application/json", | ||||
|         }, | ||||
|       }).then(resp => resp.json()) | ||||
|     }, | ||||
|     onSuccess: async (data) => { | ||||
|       if (data.error) { | ||||
|         if (onError) | ||||
|           onError({ response: data }) | ||||
|         return | ||||
|       } | ||||
|       updateGameState(data) | ||||
|       if (onPostSuccess) | ||||
|         onPostSuccess() | ||||
|     }, | ||||
|     onError: async (error: Error) => { | ||||
|       if (onError) | ||||
|         onError({ error: error }) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|  | ||||
| export const useGameStopMutation = ({ auth, updateGameState, onPostSuccess, onError }: GameProps) => { | ||||
|   return useMutation({ | ||||
|     mutationFn: async () => { | ||||
|       return fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/game/stop/`, { | ||||
|         method: "POST", | ||||
|         headers: { | ||||
|           "Authorization": `Bearer ${auth.token}`, | ||||
|           "Content-Type": "application/json", | ||||
|         }, | ||||
|       }).then(resp => resp.json()) | ||||
|     }, | ||||
|     onSuccess: async (data) => { | ||||
|       if (data.error) { | ||||
|         if (onError) | ||||
|           onError({ response: data }) | ||||
|         return | ||||
|       } | ||||
|       updateGameState(data) | ||||
|       if (onPostSuccess) | ||||
|         onPostSuccess() | ||||
|     }, | ||||
|     onError: async (error: Error) => { | ||||
|       if (onError) | ||||
|         onError({ error: error }) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|  | ||||
| export const useGameSwitchPlayerMutation = ({ auth, updateGameState, onPostSuccess, onError }: GameProps) => { | ||||
|   return useMutation({ | ||||
|     mutationFn: async () => { | ||||
|       return fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/game/switch-running-player/`, { | ||||
|         method: "POST", | ||||
|         headers: { | ||||
|           "Authorization": `Bearer ${auth.token}`, | ||||
|           "Content-Type": "application/json", | ||||
|         }, | ||||
|       }).then(resp => resp.json()) | ||||
|     }, | ||||
|     onSuccess: async (data) => { | ||||
|       if (data.error) { | ||||
|         if (onError) | ||||
|           onError({ response: data }) | ||||
|         return | ||||
|       } | ||||
|       updateGameState(data) | ||||
|       if (onPostSuccess) | ||||
|         onPostSuccess() | ||||
|     }, | ||||
|     onError: async (error: Error) => { | ||||
|       if (onError) | ||||
|         onError({ error: error }) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|  | ||||
| export const useGameRepairMutation = ({ auth, onPostSuccess, onError }: GameProps) => { | ||||
|   return useMutation({ | ||||
|     mutationFn: async () => { | ||||
|       return fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/game/repair/`, { | ||||
|         method: "PUT", | ||||
|         headers: { | ||||
|           "Authorization": `Bearer ${auth.token}`, | ||||
|           "Content-Type": "application/json", | ||||
|         }, | ||||
|       }).then(resp => resp.json()) | ||||
|     }, | ||||
|     onSuccess: async (data) => { | ||||
|       if (data.error) { | ||||
|         if (onError) | ||||
|           onError({ response: data }) | ||||
|         return | ||||
|       } | ||||
|       if (onPostSuccess) | ||||
|         onPostSuccess() | ||||
|     }, | ||||
|     onError: async (error: Error) => { | ||||
|       if (onError) | ||||
|         onError({ error: error }) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|  | ||||
| export const useGameResetMutation = ({ auth, updateGameState, onPostSuccess, onError }: GameProps) => { | ||||
|   return useMutation({ | ||||
|     mutationFn: async () => { | ||||
|       return fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/game/reset/`, { | ||||
|         method: "DELETE", | ||||
|         headers: { | ||||
|           "Authorization": `Bearer ${auth.token}`, | ||||
|           "Content-Type": "application/json", | ||||
|         }, | ||||
|       }).then(resp => resp.json()) | ||||
|     }, | ||||
|     onSuccess: async (data) => { | ||||
|       if (data.error) { | ||||
|         if (onError) | ||||
|           onError({ response: data }) | ||||
|         return | ||||
|       } | ||||
|       updateGameState(data) | ||||
|       if (onPostSuccess) | ||||
|         onPostSuccess() | ||||
|     }, | ||||
|     onError: async (error: Error) => { | ||||
|       if (onError) | ||||
|         onError({ error: error }) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| @@ -8,11 +8,6 @@ type ErrorResponse = { | ||||
|   statusCode: number | ||||
| } | ||||
|  | ||||
| type LoginForm = { | ||||
|   name: string | ||||
|   password: string | ||||
| } | ||||
|  | ||||
| type onPostSuccessFunc = (data: any, variables: LocationObject, context: unknown) => void | ||||
| type ErrorFuncProps = { response?: ErrorResponse, error?: Error } | ||||
| type onErrorFunc = (props: ErrorFuncProps) => void | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { useAppDispatch, useAppSelector } from "./useStore" | ||||
| import { setPlayerId, updateMoney } from "@/utils/features/game/gameSlice" | ||||
| import { GamePayload, setPlayerId, updateGameState, updateMoney } from "@/utils/features/game/gameSlice" | ||||
|  | ||||
| export const useGame = () => useAppSelector((state) => state.game) | ||||
| export const useSetPlayerId = () => { | ||||
| @@ -10,3 +10,7 @@ export const useUpdateMoney = () => { | ||||
|     const dispatch = useAppDispatch() | ||||
|     return (money: number) => dispatch(updateMoney(money)) | ||||
| } | ||||
| export const useUpdateGameState = () => { | ||||
|     const dispatch = useAppDispatch() | ||||
|     return (game: GamePayload) => dispatch(updateGameState(game)) | ||||
| } | ||||
|   | ||||
| @@ -7,10 +7,10 @@ export interface ChallengeAction { | ||||
|   description: string, | ||||
|   reward: number, | ||||
|   success: boolean, | ||||
|   start: Date, | ||||
|   end: Date | null, | ||||
|   penaltyStart: Date | null, | ||||
|   penaltyEnd: Date | null, | ||||
|   start: number,  // date | ||||
|   end: number | null,  // date | ||||
|   penaltyStart: number | null,  // date | ||||
|   penaltyEnd: number | null,  // date | ||||
| } | ||||
|  | ||||
| export interface ActionsState { | ||||
|   | ||||
| @@ -1,13 +1,28 @@ | ||||
| import { createSlice, PayloadAction } from '@reduxjs/toolkit' | ||||
|  | ||||
| export interface RunPayload { | ||||
|   id: number | ||||
|   gameId: number | ||||
|   runnerId: number | ||||
|   start: string | ||||
|   end: string | null | ||||
| } | ||||
|  | ||||
| export interface GamePayload { | ||||
|   id: number | ||||
|   started: boolean | ||||
|   currentRunId: number | null | ||||
|   currentRun: RunPayload | null | ||||
| } | ||||
|  | ||||
| export interface GameState { | ||||
|   playerId: number | null | ||||
|   gameStarted: boolean | ||||
|   money: number | ||||
|   currentRunner: boolean | ||||
|   chaseFreeTime: Date | null | ||||
|   penaltyStart: Date | null | ||||
|   penaltyEnd: Date | null | ||||
|   chaseFreeTime: number | null  // date | ||||
|   penaltyStart: number | null // date | ||||
|   penaltyEnd: number | null  // date | ||||
| } | ||||
|  | ||||
| const initialState: GameState = { | ||||
| @@ -30,9 +45,18 @@ export const gameSlice = createSlice({ | ||||
|     updateMoney: (state, action: PayloadAction<number>) => { | ||||
|       state.money = action.payload | ||||
|     }, | ||||
|     updateGameState: (state, action: PayloadAction<GamePayload>) => { | ||||
|       const game: GamePayload = action.payload | ||||
|       state.gameStarted = game.started | ||||
|       state.currentRunner = state.playerId === game.currentRun?.runnerId | ||||
|       if (state.currentRunner) | ||||
|         state.chaseFreeTime = null | ||||
|       else if (game.currentRun) | ||||
|         state.chaseFreeTime = new Date(game.currentRun?.start).getTime() + 45 * 60 * 1000 | ||||
|     } | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| export const { setPlayerId, updateMoney } = gameSlice.actions | ||||
| export const { setPlayerId, updateMoney, updateGameState } = gameSlice.actions | ||||
|  | ||||
| export default gameSlice.reducer | ||||
|   | ||||
| @@ -83,8 +83,8 @@ export interface TrainTrip { | ||||
|   distance: number, | ||||
|   from: string, | ||||
|   to: string, | ||||
|   departureTime: Date, | ||||
|   arrivalTime: Date, | ||||
|   departureTime: number, | ||||
|   arrivalTime: number, | ||||
| } | ||||
|  | ||||
| export interface TrainsState { | ||||
|   | ||||
| @@ -35,6 +35,7 @@ export async function startGeolocationService(): Promise<void | (() => void)> { | ||||
|       accuracy: Location.Accuracy.BestForNavigation, | ||||
|       activityType: Location.ActivityType.OtherNavigation, | ||||
|       deferredUpdatesInterval: 100, | ||||
|       timeInterval: 100, | ||||
|       foregroundService: { | ||||
|         killServiceOnDestroy: false, | ||||
|         notificationBody: "Géolocalisation activée pour « Traintrape-moi »", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user