traintrape-moi/client/app/challenges-list.tsx

303 lines
12 KiB
TypeScript
Raw Permalink Normal View History

2024-12-14 10:55:11 +00:00
import ChallengeCard from "@/components/ChallengeCard"
import { useAddChallengeMutation, useAttachNewChallenge, useDeleteChallengeMutation, useEditChallengeMutation } from "@/hooks/mutations/useChallengeMutation"
import { useAuth } from "@/hooks/useAuth"
import { useChallengeActions } from "@/hooks/useChallengeActions"
2024-12-14 10:55:11 +00:00
import { useChallenges } from "@/hooks/useChallenges"
import { useGame } from "@/hooks/useGame"
2024-12-14 10:55:11 +00:00
import { Challenge } from "@/utils/features/challenges/challengesSlice"
import { FontAwesome6 } from "@expo/vector-icons"
import { useQueryClient } from "@tanstack/react-query"
2024-12-14 10:55:11 +00:00
import { useRouter } from "expo-router"
import React, { ReactNode, useMemo, useState } from "react"
2024-12-14 10:55:11 +00:00
import { FlatList, StyleSheet } from "react-native"
import { ActivityIndicator, Appbar, Button, Dialog, Divider, FAB, List, MD3Colors, Modal, Portal, Snackbar, Surface, Text, TextInput, Tooltip } from "react-native-paper"
2024-12-14 10:55:11 +00:00
export default function ChallengesList() {
const router = useRouter()
const queryClient = useQueryClient()
const auth = useAuth()
const game = useGame()
2024-12-14 10:55:11 +00:00
const challenges = useChallenges()
const challengeActions = useChallengeActions()
const currentChallengeAction = useMemo(() => {
if (!game.activeChallengeId)
return null
return challengeActions.find((action) => action.id === game.activeChallengeId)
}, [game, challengeActions])
2024-12-14 10:55:11 +00:00
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 [challengeToAttach, setChallengeToAttach] = useState<Challenge | null>(null)
2024-12-14 10:55:11 +00:00
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)
setEditChallengeVisible(false)
queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'get-challenges' })
},
onError: ({ response, error }) => {
setErrorVisible(true)
setEditChallengeVisible(false)
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)
setEditChallengeVisible(false)
setDisplayedChallenge(null)
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é !")
setDisplayedChallenge(null)
queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'get-challenges' })
},
onError: ({ response, error }) => {
setErrorVisible(true)
setDisplayedChallenge(null)
if (response)
setError([response.statusCode, response.message])
else if (error)
setError([400, error.message])
},
})
const attachNewChallengeMutation = useAttachNewChallenge({
auth,
onPostSuccess: () => {
setChallengeToAttach(null)
setSuccessMessage("Le défi en cours a bien été modifié !")
queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'get-challenges' })
},
onError: ({ response, error }) => {
setChallengeToAttach(null)
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)
}
function sendAttachNewChallenge() {
if (!challengeToAttach || !currentChallengeAction) return
attachNewChallengeMutation.mutate({ challengeActionId: currentChallengeAction.id, newChallengeId: challengeToAttach.id })
}
2024-12-14 10:55:11 +00:00
return (
<Surface style={{ flex: 1 }}>
<Appbar.Header>
<Appbar.BackAction onPress={() => router.canGoBack() ? router.back() : router.navigate('/(tabs)/challenges')} />
2024-12-14 10:55:11 +00:00
<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}
activeRunner={game.currentRunner}
onPress={() => setDisplayedChallenge(item.item)}
onLongPress={!currentChallengeAction ? undefined : () => setChallengeToAttach(item.item)} />} />
2024-12-14 10:55:11 +00:00
<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, bottom: errorVisible || successSnackbarVisible ? 80 : styles.addButton.bottom }}
2024-12-14 10:55:11 +00:00
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={() => !addChallengeMutation.isPending && editChallengeMutation.isPending && sendEditChallenge()} />
2024-12-14 10:55:11 +00:00
</Dialog.Content>
<Dialog.Actions>
<Button onPress={() => setEditChallengeVisible(false)}>Annuler</Button>
<Button
onPress={sendEditChallenge}
disabled={!editChallengeTitle || !editChallengeDescription || !editChallengeReward || addChallengeMutation.isPending || editChallengeMutation.isPending}>
{editChallengeId ? "Modifier" : "Ajouter"}
2024-12-14 10:55:11 +00:00
</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} disabled={deleteChallengeMutation.isPending}>Confirmer</Button>
2024-12-14 10:55:11 +00:00
</Dialog.Actions>
</Dialog>
<Dialog visible={challengeToAttach !== null} onDismiss={() => setChallengeToAttach(null)}>
<Dialog.Title>Traiter ce défi</Dialog.Title>
<Dialog.Content>
<Text variant='bodyMedium'>
Voulez-vous vraiment remplacer votre défi actuel par le défi « {challengeToAttach?.title} » ?
</Text>
</Dialog.Content>
<Dialog.Actions>
<Button onPress={() => setChallengeToAttach(null)}>Annuler</Button>
<Button onPress={sendAttachNewChallenge} disabled={attachNewChallengeMutation.isPending}>Confirmer</Button>
</Dialog.Actions>
</Dialog>
2024-12-14 10:55:11 +00:00
</Portal>
</Surface>
)
}
type ChallengeListItemProps = {
challenge: Challenge,
activeRunner: boolean,
onPress?: () => void,
onLongPress?: () => void,
}
function ChallengeListItem({ challenge, activeRunner, onPress, onLongPress }: ChallengeListItemProps) {
const challengeActions = useChallengeActions()
const attachedAction = challengeActions.find(challengeAction => challengeAction.challengeId === challenge.id)
2024-12-14 10:55:11 +00:00
const description = <Text>Récompense : {challenge.reward} <FontAwesome6 name='coins' /></Text>
const icon: ReactNode = useMemo(() => {
if (!activeRunner)
return undefined
else if (!attachedAction)
return <Tooltip title="Disponible"><List.Icon icon='cards' /></Tooltip>
else if (!attachedAction.end)
return <Tooltip title="En cours d'accomplissement"><ActivityIndicator /></Tooltip>
else if (attachedAction.success)
return <Tooltip title="Précédemment réussi"><List.Icon icon='emoticon-happy' /></Tooltip>
else
return <Tooltip title="Raté"><List.Icon icon='emoticon-sad' /></Tooltip>
}, [activeRunner, attachedAction])
2024-12-14 10:55:11 +00:00
return (
<List.Item
title={challenge.title}
description={description}
right={() => icon}
onPress={onPress}
onLongPress={attachedAction ? undefined : onLongPress} />
2024-12-14 10:55:11 +00:00
)
}
const styles = StyleSheet.create({
addButton: {
position: 'absolute',
right: 25,
bottom: 25,
}
})