Compare commits
	
		
			2 Commits
		
	
	
		
			50382079c0
			...
			fd4b0e8cd1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fd4b0e8cd1 | |||
| dba5b511ae | 
| @@ -25,7 +25,7 @@ export default function TabLayout() { | ||||
|       <Tabs.Screen | ||||
|         name="challenges" | ||||
|         options={{ | ||||
|           title: 'Défis', | ||||
|           title: 'Défi en cours', | ||||
|           headerShown: false, | ||||
|           tabBarIcon: ({ color }) => <FontAwesome6 name="coins" size={24} color={color} />, | ||||
|         }} | ||||
|   | ||||
| @@ -7,15 +7,17 @@ import { useChallenges } from '@/hooks/useChallenges' | ||||
| import { useGame } from '@/hooks/useGame' | ||||
| import { FontAwesome6 } from '@expo/vector-icons' | ||||
| import { useQueryClient } from '@tanstack/react-query' | ||||
| import { useRouter } from 'expo-router' | ||||
| import { useEffect, useMemo, useState } from 'react' | ||||
| import { View } from 'react-native' | ||||
| import { ActivityIndicator, Appbar, Banner, FAB, MD3Colors, Snackbar, Surface, Text, TouchableRipple } from 'react-native-paper' | ||||
|  | ||||
| function ChallengeScreenHeader() { | ||||
|   const router = useRouter() | ||||
|   return <> | ||||
|     <Appbar.Header> | ||||
|       <Appbar.Content title={"Défis"} /> | ||||
|       <Appbar.Action icon='format-list-bulleted' /> | ||||
|       <Appbar.Content title={"Défi en cours"} /> | ||||
|       <Appbar.Action icon='format-list-bulleted' onPress={() => router.navigate('/challenges-list')} /> | ||||
|     </Appbar.Header> | ||||
|     <PenaltyBanner /> | ||||
|   </> | ||||
| @@ -85,7 +87,8 @@ function ChallengeScreenBody() { | ||||
|       <ChallengeCard | ||||
|           challenge={currentChallenge} | ||||
|           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 && <> | ||||
|       <Banner | ||||
|           visible={!currentChallenge && game.currentRunner && !loading} | ||||
|   | ||||
| @@ -24,14 +24,16 @@ export default function MapScreen() { | ||||
|             visible={game.gameStarted || game.money > 0} | ||||
|             icon={(props) => <FontAwesome6 {...props} name='coins' size={20} />} | ||||
|             color='black' | ||||
|             label={`${game.money}`} /> | ||||
|             label={`${game.money}`} | ||||
|             onPress={() => {}} /> | ||||
|         <FAB | ||||
|             style={styles.statusBadge} | ||||
|             visible={game.gameStarted || game.money > 0} | ||||
|             size='small' | ||||
|             color='black' | ||||
|             icon={game.currentRunner ? 'run-fast' : () => <FontAwesome6 name='cat' size={20} />} | ||||
|             label={game.currentRunner ? "Coureuse" : "Poursuiveuse"} /> | ||||
|             label={game.currentRunner ? "Coureuse" : "Poursuiveuse"} | ||||
|             onPress={() => {}} /> | ||||
|       </View> | ||||
|       <FreeChaseBanner /> | ||||
|     </Surface> | ||||
|   | ||||
| @@ -53,6 +53,7 @@ export default function RootLayout() { | ||||
|                   <Stack> | ||||
|                     <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> | ||||
|                     <Stack.Screen name="login" options={{ headerShown: false }} /> | ||||
|                     <Stack.Screen name="challenges-list" options={{ headerShown: false }} /> | ||||
|                     <Stack.Screen name="+not-found" /> | ||||
|                   </Stack> | ||||
|                   <StatusBar style="auto" /> | ||||
|   | ||||
							
								
								
									
										227
									
								
								client/app/challenges-list.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								client/app/challenges-list.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,227 @@ | ||||
| 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, | ||||
|   } | ||||
| }) | ||||
| @@ -1,29 +1,30 @@ | ||||
| import { Challenge } from "@/utils/features/challenges/challengesSlice" | ||||
| import { FontAwesome6 } from "@expo/vector-icons" | ||||
| import { View } from "react-native" | ||||
| import { View, ViewStyle } from "react-native" | ||||
| import { Button, Card, IconButton, MD3Colors, Surface, Text } from "react-native-paper" | ||||
|  | ||||
| export type ChallengeCardProps = { | ||||
|   challenge: Challenge, | ||||
|   challenge: Challenge | null, | ||||
|   onSuccess?: () => void, | ||||
|   onFail?: () => void, | ||||
|   onDelete?: () => void, | ||||
|   onEdit?: () => void, | ||||
|   style?: ViewStyle, | ||||
| } | ||||
|  | ||||
| export default function ChallengeCard({ challenge, onSuccess, onFail, onDelete, onEdit }: ChallengeCardProps) { | ||||
| export default function ChallengeCard({ challenge, onSuccess, onFail, onDelete, onEdit, style }: ChallengeCardProps) { | ||||
|   return ( | ||||
|     <Surface elevation={2} style={{ flex: 1, margin: 20, borderRadius: 20 }}> | ||||
|     <Surface elevation={2} style={{ ...style, borderRadius: 20 }}> | ||||
|       <Card.Title | ||||
|           title={challenge.title} | ||||
|           title={challenge?.title} | ||||
|           titleStyle={{ textAlign: 'center' }} | ||||
|           titleVariant='headlineMedium' | ||||
|           right={(props) => onEdit ? <IconButton {...props} icon='file-document-edit-outline' onPress={() => onEdit()} /> : <></>} /> | ||||
|       <View style={{ flexGrow: 1 }}> | ||||
|         <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'> | ||||
|             Récompense : {challenge.reward} <FontAwesome6 name='coins' /> | ||||
|             Récompense : {challenge?.reward} <FontAwesome6 name='coins' /> | ||||
|           </Text> | ||||
|         </Surface> | ||||
|       </View> | ||||
| @@ -34,7 +35,7 @@ export default function ChallengeCard({ challenge, onSuccess, onFail, onDelete, | ||||
|         {onSuccess && <Button key='successBtn' mode='contained' icon='check' onPress={() => onSuccess()}> | ||||
|           Terminer | ||||
|         </Button>} | ||||
|         {onDelete && <Button key='deleteBtn' mode='contained' icon='delete'  onPress={() => onDelete()} buttonColor={MD3Colors.error60}> | ||||
|         {onDelete && <Button key='deleteBtn' mode='contained' icon='delete'  onPress={() => onDelete()} buttonColor={MD3Colors.error60} textColor={MD3Colors.secondary10}> | ||||
|           Supprimer | ||||
|         </Button>} | ||||
|       </View> | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { AuthState } from "@/utils/features/auth/authSlice" | ||||
| import { Challenge } from "@/utils/features/challenges/challengesSlice" | ||||
| import { useMutation } from "@tanstack/react-query" | ||||
|  | ||||
| type ErrorResponse = { | ||||
| @@ -17,6 +18,12 @@ type ChallengeActionProps = { | ||||
|   onError?: onErrorFunc | ||||
| } | ||||
|  | ||||
| type ChallengeProps = { | ||||
|   auth: AuthState | ||||
|   onPostSuccess?: onPostSuccessFunc | ||||
|   onError?: onErrorFunc | ||||
| } | ||||
|  | ||||
| export const useDrawRandomChallengeMutation = ({ auth, onPostSuccess, onError }: ChallengeActionProps) => { | ||||
|   return useMutation({ | ||||
|     mutationFn: async () => { | ||||
| @@ -73,3 +80,94 @@ 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 }) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user