diff --git a/client/app/(tabs)/challenges.tsx b/client/app/(tabs)/challenges.tsx
index dafc085..647aea7 100644
--- a/client/app/(tabs)/challenges.tsx
+++ b/client/app/(tabs)/challenges.tsx
@@ -1,12 +1,14 @@
import ChallengeCard from '@/components/ChallengeCard'
import PenaltyBanner from '@/components/PenalyBanner'
+import { useDrawRandomChallengeMutation } from '@/hooks/mutations/useChallengeMutation'
+import { useAuth } from '@/hooks/useAuth'
import { useChallengeActions } from '@/hooks/useChallengeActions'
import { useChallenges } from '@/hooks/useChallenges'
import { useGame } from '@/hooks/useGame'
import { FontAwesome6 } from '@expo/vector-icons'
-import { useMemo } from 'react'
+import { useEffect, useMemo, useState } from 'react'
import { View } from 'react-native'
-import { Appbar, Banner, FAB, MD3Colors, Surface, Text } from 'react-native-paper'
+import { ActivityIndicator, Appbar, Banner, FAB, MD3Colors, Surface, Text, TouchableRipple } from 'react-native-paper'
function ChallengeScreenHeader() {
return <>
@@ -19,31 +21,49 @@ function ChallengeScreenHeader() {
}
function ChallengeScreenBody() {
+ const auth = useAuth()
const game = useGame()
const challengeActions = useChallengeActions()
const challenges = useChallenges()
const currentChallengeAction = useMemo(() => {
- if (!game.currentChallengeId)
+ if (!game.activeChallengeId)
return null
- return challengeActions.challengeActions.find((action) => action.id === game.currentChallengeId)
+ return challengeActions.challengeActions.find((action) => action.id === game.activeChallengeId)
}, [game, challengeActions])
const currentChallenge = useMemo(() => {
if (!currentChallengeAction)
return null
return challenges.challenges.find((challenge) => challenge.id === currentChallengeAction.challengeId)
}, [currentChallengeAction, challenges])
+ const [loading, setLoading] = useState(false)
+ const drawRandomChallengeMutation = useDrawRandomChallengeMutation({
+ auth,
+ onPostSuccess: () => setLoading(true),
+ })
+ useEffect(() => {
+ if (challengeActions)
+ setLoading(false)
+ }, [challengeActions])
return <>
{currentChallenge && }
{!currentChallenge && game.currentRunner && <>
Aucun défi en cours.
-
+ drawRandomChallengeMutation.mutate()}
+ variant='tertiary'
+ customSize={64} />
+ {loading && }
>}
{
- if (playerQuery.isSuccess && playerQuery.data)
+ if (playerQuery.isSuccess && playerQuery.data) {
updateMoney(playerQuery.data.money)
+ updateActiveChallengeId(playerQuery.data.activeChallengeId)
+ }
}, [playerQuery.status, playerQuery.dataUpdatedAt])
const trainsQuery = useQuery({
@@ -47,10 +56,27 @@ export default function GameProvider({ children }: { children: ReactNode }) {
refetchInterval: 5000,
})
useEffect(() => {
- if (trainsQuery.isSuccess && trainsQuery.data && trainsQuery)
+ if (trainsQuery.isSuccess && trainsQuery.data)
downloadTrains(trainsQuery.data)
}, [trainsQuery.status, trainsQuery.dataUpdatedAt])
+ const challengesQuery = useQuery({
+ queryKey: ['get-challenges', game.playerId, auth.token],
+ queryFn: () => fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/challenges/?size=10000`, {
+ headers: { "Authorization": `Bearer ${auth.token}` }}
+ ).then(resp => resp.json()),
+ enabled: isAuthValid(auth) && !!game.playerId,
+ refetchInterval: 5000,
+ })
+ useEffect(() => {
+ if (challengesQuery.isSuccess && challengesQuery.data) {
+ downloadChallenges(challengesQuery.data)
+ const dataWithPlayerActions = challengesQuery.data.data.filter(
+ (challenge: (Challenge & {action: ChallengeActionPayload | null})) => challenge.action !== null && challenge.action.playerId === game.playerId)
+ downloadChallengeActions({ data: dataWithPlayerActions })
+ }
+ }, [challengesQuery.status, challengesQuery.dataUpdatedAt])
+
return <>
{children}
>
diff --git a/client/hooks/mutations/useChallengeMutation.ts b/client/hooks/mutations/useChallengeMutation.ts
new file mode 100644
index 0000000..e4259c2
--- /dev/null
+++ b/client/hooks/mutations/useChallengeMutation.ts
@@ -0,0 +1,46 @@
+import { AuthState } from "@/utils/features/auth/authSlice"
+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 ChallengeActionProps = {
+ auth: AuthState
+ onPostSuccess?: onPostSuccessFunc
+ onError?: onErrorFunc
+}
+
+export const useDrawRandomChallengeMutation = ({ auth, onPostSuccess, onError }: ChallengeActionProps) => {
+ return useMutation({
+ mutationFn: async () => {
+ return fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/challenges/draw-random/`, {
+ method: "POST",
+ headers: {
+ "Authorization": `Bearer ${auth.token}`,
+ "Content-Type": "application/json",
+ },
+ }).then(resp => resp.json())
+ },
+ onSuccess: async (data) => {
+ if (data.statusCode) {
+ if (onError)
+ onError({ response: data })
+ return
+ }
+ console.log(data)
+ if (onPostSuccess)
+ onPostSuccess()
+ },
+ onError: async (error: Error) => {
+ if (onError)
+ onError({ error: error })
+ }
+ })
+}
diff --git a/client/hooks/useChallengeActions.ts b/client/hooks/useChallengeActions.ts
index 44da90d..7c4e821 100644
--- a/client/hooks/useChallengeActions.ts
+++ b/client/hooks/useChallengeActions.ts
@@ -1,3 +1,8 @@
-import { useAppSelector } from "./useStore"
+import { ChallengeActionsPayload, downloadChallengeActions } from "@/utils/features/challengeActions/challengeActionsSlice"
+import { useAppDispatch, useAppSelector } from "./useStore"
export const useChallengeActions = () => useAppSelector((state) => state.challengeActions)
+export const useDownloadChallengeActions = () => {
+ const dispath = useAppDispatch()
+ return (challengesData: ChallengeActionsPayload) => dispath(downloadChallengeActions(challengesData))
+}
diff --git a/client/hooks/useChallenges.ts b/client/hooks/useChallenges.ts
index c4b6605..97ec33d 100644
--- a/client/hooks/useChallenges.ts
+++ b/client/hooks/useChallenges.ts
@@ -1,3 +1,8 @@
-import { useAppSelector } from "./useStore"
+import { ChallengesPayload, downloadChallenges } from "@/utils/features/challenges/challengesSlice"
+import { useAppDispatch, useAppSelector } from "./useStore"
export const useChallenges = () => useAppSelector((state) => state.challenges)
+export const useDownloadChallenges = () => {
+ const dispath = useAppDispatch()
+ return (challengesData: ChallengesPayload) => dispath(downloadChallenges(challengesData))
+}
diff --git a/client/hooks/useGame.ts b/client/hooks/useGame.ts
index 8b2799f..2e746ea 100644
--- a/client/hooks/useGame.ts
+++ b/client/hooks/useGame.ts
@@ -1,5 +1,5 @@
import { useAppDispatch, useAppSelector } from "./useStore"
-import { GamePayload, setPlayerId, updateGameState, updateMoney } from "@/utils/features/game/gameSlice"
+import { GamePayload, setPlayerId, updateActiveChallengeId, updateGameState, updateMoney } from "@/utils/features/game/gameSlice"
export const useGame = () => useAppSelector((state) => state.game)
export const useSetPlayerId = () => {
@@ -10,6 +10,10 @@ export const useUpdateMoney = () => {
const dispatch = useAppDispatch()
return (money: number) => dispatch(updateMoney(money))
}
+export const useUpdateActiveChallengeId = () => {
+ const dispatch = useAppDispatch()
+ return (challengeActionId: number) => dispatch(updateActiveChallengeId(challengeActionId))
+}
export const useUpdateGameState = () => {
const dispatch = useAppDispatch()
return (game: GamePayload) => dispatch(updateGameState(game))
diff --git a/client/package.json b/client/package.json
index df46b4e..f0ddffa 100644
--- a/client/package.json
+++ b/client/package.json
@@ -19,6 +19,7 @@
"@expo/vector-icons": "^14.0.2",
"@maplibre/maplibre-react-native": "^10.0.0-alpha.28",
"@pchmn/expo-material3-theme": "github:pchmn/expo-material3-theme",
+ "@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0",
"@reduxjs/toolkit": "^2.4.0",
@@ -58,8 +59,7 @@
"react-native-screens": "~4.1.0",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.2",
- "react-redux": "^9.1.2",
- "@react-native-async-storage/async-storage": "1.23.1"
+ "react-redux": "^9.1.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
diff --git a/client/utils/features/challengeActions/challengeActionsSlice.ts b/client/utils/features/challengeActions/challengeActionsSlice.ts
index 35fa4c8..de5261a 100644
--- a/client/utils/features/challengeActions/challengeActionsSlice.ts
+++ b/client/utils/features/challengeActions/challengeActionsSlice.ts
@@ -1,11 +1,9 @@
-import { createSlice } from '@reduxjs/toolkit'
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import { Challenge } from '../challenges/challengesSlice'
export interface ChallengeAction {
id: number
challengeId: number
- title: string,
- description: string,
- reward: number,
success: boolean,
start: number, // date
end: number | null, // date
@@ -21,13 +19,45 @@ const initialState: ActionsState = {
challengeActions: []
}
+export interface ChallengeActionPayload {
+ id: number
+ playerId: number
+ challengeId: number
+ success: boolean,
+ start: string,
+ end: string | null,
+ penaltyStart: string | null,
+ penaltyEnd: string | null,
+}
+
+export interface ChallengeActionsPayload {
+ data: (Challenge & { action: ChallengeActionPayload })[]
+}
+
export const challengeActionsSlice = createSlice({
name: 'challengeActions',
initialState: initialState,
reducers: {
+
+ downloadChallengeActions(state, action: PayloadAction) {
+ if (state.challengeActions)
+ state.challengeActions = state.challengeActions.filter(challengeAction => action.payload.data.filter(dlChallenge => dlChallenge.action.id === challengeAction.id) === null)
+ for (const dlChallenge of action.payload.data) {
+ state.challengeActions.push({
+ id: dlChallenge.action.id,
+ challengeId: dlChallenge.id,
+ success: dlChallenge.action.success,
+ start: new Date(dlChallenge.action.start).getTime(),
+ end: dlChallenge.action.end ? new Date(dlChallenge.action.end).getTime() : null,
+ penaltyStart: dlChallenge.action.penaltyStart ? new Date(dlChallenge.action.penaltyStart).getTime() : null,
+ penaltyEnd: dlChallenge.action.penaltyEnd ? new Date(dlChallenge.action.penaltyEnd).getTime() : null,
+ })
+ }
+ state.challengeActions.sort((c1, c2) => c2.id - c1.id)
+ },
},
})
-export const { } = challengeActionsSlice.actions
+export const { downloadChallengeActions } = challengeActionsSlice.actions
export default challengeActionsSlice.reducer
diff --git a/client/utils/features/challenges/challengesSlice.ts b/client/utils/features/challenges/challengesSlice.ts
index 95a4e33..06e91a8 100644
--- a/client/utils/features/challenges/challengesSlice.ts
+++ b/client/utils/features/challenges/challengesSlice.ts
@@ -1,4 +1,6 @@
-import { createSlice } from '@reduxjs/toolkit'
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import { PaginationMeta } from '../common'
+import { ChallengeAction } from '../challengeActions/challengeActionsSlice'
export interface Challenge {
id: number
@@ -15,13 +17,31 @@ const initialState: ChallengesState = {
challenges: []
}
+export interface ChallengesPayload {
+ data: (Challenge & { action: ChallengeAction | null })[]
+ meta: PaginationMeta
+}
+
export const challengesSlice = createSlice({
name: 'challenges',
initialState: initialState,
reducers: {
+ downloadChallenges(state, action: PayloadAction) {
+ if (state.challenges)
+ state.challenges = state.challenges.filter(challenge => action.payload.data.filter(dlChallenge => dlChallenge.id === challenge.id) === null)
+ for (const dlChallenge of action.payload.data) {
+ state.challenges.push({
+ id: dlChallenge.id,
+ title: dlChallenge.title,
+ description: dlChallenge.description,
+ reward: dlChallenge.reward,
+ })
+ }
+ state.challenges.sort((c1, c2) => c2.id - c1.id)
+ },
},
})
-export const { } = challengesSlice.actions
+export const { downloadChallenges } = challengesSlice.actions
export default challengesSlice.reducer
diff --git a/client/utils/features/common.ts b/client/utils/features/common.ts
new file mode 100644
index 0000000..d1c4805
--- /dev/null
+++ b/client/utils/features/common.ts
@@ -0,0 +1,8 @@
+export interface PaginationMeta {
+ currentPage: number
+ lastPage: number
+ nextPage: number
+ prevPage: number
+ total: number
+ totalPerPage: number
+}
diff --git a/client/utils/features/game/gameSlice.ts b/client/utils/features/game/gameSlice.ts
index bd0531c..39711a6 100644
--- a/client/utils/features/game/gameSlice.ts
+++ b/client/utils/features/game/gameSlice.ts
@@ -21,7 +21,7 @@ export interface GameState {
gameStarted: boolean
money: number
currentRunner: boolean
- currentChallengeId: number | null
+ activeChallengeId: number | null
chaseFreeTime: number | null // date
penaltyStart: number | null // date
penaltyEnd: number | null //date
@@ -32,7 +32,7 @@ const initialState: GameState = {
gameStarted: false,
money: 0,
currentRunner: false,
- currentChallengeId: null,
+ activeChallengeId: null,
chaseFreeTime: null,
penaltyStart: null,
penaltyEnd: null,
@@ -48,6 +48,9 @@ export const gameSlice = createSlice({
updateMoney: (state, action: PayloadAction) => {
state.money = action.payload
},
+ updateActiveChallengeId: (state, action: PayloadAction) => {
+ state.activeChallengeId = action.payload
+ },
updateGameState: (state, action: PayloadAction) => {
const game: GamePayload = action.payload
state.gameStarted = game.started
@@ -60,6 +63,6 @@ export const gameSlice = createSlice({
},
})
-export const { setPlayerId, updateMoney, updateGameState } = gameSlice.actions
+export const { setPlayerId, updateMoney, updateActiveChallengeId, updateGameState } = gameSlice.actions
export default gameSlice.reducer
diff --git a/client/utils/features/train/trainSlice.ts b/client/utils/features/train/trainSlice.ts
index e29b11b..0a7f03d 100644
--- a/client/utils/features/train/trainSlice.ts
+++ b/client/utils/features/train/trainSlice.ts
@@ -1,4 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import { PaginationMeta } from '../common'
export interface InterrailTime {
hours: number
@@ -73,15 +74,6 @@ const initialState: TrainsState = {
trains: []
}
-export interface PaginationMeta {
- currentPage: number
- lastPage: number
- nextPage: number
- prevPage: number
- total: number
- totalPerPage: number
-}
-
export interface TrainsPayload {
data: TrainTrip[]
meta: PaginationMeta
@@ -105,8 +97,8 @@ export const trainSlice = createSlice({
arrivalTime: dlTrain.arrivalTime,
info: info,
})
- state.trains.sort((t1, t2) => t1.departureTime > t2.departureTime ? -1 : t1.departureTime == t2.arrivalTime ? 0 : 1)
}
+ state.trains.sort((t1, t2) => t1.departureTime > t2.departureTime ? -1 : t1.departureTime == t2.arrivalTime ? 0 : 1)
}
},
})