273 lines
8.3 KiB
TypeScript
273 lines
8.3 KiB
TypeScript
import { QuestionTypes } from "@/api/types";
|
|
import {
|
|
useAnswerMutation,
|
|
useCompleteTestMutation,
|
|
useUserTest,
|
|
} from "@/api/userTests";
|
|
import Question from "@/components/question";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogBackdrop,
|
|
AlertDialogBody,
|
|
AlertDialogContent,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
} from "@/components/ui/alert-dialog";
|
|
import Answer from "@/components/ui/answer";
|
|
import { Button, ButtonText } from "@/components/ui/button";
|
|
import Content from "@/components/ui/content";
|
|
import CustomButton from "@/components/ui/custom-button";
|
|
import { Divider } from "@/components/ui/divider";
|
|
import Panel from "@/components/ui/panel";
|
|
import { Textarea, TextareaInput } from "@/components/ui/textarea";
|
|
import { ThemedText } from "@/components/ui/themed-text";
|
|
import getErrorAxiosMessage from "@/utils/get-error-axios-message";
|
|
import getUserTestTitle from "@/utils/get-user-test-title";
|
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
|
import { useEffect, useState } from "react";
|
|
import { ScrollView, StyleSheet, View } from "react-native";
|
|
import { useToast } from "react-native-toast-notifications";
|
|
|
|
const DoingUserTestScreen = () => {
|
|
const { id: idParam } = useLocalSearchParams<{ id: string }>();
|
|
const [answer, setAnswer] = useState<(string | number)[]>([]);
|
|
const [page, setPage] = useState(0);
|
|
const [openCompleteDialog, setOpenCompleteDialog] = useState(false);
|
|
const id = +idParam;
|
|
const { data: userTest, refetch: refetchUserTest } = useUserTest(id);
|
|
const { mutateAsync: mutateAsyncAnswer, isPending: answerPending } =
|
|
useAnswerMutation();
|
|
const { mutate: mutateCompleteTest, isPending: completePending } = useCompleteTestMutation();
|
|
const toast = useToast();
|
|
useEffect(() => {
|
|
if (!userTest) return;
|
|
|
|
setAnswer(userTest.answers[page].answer || []);
|
|
}, [userTest, page]);
|
|
|
|
if (!userTest)
|
|
return (
|
|
<>
|
|
<Stack.Screen options={{ title: `Test #${id}` }} />
|
|
<Content>
|
|
<Question />
|
|
</Content>
|
|
</>
|
|
);
|
|
|
|
const currentAnswer = userTest.answers[page];
|
|
const currentQuestion = currentAnswer.question;
|
|
const pages = Array.from({ length: userTest.answers.length }).map(
|
|
(_, i) => i
|
|
);
|
|
const countAnsweredQuestion = userTest.answers.filter(
|
|
(answer) => answer.answer
|
|
).length;
|
|
const isAllAnswered = countAnsweredQuestion === userTest.answers.length;
|
|
const isLastPage = page === pages.length - 1;
|
|
const userTextAnswer = answer ? (answer[0] as string) : "";
|
|
const alertDialogMessage = isAllAnswered
|
|
? "Finish the test to see your results."
|
|
: "Warning: You haven't answered all questions. Are you sure you want to finish?";
|
|
|
|
const handleSetAnswer = (value: number | string) => {
|
|
if (currentQuestion.type === QuestionTypes.Text) {
|
|
setAnswer([value]);
|
|
}
|
|
if (currentQuestion.type === QuestionTypes.Single) {
|
|
setAnswer([value as number]);
|
|
}
|
|
if (currentQuestion.type === QuestionTypes.Multiple) {
|
|
if ((answer as number[]).includes(value as number)) {
|
|
setAnswer((prev) => prev.filter((v) => v !== value));
|
|
} else {
|
|
setAnswer((prev) => {
|
|
const set = new Set(prev as number[]);
|
|
set.add(value as number);
|
|
return Array.from(set);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
const handleSaveAnswer = async () => {
|
|
let ans = answer;
|
|
|
|
if (currentQuestion.type === QuestionTypes.Text && answer) {
|
|
ans = [(answer[0] as string).trim()];
|
|
}
|
|
|
|
try {
|
|
await mutateAsyncAnswer(
|
|
{
|
|
answerId: currentAnswer.id,
|
|
answer: ans,
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
toast.show("Your answer was saved", { type: "success" });
|
|
if(!isLastPage)
|
|
goPage(page + 1);
|
|
},
|
|
onError: (error) => {
|
|
toast.show(getErrorAxiosMessage(error), { type: "danger" });
|
|
},
|
|
}
|
|
);
|
|
} catch {}
|
|
await refetchUserTest();
|
|
};
|
|
const handleCompleteTest = () => {
|
|
mutateCompleteTest({
|
|
testId: userTest.id
|
|
}, {
|
|
onSuccess: () => {
|
|
router.replace(`/user-tests/${userTest.id}`);
|
|
},
|
|
onError: (error) => {
|
|
toast.show(getErrorAxiosMessage(error), { type: "danger" });
|
|
}
|
|
})
|
|
};
|
|
const goPage = (page: number) => {
|
|
setAnswer([]);
|
|
setPage(page);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Stack.Screen options={{ title: getUserTestTitle(userTest) }} />
|
|
<View style={styles.container}>
|
|
<Content style={styles.content}>
|
|
<View style={styles.top}>
|
|
<ScrollView horizontal contentContainerStyle={styles.topScrollView}>
|
|
{pages.map((p) => (
|
|
<Button
|
|
key={p}
|
|
variant={p === page ? "solid" : "outline"}
|
|
action={
|
|
p === page || userTest.answers[p].answer
|
|
? "primary"
|
|
: "secondary"
|
|
}
|
|
onPress={() => goPage(p)}
|
|
>
|
|
<ButtonText>{p + 1}</ButtonText>
|
|
</Button>
|
|
))}
|
|
<Button
|
|
variant="outline"
|
|
action="negative"
|
|
onPress={() => setOpenCompleteDialog(true)}
|
|
>
|
|
<ButtonText>Complete test</ButtonText>
|
|
</Button>
|
|
</ScrollView>
|
|
</View>
|
|
|
|
<Question question={currentQuestion} />
|
|
|
|
<Divider className="mt-5 mb-5" />
|
|
|
|
<View className="gap-4 mb-4">
|
|
{currentQuestion.variants.map((variant) => (
|
|
<Answer
|
|
key={variant.id}
|
|
answer={variant}
|
|
isTextType={false}
|
|
userAnswers={answer}
|
|
onPress={() => handleSetAnswer(variant.id)}
|
|
displayUserAnswer={true}
|
|
/>
|
|
))}
|
|
{currentQuestion.type === QuestionTypes.Text && (
|
|
<Panel>
|
|
<ThemedText className="mb-1 text-sm" type="defaultSemiBold">
|
|
Your answer:
|
|
</ThemedText>
|
|
<Textarea>
|
|
<TextareaInput
|
|
onChangeText={(text) => handleSetAnswer(text)}
|
|
value={userTextAnswer}
|
|
/>
|
|
</Textarea>
|
|
</Panel>
|
|
)}
|
|
</View>
|
|
|
|
<CustomButton isLoading={answerPending} onPress={handleSaveAnswer}>
|
|
<ButtonText>Save answer</ButtonText>
|
|
</CustomButton>
|
|
|
|
<Divider className="mt-2 mb-3" />
|
|
|
|
{(isAllAnswered || isLastPage) && (
|
|
<Button
|
|
variant={isAllAnswered ? "outline" : "solid"}
|
|
action="negative"
|
|
onPress={() => setOpenCompleteDialog(true)}
|
|
>
|
|
<ButtonText>Complete test</ButtonText>
|
|
</Button>
|
|
)}
|
|
</Content>
|
|
</View>
|
|
|
|
<AlertDialog
|
|
isOpen={openCompleteDialog}
|
|
onClose={() => setOpenCompleteDialog(false)}
|
|
>
|
|
<AlertDialogBackdrop />
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader className="mb-2">
|
|
<ThemedText type="defaultSemiBold">Complete test</ThemedText>
|
|
</AlertDialogHeader>
|
|
<AlertDialogBody className="mb-3">
|
|
<ThemedText>{alertDialogMessage}</ThemedText>
|
|
</AlertDialogBody>
|
|
<AlertDialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
action="secondary"
|
|
onPress={() => setOpenCompleteDialog(false)}
|
|
size="sm"
|
|
>
|
|
<ButtonText>Cancel</ButtonText>
|
|
</Button>
|
|
<CustomButton
|
|
variant={isAllAnswered ? "outline" : "solid"}
|
|
action="negative"
|
|
size="sm"
|
|
isLoading={completePending}
|
|
onPress={handleCompleteTest}
|
|
>
|
|
<ButtonText>Complete</ButtonText>
|
|
</CustomButton>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
alignItems: "flex-start",
|
|
},
|
|
top: {
|
|
width: "100%",
|
|
marginBottom: 15,
|
|
},
|
|
topScrollView: {
|
|
flexDirection: "row",
|
|
alignItems: "flex-start",
|
|
gap: 10,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
width: "100%",
|
|
},
|
|
});
|
|
|
|
export default DoingUserTestScreen;
|