Added Random Form test and hooks for UserTest api
This commit is contained in:
parent
8ac1f6be34
commit
dad0063f44
@ -1 +1,2 @@
|
||||
EXPO_BACKEND_BASE_URL=
|
||||
EXPO_PUBLIC_BACKEND_BASE_URL=
|
||||
EXPO_PUBLIC_FRONTEND_REGISTER_URL=
|
||||
@ -101,3 +101,27 @@ export const post_login = async (email: string, password: string) => {
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const post_start_random_test = async (min_difficulty: number, max_difficulty: number, category_id: number) => {
|
||||
const response = await axiosInstance.post<{data: UserTestResponse}>(
|
||||
"/api/user-tests/",
|
||||
{
|
||||
category_id,
|
||||
min_difficulty,
|
||||
max_difficulty,
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export const post_start_test = async (test_id: number) => {
|
||||
const response = await axiosInstance.post<{ data: UserTestResponse }>(
|
||||
"/api/user-tests/by-test/",
|
||||
{
|
||||
test_id
|
||||
}
|
||||
)
|
||||
|
||||
return response.data;
|
||||
}
|
||||
@ -29,6 +29,9 @@ export const useLoginMutation = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["current-user"]
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["user-tests"]
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { get_my_user_tests, get_user_test } from "./_client";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
import { get_my_user_tests, get_user_test, post_start_random_test, post_start_test } from "./_client";
|
||||
|
||||
export const useMyTests = () => {
|
||||
return useQuery({
|
||||
@ -13,10 +14,50 @@ export const useMyTests = () => {
|
||||
|
||||
export const useUserTest = (id: number) => {
|
||||
return useQuery({
|
||||
queryKey: ['test', id],
|
||||
queryKey: ['user-test', id],
|
||||
queryFn: async () => {
|
||||
const response = await get_user_test(id);
|
||||
return response.data;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface RandomUserTestAttrs {
|
||||
min: number,
|
||||
max: number,
|
||||
category_id: number
|
||||
}
|
||||
export const useRandomUserTestMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<any, AxiosError, RandomUserTestAttrs>({
|
||||
mutationFn: async ({ min, max, category_id }: RandomUserTestAttrs) => {
|
||||
const response = await post_start_random_test(min, max, category_id);
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["user-tests"]
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface StartTestAttrs {
|
||||
test_id: number
|
||||
}
|
||||
export const useStartTestMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<any, AxiosError, StartTestAttrs>({
|
||||
mutationFn: async ({ test_id }: StartTestAttrs) => {
|
||||
const response = await post_start_test(test_id);
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["user-tests"]
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -2,23 +2,46 @@ import { Image } from "expo-image";
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
import { useQuestions } from "@/api";
|
||||
import { useRandomUserTestMutation } from "@/api/userTests";
|
||||
import { HelloWave } from "@/components/hello-wave";
|
||||
import ParallaxScrollView from "@/components/parallax-scroll-view";
|
||||
import useAuthContext from "@/components/providers/auth-provider/hook";
|
||||
import Question from "@/components/question";
|
||||
import { ThemedView } from "@/components/themed-view";
|
||||
import Panel from "@/components/ui/panel";
|
||||
import RandomTestForm from "@/components/ui/random-test-form";
|
||||
import { ThemedText } from "@/components/ui/themed-text";
|
||||
import getErrorAxiosMessage from "@/utils/get-error-axios-message";
|
||||
import { router } from "expo-router";
|
||||
import { useToast } from "react-native-toast-notifications";
|
||||
|
||||
export default function HomeScreen() {
|
||||
const { data: questions, isLoading: isLoadingQuestions } = useQuestions();
|
||||
const { user, isAuthorized } = useAuthContext();
|
||||
const { mutate, isPending } = useRandomUserTestMutation();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const username = isAuthorized ? user!.username : "Guest";
|
||||
|
||||
const questionsLoaded =
|
||||
!isLoadingQuestions && questions && questions.meta.total > 0;
|
||||
|
||||
const handleStartRandomTest = (min: number, max: number, catId: number) => {
|
||||
mutate({
|
||||
min,
|
||||
max,
|
||||
category_id: catId
|
||||
}, {
|
||||
onSuccess: (data) => {
|
||||
toast.show(`New user test created: ${data.id}` , { type: "success" })
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.show(getErrorAxiosMessage(error), { type: "danger" })
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ParallaxScrollView
|
||||
headerBackgroundColor={{ light: "#A1CEDC", dark: "#1D3D47" }}
|
||||
@ -34,6 +57,12 @@ export default function HomeScreen() {
|
||||
<HelloWave />
|
||||
</ThemedView>
|
||||
|
||||
<Panel>
|
||||
<ThemedText className="text-center mb-2" type="subtitle">Take a random test</ThemedText>
|
||||
|
||||
<RandomTestForm isLoading={isPending} onStartRandomTest={handleStartRandomTest} />
|
||||
</Panel>
|
||||
|
||||
<ThemedText type="subtitle">Questions</ThemedText>
|
||||
{questionsLoaded &&
|
||||
questions.data.map((question) => (
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import useAuthContext from "@/components/providers/auth-provider/hook";
|
||||
import { Button, ButtonText } from "@/components/ui/button";
|
||||
import Content from "@/components/ui/content";
|
||||
import { Divider } from "@/components/ui/divider";
|
||||
import LoginForm, { LoginFormData } from "@/components/ui/login-form";
|
||||
import Panel from "@/components/ui/panel";
|
||||
import { ThemedText } from "@/components/ui/themed-text";
|
||||
import getErrorAxiosMessage from "@/utils/get-error-axios-message";
|
||||
import { Stack } from "expo-router";
|
||||
import { Linking } from "react-native";
|
||||
import { useToast } from "react-native-toast-notifications";
|
||||
|
||||
const LoginScreen = () => {
|
||||
@ -28,8 +31,12 @@ const LoginScreen = () => {
|
||||
<Content>
|
||||
<Panel>
|
||||
<ThemedText type="subtitle" className="text-center mb-2">Login</ThemedText>
|
||||
<ThemedText className="text-center mb-2">Hello there</ThemedText>
|
||||
<ThemedText className="text-center mb-2">Welcome back! Please sign in to continue.</ThemedText>
|
||||
<LoginForm onSubmit={onLoginSubmit} isLoading={isPendingLogin} />
|
||||
<Divider className="mb-3 mt-3" />
|
||||
<Button variant="link" onPress={() => Linking.openURL(process.env.EXPO_PUBLIC_FRONTEND_REGISTER_URL as string)}>
|
||||
<ButtonText>Create a new account</ButtonText>
|
||||
</Button>
|
||||
</Panel>
|
||||
</Content>
|
||||
</>
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { useTest } from "@/api/tests";
|
||||
import { useStartTestMutation } from "@/api/userTests";
|
||||
import Question from "@/components/question";
|
||||
import Test from "@/components/test";
|
||||
import { Button, ButtonText } from "@/components/ui/button";
|
||||
import { ButtonText } from "@/components/ui/button";
|
||||
import Content from "@/components/ui/content";
|
||||
import CustomButton from "@/components/ui/custom-button";
|
||||
import getErrorAxiosMessage from "@/utils/get-error-axios-message";
|
||||
import { Stack, useLocalSearchParams } from "expo-router";
|
||||
import { View } from "react-native";
|
||||
import { useToast } from "react-native-toast-notifications";
|
||||
@ -11,6 +14,7 @@ const TestScreen = () => {
|
||||
const { id: idParam } = useLocalSearchParams<{ id: string }>();
|
||||
const id = +idParam;
|
||||
const { data, isLoading } = useTest(id);
|
||||
const { mutate, isPending } = useStartTestMutation();
|
||||
const toast = useToast();
|
||||
|
||||
if (!data)
|
||||
@ -22,7 +26,17 @@ const TestScreen = () => {
|
||||
);
|
||||
|
||||
const handleStartTest = () => {
|
||||
toast.show("Test started!", { type: "success" });
|
||||
mutate(
|
||||
{
|
||||
test_id: data.id,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {},
|
||||
onError: (error) => {
|
||||
toast.show(getErrorAxiosMessage(error), { type: "danger" });
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -33,9 +47,15 @@ const TestScreen = () => {
|
||||
<Test test={data} />
|
||||
</View>
|
||||
|
||||
{data.is_available && <Button className="mb-3" onPress={handleStartTest}>
|
||||
<ButtonText>Start Test</ButtonText>
|
||||
</Button>}
|
||||
{data.is_available && (
|
||||
<CustomButton
|
||||
className="mb-3"
|
||||
onPress={handleStartTest}
|
||||
isLoading={isPending}
|
||||
>
|
||||
<ButtonText>Start Test</ButtonText>
|
||||
</CustomButton>
|
||||
)}
|
||||
|
||||
<View className="gap-4">
|
||||
{data.questions?.map((question) => (
|
||||
|
||||
25
components/ui/custom-button.tsx
Normal file
25
components/ui/custom-button.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { ComponentProps } from "react";
|
||||
import { Button, ButtonSpinner } from "./button";
|
||||
|
||||
type CustomButtonAttrs = {
|
||||
isLoading?: boolean;
|
||||
} & ComponentProps<typeof Button>;
|
||||
|
||||
const CustomButton = ({
|
||||
isLoading = false,
|
||||
children,
|
||||
...rest
|
||||
}: CustomButtonAttrs) => {
|
||||
return (
|
||||
<Button
|
||||
className={isLoading ? "opacity-70" : ""}
|
||||
disabled={isLoading}
|
||||
{...rest}
|
||||
>
|
||||
{isLoading && <ButtonSpinner />}
|
||||
<>{children}</>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomButton;
|
||||
@ -2,7 +2,8 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from "react-hook-form";
|
||||
import { View } from "react-native";
|
||||
import { z } from 'zod';
|
||||
import { Button, ButtonSpinner, ButtonText } from "../button";
|
||||
import { ButtonText } from "../button";
|
||||
import CustomButton from '../custom-button';
|
||||
import FormInput from "../form-input";
|
||||
|
||||
export type LoginFormData = {
|
||||
@ -54,9 +55,9 @@ const LoginForm = ({ onSubmit, isLoading=false }: LoginFormProps) => {
|
||||
type: "password"
|
||||
}}
|
||||
/>
|
||||
<Button className={isLoading ? "opacity-70" : ""} disabled={isLoading} onPress={handleSubmit(onSubmitHandler)}>
|
||||
{isLoading && <ButtonSpinner />}<ButtonText>Sign In</ButtonText>
|
||||
</Button>
|
||||
<CustomButton isLoading={isLoading} onPress={handleSubmit(onSubmitHandler)}>
|
||||
<ButtonText>Sign In</ButtonText>
|
||||
</CustomButton>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
83
components/ui/random-test-form/index.tsx
Normal file
83
components/ui/random-test-form/index.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import useTaxonomyContext from "@/components/providers/taxonomy-provider/hook";
|
||||
import getCategoryOptions from "@/utils/get-category-options";
|
||||
import { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useToast } from "react-native-toast-notifications";
|
||||
import { ButtonText } from "../button";
|
||||
import CustomButton from "../custom-button";
|
||||
import CustomSelect from "../custom-select";
|
||||
import { ThemedText } from "../themed-text";
|
||||
|
||||
interface RandomTestFormProps {
|
||||
onStartRandomTest?: (min: number, max: number, catId: number) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const RandomTestForm = ({ onStartRandomTest, isLoading }: RandomTestFormProps) => {
|
||||
const [minDifficulty, setMinDifficulty] = useState(0);
|
||||
const [maxDifficulty, setMaxDifficulty] = useState(10);
|
||||
const [categoryId, setCategoryId] = useState<number|undefined>(undefined);
|
||||
const { categories } = useTaxonomyContext();
|
||||
const toast = useToast();
|
||||
|
||||
const catOptions = getCategoryOptions(categories);
|
||||
const minOptions = Array.from({ length: 9 }, (_, i) => {
|
||||
return { label: String(i), value: String(i) };
|
||||
});
|
||||
const maxOptions = Array.from({ length: 10 }, (_, i) => {
|
||||
return { label: String(i), value: String(i) };
|
||||
});
|
||||
|
||||
const handleStartRandomTest = () => {
|
||||
if(minDifficulty >= maxDifficulty) {
|
||||
toast.show("Minimum difficulty must be less than maximum difficulty.", { type: "danger" });
|
||||
return;
|
||||
}
|
||||
if(categoryId === undefined) {
|
||||
toast.show("Category must be selected", { type: "danger" });
|
||||
return;
|
||||
}
|
||||
onStartRandomTest?.(minDifficulty, maxDifficulty, categoryId);
|
||||
}
|
||||
const handleSelect = (setStateFunction: (v: number) => void, value: string) => {
|
||||
const valueNum = +value;
|
||||
setStateFunction(valueNum);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View className="flex-row gap-3 mb-3">
|
||||
<View className="flex-1">
|
||||
<ThemedText className="mb-1" type="defaultSemiBold">Min Difficulty</ThemedText>
|
||||
<CustomSelect
|
||||
options={minOptions}
|
||||
selectedValue={String(minDifficulty)}
|
||||
onValueChange={(value) => handleSelect(setMinDifficulty, value)}
|
||||
/>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<ThemedText className="mb-1" type="defaultSemiBold">Max Difficulty</ThemedText>
|
||||
<CustomSelect
|
||||
options={maxOptions}
|
||||
selectedValue={String(maxDifficulty)}
|
||||
onValueChange={(value) => handleSelect(setMaxDifficulty, value)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View className="mb-3">
|
||||
<ThemedText className="mb-1" type="defaultSemiBold">Category</ThemedText>
|
||||
<CustomSelect
|
||||
options={catOptions}
|
||||
placeholder="Select category"
|
||||
onValueChange={(value) => handleSelect(setCategoryId, value)}
|
||||
|
||||
/>
|
||||
</View>
|
||||
<CustomButton isLoading={isLoading} onPress={() => handleStartRandomTest()}>
|
||||
<ButtonText>Start test</ButtonText>
|
||||
</CustomButton>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default RandomTestForm;
|
||||
Loading…
x
Reference in New Issue
Block a user