Compare commits

..

No commits in common. "fd4b0e8cd1e580d2f4cf2b090181435c16f200d0" and "50382079c0d250ebc8d2198bb98f0fd0e7e422b5" have entirely different histories.

7 changed files with 14 additions and 346 deletions

View File

@ -25,7 +25,7 @@ export default function TabLayout() {
<Tabs.Screen <Tabs.Screen
name="challenges" name="challenges"
options={{ options={{
title: 'Défi en cours', title: 'Défis',
headerShown: false, headerShown: false,
tabBarIcon: ({ color }) => <FontAwesome6 name="coins" size={24} color={color} />, tabBarIcon: ({ color }) => <FontAwesome6 name="coins" size={24} color={color} />,
}} }}

View File

@ -7,17 +7,15 @@ import { useChallenges } from '@/hooks/useChallenges'
import { useGame } from '@/hooks/useGame' import { useGame } from '@/hooks/useGame'
import { FontAwesome6 } from '@expo/vector-icons' import { FontAwesome6 } from '@expo/vector-icons'
import { useQueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'expo-router'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { View } from 'react-native' import { View } from 'react-native'
import { ActivityIndicator, Appbar, Banner, FAB, MD3Colors, Snackbar, Surface, Text, TouchableRipple } from 'react-native-paper' import { ActivityIndicator, Appbar, Banner, FAB, MD3Colors, Snackbar, Surface, Text, TouchableRipple } from 'react-native-paper'
function ChallengeScreenHeader() { function ChallengeScreenHeader() {
const router = useRouter()
return <> return <>
<Appbar.Header> <Appbar.Header>
<Appbar.Content title={"Défi en cours"} /> <Appbar.Content title={"Défis"} />
<Appbar.Action icon='format-list-bulleted' onPress={() => router.navigate('/challenges-list')} /> <Appbar.Action icon='format-list-bulleted' />
</Appbar.Header> </Appbar.Header>
<PenaltyBanner /> <PenaltyBanner />
</> </>
@ -87,8 +85,7 @@ function ChallengeScreenBody() {
<ChallengeCard <ChallengeCard
challenge={currentChallenge} challenge={currentChallenge}
onSuccess={() => { setLoading(true); endChallenge.mutate({ success: true }) }} onSuccess={() => { setLoading(true); endChallenge.mutate({ success: true }) }}
onFail={() => endChallenge.mutate({ success: false })} onFail={() => endChallenge.mutate({ success: false })} />}
style={{ flex: 1, margin: 20 }} />}
{!loading && !game.penaltyEnd && !currentChallenge && game.currentRunner && <> {!loading && !game.penaltyEnd && !currentChallenge && game.currentRunner && <>
<Banner <Banner
visible={!currentChallenge && game.currentRunner && !loading} visible={!currentChallenge && game.currentRunner && !loading}

View File

@ -24,16 +24,14 @@ export default function MapScreen() {
visible={game.gameStarted || game.money > 0} visible={game.gameStarted || game.money > 0}
icon={(props) => <FontAwesome6 {...props} name='coins' size={20} />} icon={(props) => <FontAwesome6 {...props} name='coins' size={20} />}
color='black' color='black'
label={`${game.money}`} label={`${game.money}`} />
onPress={() => {}} />
<FAB <FAB
style={styles.statusBadge} style={styles.statusBadge}
visible={game.gameStarted || game.money > 0} visible={game.gameStarted || game.money > 0}
size='small' size='small'
color='black' color='black'
icon={game.currentRunner ? 'run-fast' : () => <FontAwesome6 name='cat' size={20} />} icon={game.currentRunner ? 'run-fast' : () => <FontAwesome6 name='cat' size={20} />}
label={game.currentRunner ? "Coureuse" : "Poursuiveuse"} label={game.currentRunner ? "Coureuse" : "Poursuiveuse"} />
onPress={() => {}} />
</View> </View>
<FreeChaseBanner /> <FreeChaseBanner />
</Surface> </Surface>

View File

@ -53,7 +53,6 @@ export default function RootLayout() {
<Stack> <Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="login" options={{ headerShown: false }} /> <Stack.Screen name="login" options={{ headerShown: false }} />
<Stack.Screen name="challenges-list" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" /> <Stack.Screen name="+not-found" />
</Stack> </Stack>
<StatusBar style="auto" /> <StatusBar style="auto" />

View File

@ -1,227 +0,0 @@
import ChallengeCard from "@/components/ChallengeCard"
import { useAddChallengeMutation, useDeleteChallengeMutation, useEditChallengeMutation } from "@/hooks/mutations/useChallengeMutation"
import { useAuth } from "@/hooks/useAuth"
import { useChallenges } from "@/hooks/useChallenges"
import { Challenge } from "@/utils/features/challenges/challengesSlice"
import { FontAwesome6 } from "@expo/vector-icons"
import { useQueryClient } from "@tanstack/react-query"
import { useRouter } from "expo-router"
import { useState } from "react"
import { FlatList, StyleSheet } from "react-native"
import { Appbar, Button, Dialog, Divider, FAB, List, MD3Colors, Modal, Portal, Snackbar, Surface, Text, TextInput } from "react-native-paper"
export default function ChallengesList() {
const router = useRouter()
const queryClient = useQueryClient()
const auth = useAuth()
const challenges = useChallenges()
const [editChallengeVisible, setEditChallengeVisible] = useState(false)
const [editChallengeTitle, setEditChallengeTitle] = useState("")
const [editChallengeDescription, setEditChallengeDescription] = useState("")
const [editChallengeReward, setEditChallengeReward] = useState(0)
const [editChallengeId, setEditChallengeId] = useState<number |null>(null)
const [displayedChallenge, setDisplayedChallenge] = useState<Challenge | null>(null)
const [confirmDeletedVisible, setConfirmDeleteVisible] = useState(false)
const [successSnackbarVisible, setSuccessSnackbarVisible] = useState(false)
const [successMessage, setSuccessMessage] = useState("")
const [errorVisible, setErrorVisible] = useState(false)
const [error, setError] = useState([200, ""])
const addChallengeMutation = useAddChallengeMutation({
auth,
onPostSuccess: () => {
setSuccessMessage("Le défi a bien été ajouté !")
setSuccessSnackbarVisible(true)
queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'get-challenges' })
},
onError: ({ response, error }) => {
setErrorVisible(true)
if (response)
setError([response.statusCode, response.message])
else if (error)
setError([400, error.message])
},
})
const editChallengeMutation = useEditChallengeMutation({
auth,
onPostSuccess: () => {
setSuccessMessage("Le défi a bien été modifié !")
setSuccessSnackbarVisible(true)
setEditChallengeVisible(false)
setDisplayedChallenge(null)
queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'get-challenges' })
},
onError: ({ response, error }) => {
setErrorVisible(true)
if (response)
setError([response.statusCode, response.message])
else if (error)
setError([400, error.message])
},
})
const deleteChallengeMutation = useDeleteChallengeMutation({
auth,
onPostSuccess: () => {
setSuccessMessage("Le défi a bien été supprimé !")
setSuccessSnackbarVisible(true)
setEditChallengeVisible(false)
queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'get-challenges' })
},
onError: ({ response, error }) => {
setErrorVisible(true)
if (response)
setError([response.statusCode, response.message])
else if (error)
setError([400, error.message])
},
})
function sendEditChallenge() {
if (editChallengeId) {
editChallengeMutation.mutate({
id: editChallengeId,
title: editChallengeTitle,
description: editChallengeDescription,
reward: editChallengeReward,
})
}
else {
addChallengeMutation.mutate({
title: editChallengeTitle,
description: editChallengeDescription,
reward: editChallengeReward,
})
}
}
function sendDeleteChallenge() {
displayedChallenge && deleteChallengeMutation.mutate(displayedChallenge)
}
return (
<Surface style={{ flex: 1 }}>
<Appbar.Header>
{router.canGoBack() ? <Appbar.BackAction onPress={() => router.back()} /> : undefined}
<Appbar.Content title={"Liste des défis"} />
</Appbar.Header>
<FlatList
data={challenges}
keyExtractor={(challenge) => `challenge-list-item-${challenge.id}`}
ItemSeparatorComponent={() => <Divider />}
renderItem={(item) => <ChallengeListItem challenge={item.item} onPress={() => setDisplayedChallenge(item.item)} />} />
<Snackbar
key='success-snackbar'
visible={successSnackbarVisible}
icon={'close'}
onDismiss={() => setSuccessSnackbarVisible(false)}
onIconPress={() => setSuccessSnackbarVisible(false)}>
<Text variant='bodyMedium' style={{ color: MD3Colors.secondary0 }}>
{successMessage}
</Text>
</Snackbar>
<Snackbar
key='error-snackbar'
visible={errorVisible}
icon={'close'}
onDismiss={() => setErrorVisible(false)}
onIconPress={() => setErrorVisible(false)}>
<Text variant='bodyMedium' style={{ color: MD3Colors.secondary0 }}>
Erreur {error[0]} : {error[1]}
</Text>
</Snackbar>
<FAB
icon='plus'
style={styles.addButton}
onPress={() => {
if (editChallengeId) {
setEditChallengeTitle("")
setEditChallengeDescription("")
setEditChallengeReward(0)
setEditChallengeId(null)
}
setEditChallengeVisible(true)
}} />
<Portal>
<Modal
visible={displayedChallenge !== null}
onDismiss={() => setDisplayedChallenge(null)}
contentContainerStyle={{ flex: 1, marginHorizontal: 20, marginVertical: 100 }}>
<ChallengeCard
challenge={displayedChallenge}
onEdit={() => {
setEditChallengeTitle(displayedChallenge?.title ?? "")
setEditChallengeDescription(displayedChallenge?.description ?? "")
setEditChallengeReward(displayedChallenge?.reward ?? 0)
setEditChallengeId(displayedChallenge?.id ?? null)
setEditChallengeVisible(true)
}}
onDelete={() => setConfirmDeleteVisible(true)}
style={{ flexGrow: 1 }} />
</Modal>
<Dialog visible={editChallengeVisible} onDismiss={() => setEditChallengeVisible(false)}>
<Dialog.Title>{editChallengeId ? "Modification d'un défi" : "Ajout d'un défi"}</Dialog.Title>
<Dialog.Content>
<TextInput
label="Titre"
defaultValue={editChallengeTitle}
onChangeText={setEditChallengeTitle}
error={!editChallengeTitle} />
<TextInput
label="Description"
defaultValue={editChallengeDescription}
multiline={true}
onChangeText={setEditChallengeDescription}
error={!editChallengeDescription} />
<TextInput
label="Récompense"
defaultValue={editChallengeReward ? editChallengeReward.toString() : ""}
inputMode='numeric'
onChangeText={(text) => setEditChallengeReward(+text)}
error={!editChallengeReward}
onEndEditing={sendEditChallenge} />
</Dialog.Content>
<Dialog.Actions>
<Button onPress={() => setEditChallengeVisible(false)}>Annuler</Button>
<Button
onPress={sendEditChallenge}
disabled={!editChallengeTitle || !editChallengeDescription || !editChallengeReward}>
{editChallengeId ? "Modifier" : "Ajouter"}
</Button>
</Dialog.Actions>
</Dialog>
<Dialog visible={confirmDeletedVisible} onDismiss={() => setConfirmDeleteVisible(false)}>
<Dialog.Title>Êtes-vous sûre ?</Dialog.Title>
<Dialog.Content>
<Text variant='bodyMedium'>
Voulez-vous vraiment supprimer le défi « {displayedChallenge?.title} » ? Cette opération est irréversible !
</Text>
</Dialog.Content>
<Dialog.Actions>
<Button onPress={() => setConfirmDeleteVisible(false)}>Annuler</Button>
<Button onPress={sendDeleteChallenge}>Confirmer</Button>
</Dialog.Actions>
</Dialog>
</Portal>
</Surface>
)
}
function ChallengeListItem({ challenge, onPress }: { challenge: Challenge, onPress?: () => void }) {
const description = <Text>Récompense : {challenge.reward} <FontAwesome6 name='coins' /></Text>
return (
<List.Item
title={challenge.title}
description={description}
onPress={onPress} />
)
}
const styles = StyleSheet.create({
addButton: {
position: 'absolute',
right: 25,
bottom: 25,
}
})

View File

@ -1,30 +1,29 @@
import { Challenge } from "@/utils/features/challenges/challengesSlice" import { Challenge } from "@/utils/features/challenges/challengesSlice"
import { FontAwesome6 } from "@expo/vector-icons" import { FontAwesome6 } from "@expo/vector-icons"
import { View, ViewStyle } from "react-native" import { View } from "react-native"
import { Button, Card, IconButton, MD3Colors, Surface, Text } from "react-native-paper" import { Button, Card, IconButton, MD3Colors, Surface, Text } from "react-native-paper"
export type ChallengeCardProps = { export type ChallengeCardProps = {
challenge: Challenge | null, challenge: Challenge,
onSuccess?: () => void, onSuccess?: () => void,
onFail?: () => void, onFail?: () => void,
onDelete?: () => void, onDelete?: () => void,
onEdit?: () => void, onEdit?: () => void,
style?: ViewStyle,
} }
export default function ChallengeCard({ challenge, onSuccess, onFail, onDelete, onEdit, style }: ChallengeCardProps) { export default function ChallengeCard({ challenge, onSuccess, onFail, onDelete, onEdit }: ChallengeCardProps) {
return ( return (
<Surface elevation={2} style={{ ...style, borderRadius: 20 }}> <Surface elevation={2} style={{ flex: 1, margin: 20, borderRadius: 20 }}>
<Card.Title <Card.Title
title={challenge?.title} title={challenge.title}
titleStyle={{ textAlign: 'center' }} titleStyle={{ textAlign: 'center' }}
titleVariant='headlineMedium' titleVariant='headlineMedium'
right={(props) => onEdit ? <IconButton {...props} icon='file-document-edit-outline' onPress={() => onEdit()} /> : <></>} /> right={(props) => onEdit ? <IconButton {...props} icon='file-document-edit-outline' onPress={() => onEdit()} /> : <></>} />
<View style={{ flexGrow: 1 }}> <View style={{ flexGrow: 1 }}>
<Surface elevation={5} mode='flat' style={{ flexGrow: 1, paddingHorizontal: 15, paddingVertical: 20 }}> <Surface elevation={5} mode='flat' style={{ flexGrow: 1, paddingHorizontal: 15, paddingVertical: 20 }}>
<Text variant='bodyLarge' style={{ flexGrow: 1 }}>{challenge?.description}</Text> <Text variant='bodyLarge' style={{ flexGrow: 1 }}>{challenge.description}</Text>
<Text variant='titleMedium'> <Text variant='titleMedium'>
Récompense : {challenge?.reward} <FontAwesome6 name='coins' /> Récompense : {challenge.reward} <FontAwesome6 name='coins' />
</Text> </Text>
</Surface> </Surface>
</View> </View>
@ -35,7 +34,7 @@ export default function ChallengeCard({ challenge, onSuccess, onFail, onDelete,
{onSuccess && <Button key='successBtn' mode='contained' icon='check' onPress={() => onSuccess()}> {onSuccess && <Button key='successBtn' mode='contained' icon='check' onPress={() => onSuccess()}>
Terminer Terminer
</Button>} </Button>}
{onDelete && <Button key='deleteBtn' mode='contained' icon='delete' onPress={() => onDelete()} buttonColor={MD3Colors.error60} textColor={MD3Colors.secondary10}> {onDelete && <Button key='deleteBtn' mode='contained' icon='delete' onPress={() => onDelete()} buttonColor={MD3Colors.error60}>
Supprimer Supprimer
</Button>} </Button>}
</View> </View>

View File

@ -1,5 +1,4 @@
import { AuthState } from "@/utils/features/auth/authSlice" import { AuthState } from "@/utils/features/auth/authSlice"
import { Challenge } from "@/utils/features/challenges/challengesSlice"
import { useMutation } from "@tanstack/react-query" import { useMutation } from "@tanstack/react-query"
type ErrorResponse = { type ErrorResponse = {
@ -18,12 +17,6 @@ type ChallengeActionProps = {
onError?: onErrorFunc onError?: onErrorFunc
} }
type ChallengeProps = {
auth: AuthState
onPostSuccess?: onPostSuccessFunc
onError?: onErrorFunc
}
export const useDrawRandomChallengeMutation = ({ auth, onPostSuccess, onError }: ChallengeActionProps) => { export const useDrawRandomChallengeMutation = ({ auth, onPostSuccess, onError }: ChallengeActionProps) => {
return useMutation({ return useMutation({
mutationFn: async () => { mutationFn: async () => {
@ -80,94 +73,3 @@ export const useEndChallenge = ({ auth, onPostSuccess, onError }: ChallengeActio
} }
}) })
} }
export const useAddChallengeMutation = ({ auth, onPostSuccess, onError }: ChallengeProps) => {
return useMutation({
mutationFn: async (challenge: Omit<Challenge, 'id'>) => {
return fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/challenges/`, {
method: "POST",
headers: {
"Authorization": `Bearer ${auth.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: challenge.title,
description: challenge.description,
reward: challenge.reward,
})
}).then(resp => resp.json())
},
onSuccess: async (data) => {
if (data.statusCode) {
if (onError)
onError({ response: data })
return
}
if (onPostSuccess)
onPostSuccess()
},
onError: async (error: Error) => {
if (onError)
onError({ error: error })
}
})
}
export const useEditChallengeMutation = ({ auth, onPostSuccess, onError }: ChallengeProps) => {
return useMutation({
mutationFn: async (challenge: Challenge) => {
return fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/challenges/${challenge.id}/`, {
method: "PATCH",
headers: {
"Authorization": `Bearer ${auth.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: challenge.title,
description: challenge.description,
reward: challenge.reward,
})
}).then(resp => resp.json())
},
onSuccess: async (data) => {
if (data.statusCode) {
if (onError)
onError({ response: data })
return
}
if (onPostSuccess)
onPostSuccess()
},
onError: async (error: Error) => {
if (onError)
onError({ error: error })
}
})
}
export const useDeleteChallengeMutation = ({ auth, onPostSuccess, onError }: ChallengeProps) => {
return useMutation({
mutationFn: async (challenge: Challenge) => {
return fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/challenges/${challenge.id}/`, {
method: "DELETE",
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
}
if (onPostSuccess)
onPostSuccess()
},
onError: async (error: Error) => {
if (onError)
onError({ error: error })
}
})
}