Added UserTest single and list
This commit is contained in:
parent
59298e1581
commit
8ac1f6be34
@ -74,10 +74,10 @@ export const get_test = async (id: number) => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const get_user_tests = async () => {
|
export const get_my_user_tests = async () => {
|
||||||
const response = await axiosInstance.get<{
|
const response = await axiosInstance.get<{
|
||||||
data: UserTestResponse[];
|
data: UserTestResponse[];
|
||||||
}>("/api/user-tests/");
|
}>("/api/user-tests/me/");
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export interface QuestionResponse {
|
|||||||
type: QuestionTypes;
|
type: QuestionTypes;
|
||||||
difficulty: number;
|
difficulty: number;
|
||||||
|
|
||||||
variants?: QuestionVariant[];
|
variants: QuestionVariant[];
|
||||||
correct_answers: number[]|string[];
|
correct_answers: number[]|string[];
|
||||||
|
|
||||||
category_id: number;
|
category_id: number;
|
||||||
@ -63,7 +63,8 @@ export interface TestResponse {
|
|||||||
|
|
||||||
author_id: number;
|
author_id: number;
|
||||||
author: UserResponse;
|
author: UserResponse;
|
||||||
questions: QuestionResponse[];
|
questions?: QuestionResponse[];
|
||||||
|
questions_count: number;
|
||||||
|
|
||||||
closed_at: string;
|
closed_at: string;
|
||||||
|
|
||||||
@ -98,7 +99,6 @@ export interface UserTestResponse {
|
|||||||
|
|
||||||
answers: AnswerResponse[];
|
answers: AnswerResponse[];
|
||||||
|
|
||||||
|
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { get_user_test, get_user_tests } from "./_client";
|
import { get_my_user_tests, get_user_test } from "./_client";
|
||||||
|
|
||||||
export const useTests = () => {
|
export const useMyTests = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['user-tests'],
|
queryKey: ['user-tests'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await get_user_tests();
|
const response = await get_my_user_tests();
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTest = (id: number) => {
|
export const useUserTest = (id: number) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['test', id],
|
queryKey: ['test', id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
|
import useAuthContext from "@/components/providers/auth-provider/hook";
|
||||||
import useAuthContext from '@/components/providers/auth-provider/hook';
|
import { ButtonText } from "@/components/ui/button";
|
||||||
import { Button, ButtonText } from '@/components/ui/button';
|
import ButtonSection from "@/components/ui/button-section";
|
||||||
import Content from '@/components/ui/content';
|
import Content from "@/components/ui/content";
|
||||||
import { Divider } from '@/components/ui/divider';
|
import { Divider } from "@/components/ui/divider";
|
||||||
import UserHeader from '@/components/user-header';
|
import UserHeader from "@/components/user-header";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
|
||||||
export default function MeScreen() {
|
export default function MeScreen() {
|
||||||
const { user, logout, isAuthorized } = useAuthContext();
|
const { user, logout, isAuthorized } = useAuthContext();
|
||||||
@ -12,9 +13,25 @@ export default function MeScreen() {
|
|||||||
<Content>
|
<Content>
|
||||||
<UserHeader user={user} />
|
<UserHeader user={user} />
|
||||||
|
|
||||||
<Divider className='mt-3 mb-3' />
|
<Divider className="mt-3 mb-3" />
|
||||||
|
|
||||||
{isAuthorized && <Button action="negative" onPress={() => logout()}><ButtonText>Logout</ButtonText></Button>}
|
{isAuthorized && (
|
||||||
|
<>
|
||||||
|
<ButtonSection onPress={() => router.push('/me/user-tests')}>
|
||||||
|
<ButtonText>My Tests</ButtonText>
|
||||||
|
</ButtonSection>
|
||||||
|
|
||||||
|
<Divider className="mt-3 mb-3" />
|
||||||
|
|
||||||
|
<ButtonSection
|
||||||
|
action="negative"
|
||||||
|
onPress={() => logout()}
|
||||||
|
variant="solid"
|
||||||
|
>
|
||||||
|
<ButtonText>Logout</ButtonText>
|
||||||
|
</ButtonSection>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Content>
|
</Content>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -3,11 +3,13 @@ import { useTests } from '@/api/tests';
|
|||||||
import { TestResponse } from '@/api/types';
|
import { TestResponse } from '@/api/types';
|
||||||
import useTaxonomyContext from '@/components/providers/taxonomy-provider/hook';
|
import useTaxonomyContext from '@/components/providers/taxonomy-provider/hook';
|
||||||
import Test from '@/components/test';
|
import Test from '@/components/test';
|
||||||
|
import { Button, ButtonText } from '@/components/ui/button';
|
||||||
import Content from '@/components/ui/content';
|
import Content from '@/components/ui/content';
|
||||||
import CustomSelect from '@/components/ui/custom-select';
|
import CustomSelect from '@/components/ui/custom-select';
|
||||||
import PaginationList from '@/components/ui/pagination-list';
|
import PaginationList from '@/components/ui/pagination-list';
|
||||||
import { ThemedText } from '@/components/ui/themed-text';
|
import { ThemedText } from '@/components/ui/themed-text';
|
||||||
import getCategoryOptions from '@/utils/get-category-options';
|
import getCategoryOptions from '@/utils/get-category-options';
|
||||||
|
import { FontAwesome5 } from '@expo/vector-icons';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { ScrollView, View } from 'react-native';
|
import { ScrollView, View } from 'react-native';
|
||||||
@ -39,13 +41,17 @@ export default function TestsScreen() {
|
|||||||
<Content ref={scrollRef}>
|
<Content ref={scrollRef}>
|
||||||
<ThemedText type="title" className='mb-3'>Tests</ThemedText>
|
<ThemedText type="title" className='mb-3'>Tests</ThemedText>
|
||||||
|
|
||||||
<View className="mb-4">
|
<View className="mb-4 flex-row">
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
|
className="mr-2 flex-1"
|
||||||
options={categoryOptions}
|
options={categoryOptions}
|
||||||
noneOption={{ label: "All categories", value: "all" }}
|
noneOption={{ label: "All categories", value: "all" }}
|
||||||
selectedValue={category ? `${category}` : undefined}
|
selectedValue={category ? `${category}` : undefined}
|
||||||
onValueChange={handleChangeCategory}
|
onValueChange={handleChangeCategory}
|
||||||
placeholder="Select category" />
|
placeholder="Select category" />
|
||||||
|
<Button>
|
||||||
|
<ButtonText><FontAwesome5 name="qrcode" size={18} /></ButtonText>
|
||||||
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<PaginationList<TestResponse>
|
<PaginationList<TestResponse>
|
||||||
|
|||||||
61
app/me/user-tests.tsx
Normal file
61
app/me/user-tests.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { useMyTests } from "@/api/userTests";
|
||||||
|
import Content from "@/components/ui/content";
|
||||||
|
import CustomSelect from "@/components/ui/custom-select";
|
||||||
|
import UserTest from "@/components/user-test";
|
||||||
|
import { getUserTestStatus, TestStatus } from "@/utils/get-user-test-status";
|
||||||
|
import { router, Stack } from "expo-router";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
const filterSelectOptions = [
|
||||||
|
{ label: TestStatus.Completed, value: TestStatus.Completed },
|
||||||
|
{ label: TestStatus.InProgress, value: TestStatus.InProgress },
|
||||||
|
{ label: TestStatus.Expired, value: TestStatus.Expired },
|
||||||
|
];
|
||||||
|
|
||||||
|
const MeUserTestsScreen = () => {
|
||||||
|
const { data: userTests } = useMyTests();
|
||||||
|
const [selectedStatus, setStatus] = useState<string>("all");
|
||||||
|
const filteredUserTests = useMemo(() => {
|
||||||
|
if(!userTests) return [];
|
||||||
|
if(selectedStatus !== "all") {
|
||||||
|
return userTests.filter((userTest) => getUserTestStatus(userTest) === selectedStatus);
|
||||||
|
}
|
||||||
|
return userTests;
|
||||||
|
}, [userTests, selectedStatus]);
|
||||||
|
|
||||||
|
if(!userTests)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen options={{ title: `My tests` }} />
|
||||||
|
<Content>
|
||||||
|
<UserTest />
|
||||||
|
</Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen options={{ title: `My tests` }} />
|
||||||
|
<Content>
|
||||||
|
<View className="mb-4">
|
||||||
|
<CustomSelect
|
||||||
|
options={filterSelectOptions}
|
||||||
|
noneOption={{ label: "All tests", value: "all" }}
|
||||||
|
placeholder="Filter by status"
|
||||||
|
selectedValue={selectedStatus}
|
||||||
|
onValueChange={(value) => setStatus(value)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{filteredUserTests.map((userTest) => (
|
||||||
|
<View key={userTest.id} className="mb-3">
|
||||||
|
<UserTest userTest={userTest} onPress={() => router.push(`/user-tests/${userTest.id}`)} />
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</Content>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MeUserTestsScreen;
|
||||||
@ -5,11 +5,13 @@ import { Button, ButtonText } from "@/components/ui/button";
|
|||||||
import Content from "@/components/ui/content";
|
import Content from "@/components/ui/content";
|
||||||
import { Stack, useLocalSearchParams } from "expo-router";
|
import { Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
|
import { useToast } from "react-native-toast-notifications";
|
||||||
|
|
||||||
const TestScreen = () => {
|
const TestScreen = () => {
|
||||||
const { id: idParam } = useLocalSearchParams<{ id: string }>();
|
const { id: idParam } = useLocalSearchParams<{ id: string }>();
|
||||||
const id = +idParam;
|
const id = +idParam;
|
||||||
const { data, isLoading } = useTest(id);
|
const { data, isLoading } = useTest(id);
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
if (!data)
|
if (!data)
|
||||||
return (
|
return (
|
||||||
@ -19,7 +21,9 @@ const TestScreen = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleStartTest = () => {};
|
const handleStartTest = () => {
|
||||||
|
toast.show("Test started!", { type: "success" });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -34,7 +38,7 @@ const TestScreen = () => {
|
|||||||
</Button>}
|
</Button>}
|
||||||
|
|
||||||
<View className="gap-4">
|
<View className="gap-4">
|
||||||
{data.questions.map((question) => (
|
{data.questions?.map((question) => (
|
||||||
<Question key={question.id} question={question} />
|
<Question key={question.id} question={question} />
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
95
app/user-tests/[id].tsx
Normal file
95
app/user-tests/[id].tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { AnswerResponse, QuestionVariant } from "@/api/types";
|
||||||
|
import { useUserTest } from "@/api/userTests";
|
||||||
|
import Question from "@/components/question";
|
||||||
|
import Answer from "@/components/ui/answer";
|
||||||
|
import { Button, ButtonText } from "@/components/ui/button";
|
||||||
|
import Content from "@/components/ui/content";
|
||||||
|
import { Divider } from "@/components/ui/divider";
|
||||||
|
import UserTest from "@/components/user-test";
|
||||||
|
import { getUserTestStatus, TestStatus } from "@/utils/get-user-test-status";
|
||||||
|
import getUserTestTitle from "@/utils/get-user-test-title";
|
||||||
|
import { Stack, useLocalSearchParams } from "expo-router";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
const UserTestScreen = () => {
|
||||||
|
const { id: idParam } = useLocalSearchParams<{ id: string }>();
|
||||||
|
const id = +idParam;
|
||||||
|
const { data: userTest } = useUserTest(id);
|
||||||
|
|
||||||
|
if (!userTest)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen options={{ title: `Test #${id}` }} />
|
||||||
|
<Content><UserTest /></Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const status = getUserTestStatus(userTest);
|
||||||
|
const displayCorrectAnswers = status !== TestStatus.InProgress;
|
||||||
|
|
||||||
|
const handleContinueTest = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
const isCorrectVariation = (answer: AnswerResponse, variant: QuestionVariant) => {
|
||||||
|
if(answer.is_correct) return true;
|
||||||
|
|
||||||
|
const isCorrectVariant = (answer.question.correct_answers as number[]).includes(variant.id);
|
||||||
|
const isUserAnsweredVariant = answer.answer ? (answer.answer as number[]).includes(variant.id) : false;
|
||||||
|
|
||||||
|
return isCorrectVariant === isUserAnsweredVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen options={{ title: getUserTestTitle(userTest) }} />
|
||||||
|
<Content>
|
||||||
|
<View className="mb-3">
|
||||||
|
<UserTest userTest={userTest} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{status === TestStatus.InProgress && <Button className="mb-3" onPress={handleContinueTest}>
|
||||||
|
<ButtonText>Continue</ButtonText>
|
||||||
|
</Button>}
|
||||||
|
|
||||||
|
<Divider className="mb-4" />
|
||||||
|
|
||||||
|
<View className="gap-3">
|
||||||
|
{userTest.answers.map((answer) => (
|
||||||
|
<View key={answer.id}>
|
||||||
|
<Question question={answer.question} withCategory={false} />
|
||||||
|
<View className="mt-2 pl-5">
|
||||||
|
{answer.question.type !== 'text' && answer.question.variants.map((variant) => (
|
||||||
|
<View key={variant.id} className="mb-2">
|
||||||
|
<Answer
|
||||||
|
answer={variant}
|
||||||
|
isTextType={false}
|
||||||
|
userAnswers={answer.answer}
|
||||||
|
correctAnswers={answer.question.correct_answers}
|
||||||
|
isCorrectUserAnswer={isCorrectVariation(answer, variant)}
|
||||||
|
displayUserAnswer={true}
|
||||||
|
displayCorrect={displayCorrectAnswers}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
{answer.question.type === 'text' && (
|
||||||
|
<View className="mb-2">
|
||||||
|
<Answer
|
||||||
|
answer={{ id: 0, text: answer.answer ? answer.answer[0] as string : '-' }}
|
||||||
|
isTextType={true}
|
||||||
|
isCorrectUserAnswer={answer.is_correct}
|
||||||
|
displayUserAnswer={true}
|
||||||
|
displayCorrect={displayCorrectAnswers}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
</Content>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserTestScreen;
|
||||||
@ -43,7 +43,7 @@ const Test = ({ test, onPress }: TestProps) => {
|
|||||||
<ThemedText type="meta">
|
<ThemedText type="meta">
|
||||||
<FontAwesome5 name="user" /> {test.author.username}
|
<FontAwesome5 name="user" /> {test.author.username}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ThemedText type="meta">{test.questions.length} Questions</ThemedText>
|
<ThemedText type="meta">{test.questions_count} Questions</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
</Panel>
|
</Panel>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
@ -91,7 +91,7 @@ const Answer = ({
|
|||||||
return style;
|
return style;
|
||||||
}, [displayCorrect, displayUserAnswer, isAnsweredByUser, isCorrectAnswer, isTextType, userAnsweredCorrectly]);
|
}, [displayCorrect, displayUserAnswer, isAnsweredByUser, isCorrectAnswer, isTextType, userAnsweredCorrectly]);
|
||||||
const textColor = useMemo(() => {
|
const textColor = useMemo(() => {
|
||||||
if(stylePanel)
|
if(stylePanel === styles.correctAnswer || stylePanel === styles.incorrectAnswer)
|
||||||
return "#ffffff";
|
return "#ffffff";
|
||||||
else
|
else
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
18
components/ui/button-section.tsx
Normal file
18
components/ui/button-section.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ComponentProps } from "react";
|
||||||
|
import { Button } from "./button";
|
||||||
|
|
||||||
|
import Panel from "./panel";
|
||||||
|
|
||||||
|
type ButtonSectionProps = ComponentProps<typeof Button>;
|
||||||
|
|
||||||
|
const ButtonSection = ({children, ...rest }: ButtonSectionProps) => {
|
||||||
|
return (
|
||||||
|
<Panel withPadding={false}>
|
||||||
|
<Button variant="outline" action="primary" {...rest}>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
</Panel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ButtonSection;
|
||||||
@ -14,6 +14,7 @@ interface CustomSelectProps {
|
|||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
selectedValue?:string;
|
selectedValue?:string;
|
||||||
onValueChange?: (value: string) => void;
|
onValueChange?: (value: string) => void;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomSelect = ({
|
const CustomSelect = ({
|
||||||
@ -22,6 +23,7 @@ const CustomSelect = ({
|
|||||||
defaultValue,
|
defaultValue,
|
||||||
selectedValue,
|
selectedValue,
|
||||||
noneOption,
|
noneOption,
|
||||||
|
className,
|
||||||
placeholder = "Select option"
|
placeholder = "Select option"
|
||||||
}: CustomSelectProps) => {
|
}: CustomSelectProps) => {
|
||||||
const optionsComputed = useMemo(() => {
|
const optionsComputed = useMemo(() => {
|
||||||
@ -33,7 +35,7 @@ const CustomSelect = ({
|
|||||||
const backgroundColor = useThemeColor({ }, "background");
|
const backgroundColor = useThemeColor({ }, "background");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select onValueChange={onValueChange} selectedValue={selectedValue} defaultValue={defaultValue}>
|
<Select className={className} onValueChange={onValueChange} selectedValue={selectedValue} defaultValue={defaultValue}>
|
||||||
<SelectTrigger variant="outline" size="md" style={{ backgroundColor }}>
|
<SelectTrigger variant="outline" size="md" style={{ backgroundColor }}>
|
||||||
<SelectInput placeholder={placeholder} />
|
<SelectInput placeholder={placeholder} />
|
||||||
<FontAwesome5 className="mr-3" name="chevron-down" />
|
<FontAwesome5 className="mr-3" name="chevron-down" />
|
||||||
|
|||||||
@ -6,13 +6,14 @@ interface PanelProps {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: StyleProp<ViewStyle>;
|
style?: StyleProp<ViewStyle>;
|
||||||
|
withPadding?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Panel = ({children, className, style}: PanelProps) => {
|
const Panel = ({children, className, style, withPadding = true}: PanelProps) => {
|
||||||
const backgroundColor = useThemeColor({ }, 'background');
|
const backgroundColor = useThemeColor({ }, 'background');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.panel, { backgroundColor }, style]} className={className}>
|
<View style={[styles.panel, withPadding ? styles.panelPadding : null, { backgroundColor }, style]} className={className}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@ -20,10 +21,12 @@ const Panel = ({children, className, style}: PanelProps) => {
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
panel: {
|
panel: {
|
||||||
padding: 15,
|
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
boxShadow: '0px 4px 5px 0px rgba(0,0,0,0.18)'
|
boxShadow: '0px 4px 5px 0px rgba(0,0,0,0.18)'
|
||||||
},
|
},
|
||||||
|
panelPadding: {
|
||||||
|
padding: 15,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
107
components/user-test.tsx
Normal file
107
components/user-test.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { UserTestResponse } from "@/api/types";
|
||||||
|
import formatISODate from "@/utils/format-iso-date";
|
||||||
|
import { getUserTestStatus } from "@/utils/get-user-test-status";
|
||||||
|
import getUserTestTitle from "@/utils/get-user-test-title";
|
||||||
|
import { FontAwesome5 } from "@expo/vector-icons";
|
||||||
|
import { Pressable, StyleSheet, View } from "react-native";
|
||||||
|
import { Badge, BadgeText } from "./ui/badge";
|
||||||
|
import { Divider } from "./ui/divider";
|
||||||
|
import Panel from "./ui/panel";
|
||||||
|
import { Skeleton } from "./ui/skeleton";
|
||||||
|
import { ThemedText } from "./ui/themed-text";
|
||||||
|
|
||||||
|
type BadgeType = "muted" | "success" | "info" | "warning" | "error";
|
||||||
|
enum TestStatus {
|
||||||
|
Completed = "Completed",
|
||||||
|
Expired = "Expired",
|
||||||
|
InProgress = "In Progress",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserTestProps {
|
||||||
|
userTest?: UserTestResponse;
|
||||||
|
onPress?: (userTest: UserTestResponse) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserTest = ({ userTest, onPress }: UserTestProps) => {
|
||||||
|
if (!userTest) return <Skeleton className="h-20" />;
|
||||||
|
|
||||||
|
const title = getUserTestTitle(userTest);
|
||||||
|
|
||||||
|
const testStatus = getUserTestStatus(userTest);
|
||||||
|
let scoreType: BadgeType = "muted";
|
||||||
|
|
||||||
|
if (userTest.is_completed || !userTest.is_available) {
|
||||||
|
if (userTest.score >= 80) scoreType = "success";
|
||||||
|
else if (userTest.score >= 60) scoreType = "info";
|
||||||
|
else if (userTest.score >= 40) scoreType = "warning";
|
||||||
|
else if (userTest.score >= 0) scoreType = "error";
|
||||||
|
}
|
||||||
|
const successAnswers = userTest.answers.filter(answer => answer.is_correct).length;
|
||||||
|
|
||||||
|
let testStatusType: BadgeType = "warning";
|
||||||
|
if (testStatus === TestStatus.Completed) testStatusType = "success";
|
||||||
|
else if (testStatus === TestStatus.Expired) testStatusType = "error";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable onPress={() => onPress?.(userTest)}>
|
||||||
|
<Panel>
|
||||||
|
<View className="justify-between flex-row items-center">
|
||||||
|
<Badge action={testStatusType}>
|
||||||
|
<BadgeText>
|
||||||
|
<FontAwesome5 name="info-circle" /> {testStatus}
|
||||||
|
</BadgeText>
|
||||||
|
</Badge>
|
||||||
|
<Badge action={scoreType}>
|
||||||
|
<BadgeText>
|
||||||
|
<FontAwesome5 name="star" /> {userTest.score}
|
||||||
|
</BadgeText>
|
||||||
|
</Badge>
|
||||||
|
</View>
|
||||||
|
<Divider className="mt-2 mb-2" />
|
||||||
|
|
||||||
|
<ThemedText style={styles.testTitle}>{title}</ThemedText>
|
||||||
|
|
||||||
|
<View className="flex-row flex-wrap">
|
||||||
|
<View className="mr-6">
|
||||||
|
<View>
|
||||||
|
<ThemedText type="meta">Started at:</ThemedText>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<ThemedText>{formatISODate(userTest.created_at)}</ThemedText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<View>
|
||||||
|
<ThemedText type="meta">Closed at:</ThemedText>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<ThemedText>{formatISODate(userTest.closed_at)}</ThemedText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Divider className="mt-2 mb-2" />
|
||||||
|
|
||||||
|
<View className="justify-between flex-row items-center">
|
||||||
|
<ThemedText type="meta">
|
||||||
|
{userTest.answers.length} Questions
|
||||||
|
</ThemedText>
|
||||||
|
|
||||||
|
<ThemedText type="meta">
|
||||||
|
{successAnswers} / {userTest.answers.length} Correct
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
</Panel>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
testTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "600",
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UserTest;
|
||||||
20
utils/get-user-test-status.ts
Normal file
20
utils/get-user-test-status.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { UserTestResponse } from "@/api/types";
|
||||||
|
|
||||||
|
export enum TestStatus {
|
||||||
|
Completed = "Completed",
|
||||||
|
Expired = "Expired",
|
||||||
|
InProgress = "In Progress"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserTestStatus = (userTest: UserTestResponse): TestStatus => {
|
||||||
|
let status: TestStatus = TestStatus.Completed;
|
||||||
|
|
||||||
|
if(!userTest.is_available && !userTest.is_completed)
|
||||||
|
return TestStatus.Expired;
|
||||||
|
|
||||||
|
if(!userTest.is_completed)
|
||||||
|
return TestStatus.InProgress;
|
||||||
|
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
9
utils/get-user-test-title.ts
Normal file
9
utils/get-user-test-title.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { UserTestResponse } from "@/api/types";
|
||||||
|
|
||||||
|
const getUserTestTitle = (userTest: UserTestResponse): string => {
|
||||||
|
return userTest.test_id !== null
|
||||||
|
? `Test #${userTest.id}`
|
||||||
|
: `Random Test #${userTest.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getUserTestTitle;
|
||||||
Loading…
x
Reference in New Issue
Block a user