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;
|
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({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["current-user"]
|
queryKey: ["current-user"]
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["user-tests"]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { get_my_user_tests, get_user_test } from "./_client";
|
import { AxiosError } from "axios";
|
||||||
|
import { get_my_user_tests, get_user_test, post_start_random_test, post_start_test } from "./_client";
|
||||||
|
|
||||||
export const useMyTests = () => {
|
export const useMyTests = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@ -13,10 +14,50 @@ export const useMyTests = () => {
|
|||||||
|
|
||||||
export const useUserTest = (id: number) => {
|
export const useUserTest = (id: number) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['test', id],
|
queryKey: ['user-test', id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await get_user_test(id);
|
const response = await get_user_test(id);
|
||||||
return response.data;
|
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 { StyleSheet } from "react-native";
|
||||||
|
|
||||||
import { useQuestions } from "@/api";
|
import { useQuestions } from "@/api";
|
||||||
|
import { useRandomUserTestMutation } from "@/api/userTests";
|
||||||
import { HelloWave } from "@/components/hello-wave";
|
import { HelloWave } from "@/components/hello-wave";
|
||||||
import ParallaxScrollView from "@/components/parallax-scroll-view";
|
import ParallaxScrollView from "@/components/parallax-scroll-view";
|
||||||
import useAuthContext from "@/components/providers/auth-provider/hook";
|
import useAuthContext from "@/components/providers/auth-provider/hook";
|
||||||
import Question from "@/components/question";
|
import Question from "@/components/question";
|
||||||
import { ThemedView } from "@/components/themed-view";
|
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 { ThemedText } from "@/components/ui/themed-text";
|
||||||
|
import getErrorAxiosMessage from "@/utils/get-error-axios-message";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
|
import { useToast } from "react-native-toast-notifications";
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { data: questions, isLoading: isLoadingQuestions } = useQuestions();
|
const { data: questions, isLoading: isLoadingQuestions } = useQuestions();
|
||||||
const { user, isAuthorized } = useAuthContext();
|
const { user, isAuthorized } = useAuthContext();
|
||||||
|
const { mutate, isPending } = useRandomUserTestMutation();
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
const username = isAuthorized ? user!.username : "Guest";
|
const username = isAuthorized ? user!.username : "Guest";
|
||||||
|
|
||||||
const questionsLoaded =
|
const questionsLoaded =
|
||||||
!isLoadingQuestions && questions && questions.meta.total > 0;
|
!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 (
|
return (
|
||||||
<ParallaxScrollView
|
<ParallaxScrollView
|
||||||
headerBackgroundColor={{ light: "#A1CEDC", dark: "#1D3D47" }}
|
headerBackgroundColor={{ light: "#A1CEDC", dark: "#1D3D47" }}
|
||||||
@ -34,6 +57,12 @@ export default function HomeScreen() {
|
|||||||
<HelloWave />
|
<HelloWave />
|
||||||
</ThemedView>
|
</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>
|
<ThemedText type="subtitle">Questions</ThemedText>
|
||||||
{questionsLoaded &&
|
{questionsLoaded &&
|
||||||
questions.data.map((question) => (
|
questions.data.map((question) => (
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import useAuthContext from "@/components/providers/auth-provider/hook";
|
import useAuthContext from "@/components/providers/auth-provider/hook";
|
||||||
|
import { Button, ButtonText } from "@/components/ui/button";
|
||||||
import Content from "@/components/ui/content";
|
import Content from "@/components/ui/content";
|
||||||
|
import { Divider } from "@/components/ui/divider";
|
||||||
import LoginForm, { LoginFormData } from "@/components/ui/login-form";
|
import LoginForm, { LoginFormData } from "@/components/ui/login-form";
|
||||||
import Panel from "@/components/ui/panel";
|
import Panel from "@/components/ui/panel";
|
||||||
import { ThemedText } from "@/components/ui/themed-text";
|
import { ThemedText } from "@/components/ui/themed-text";
|
||||||
import getErrorAxiosMessage from "@/utils/get-error-axios-message";
|
import getErrorAxiosMessage from "@/utils/get-error-axios-message";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
|
import { Linking } from "react-native";
|
||||||
import { useToast } from "react-native-toast-notifications";
|
import { useToast } from "react-native-toast-notifications";
|
||||||
|
|
||||||
const LoginScreen = () => {
|
const LoginScreen = () => {
|
||||||
@ -28,8 +31,12 @@ const LoginScreen = () => {
|
|||||||
<Content>
|
<Content>
|
||||||
<Panel>
|
<Panel>
|
||||||
<ThemedText type="subtitle" className="text-center mb-2">Login</ThemedText>
|
<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} />
|
<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>
|
</Panel>
|
||||||
</Content>
|
</Content>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import { useTest } from "@/api/tests";
|
import { useTest } from "@/api/tests";
|
||||||
|
import { useStartTestMutation } from "@/api/userTests";
|
||||||
import Question from "@/components/question";
|
import Question from "@/components/question";
|
||||||
import Test from "@/components/test";
|
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 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 { Stack, useLocalSearchParams } from "expo-router";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useToast } from "react-native-toast-notifications";
|
import { useToast } from "react-native-toast-notifications";
|
||||||
@ -11,6 +14,7 @@ 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 { mutate, isPending } = useStartTestMutation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
if (!data)
|
if (!data)
|
||||||
@ -22,7 +26,17 @@ const TestScreen = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleStartTest = () => {
|
const handleStartTest = () => {
|
||||||
toast.show("Test started!", { type: "success" });
|
mutate(
|
||||||
|
{
|
||||||
|
test_id: data.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (data) => {},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.show(getErrorAxiosMessage(error), { type: "danger" });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -33,9 +47,15 @@ const TestScreen = () => {
|
|||||||
<Test test={data} />
|
<Test test={data} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{data.is_available && <Button className="mb-3" onPress={handleStartTest}>
|
{data.is_available && (
|
||||||
<ButtonText>Start Test</ButtonText>
|
<CustomButton
|
||||||
</Button>}
|
className="mb-3"
|
||||||
|
onPress={handleStartTest}
|
||||||
|
isLoading={isPending}
|
||||||
|
>
|
||||||
|
<ButtonText>Start Test</ButtonText>
|
||||||
|
</CustomButton>
|
||||||
|
)}
|
||||||
|
|
||||||
<View className="gap-4">
|
<View className="gap-4">
|
||||||
{data.questions?.map((question) => (
|
{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 { useForm } from "react-hook-form";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Button, ButtonSpinner, ButtonText } from "../button";
|
import { ButtonText } from "../button";
|
||||||
|
import CustomButton from '../custom-button';
|
||||||
import FormInput from "../form-input";
|
import FormInput from "../form-input";
|
||||||
|
|
||||||
export type LoginFormData = {
|
export type LoginFormData = {
|
||||||
@ -54,9 +55,9 @@ const LoginForm = ({ onSubmit, isLoading=false }: LoginFormProps) => {
|
|||||||
type: "password"
|
type: "password"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button className={isLoading ? "opacity-70" : ""} disabled={isLoading} onPress={handleSubmit(onSubmitHandler)}>
|
<CustomButton isLoading={isLoading} onPress={handleSubmit(onSubmitHandler)}>
|
||||||
{isLoading && <ButtonSpinner />}<ButtonText>Sign In</ButtonText>
|
<ButtonText>Sign In</ButtonText>
|
||||||
</Button>
|
</CustomButton>
|
||||||
</View>
|
</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