diff --git a/src/.env b/.env
similarity index 100%
rename from src/.env
rename to .env
diff --git a/index.html b/index.html
index 68de6bf..515502d 100644
--- a/index.html
+++ b/index.html
@@ -10,7 +10,7 @@
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet"
/>
-
test-ai
+ HoshiAI
diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts
index 686a88b..6a38bbd 100644
--- a/src/api/axiosInstance.ts
+++ b/src/api/axiosInstance.ts
@@ -1,7 +1,9 @@
import axios from "axios";
+const baseURL = import.meta.env.VITE_API_URL;
+
const axiosInstance = axios.create({
- baseURL: "http://127.0.0.1:8000",
+ baseURL,
timeout: 30 * 1000,
});
diff --git a/src/api/questionsApi.ts b/src/api/questionsApi.ts
index b9649b1..e0a67f2 100644
--- a/src/api/questionsApi.ts
+++ b/src/api/questionsApi.ts
@@ -1,7 +1,5 @@
import axiosInstance from "./axiosInstance";
-
-
type GetQuestionsParams = {
page?: number;
id?: number;
diff --git a/src/api/testApi.ts b/src/api/testApi.ts
index 974696e..489d1e6 100644
--- a/src/api/testApi.ts
+++ b/src/api/testApi.ts
@@ -28,8 +28,8 @@ export const getUserTestById = async (userTestId: number) => {
};
export const getUserTests = async () => {
- const res = await axiosInstance.get("/api/user-tests/me");
- return res.data;
+ const res = await axiosInstance.get<{data: UserTestType[]}>("/api/user-tests/me");
+ return res.data.data;
};
export const submitAnswer = async (data: SubmitAnswerPayload) => {
diff --git a/src/components/Answers/Answers.tsx b/src/components/Answers/Answers.tsx
index 63f64d8..2c978f5 100644
--- a/src/components/Answers/Answers.tsx
+++ b/src/components/Answers/Answers.tsx
@@ -1,5 +1,6 @@
import { useState } from "react";
import { Box, Button, Typography } from "@mui/material";
+import { arraysEqual } from "../../utils/functions";
interface Variant {
id: number;
@@ -9,45 +10,99 @@ interface Variant {
interface LearningAnswersProps {
variants?: Variant[];
correctAnswers: number[] | string[];
- type: "single" | "text";
+ type: "single" | "multiple" | "text";
+ userAnswers?: number[];
}
const LearningAnswers = ({
variants = [],
correctAnswers = [],
type,
+ userAnswers,
}: LearningAnswersProps) => {
const [showAnswer, setShowAnswer] = useState(false);
+ const hasAnswered = userAnswers !== null && userAnswers !== undefined;
+ const isEmptyAnswer = hasAnswered && userAnswers.length === 0;
+ const isCorrect = hasAnswered
+ ? arraysEqual(correctAnswers as number[], userAnswers as number[])
+ : false;
+ const normalizedUserAnswers = userAnswers ?? [];
+
+ const getUserAnswerText = () => {
+ if (!normalizedUserAnswers.length) return "";
+ const selectedVariants = variants.filter((v) =>
+ normalizedUserAnswers.includes(v.id)
+ );
+ return selectedVariants.map((v) => v.text).join(", ");
+ };
+
if (type === "text") {
return (
- {showAnswer && (
+ {hasAnswered ? (
+ isEmptyAnswer ? (
+
+ You didn’t answer
+
+ ) : (
+
+ Your answer: {userAnswers.join(", ")}
+
+ )
+ ) : null}
+
+ {(hasAnswered || showAnswer) && (
- Correct answer:{" "}
- {Array.isArray(correctAnswers)
- ? correctAnswers.join(", ")
- : correctAnswers}
+ Correct answer: {correctAnswers.join(", ")}
)}
-
+
+ {!hasAnswered && (
+
+ )}
);
}
return (
+ {hasAnswered ? (
+ isEmptyAnswer ? (
+
+ You didn’t answer
+
+ ) : (
+
+ Your answer: {getUserAnswerText()} —{" "}
+ {isCorrect ? "Correct" : "Incorrect"}
+
+ )
+ ) : null}
+
{variants.map((variant) => (
@@ -66,14 +125,16 @@ const LearningAnswers = ({
))}
-
+ {!hasAnswered && (
+
+ )}
);
};
diff --git a/src/components/TestCard/TestCard.tsx b/src/components/TestCard/TestCard.tsx
new file mode 100644
index 0000000..7e4661c
--- /dev/null
+++ b/src/components/TestCard/TestCard.tsx
@@ -0,0 +1,80 @@
+import { Box, Button, Chip, Typography } from "@mui/material";
+import { Link } from "react-router-dom";
+import { formatDate } from "../../utils/functions";
+import type { UserTestType } from "../shared/types/TestTypes";
+
+interface TestCardProps {
+ test: UserTestType;
+}
+
+const TestCard = ({ test }: TestCardProps) => {
+ const title = test.test?.title || "User Test";
+
+ let statusLabel = "";
+ let statusColor: "primary" | "success" | "error" = "primary";
+
+ if (test.is_completed) {
+ statusLabel = "Completed";
+ statusColor = "success";
+ } else if (!test.is_available) {
+ statusLabel = "Expired";
+ statusColor = "error";
+ } else {
+ statusLabel = "Active";
+ statusColor = "primary";
+ }
+
+ return (
+
+
+ {title}
+ {test.closed_at && (
+
+ Expires at: {formatDate(test.closed_at)}
+
+ )}
+ {test.is_completed && test.score !== undefined && (
+
+ Score: {test.score}%
+
+ )}
+
+
+
+
+ {test.is_available && !test.is_completed && (
+
+ )}
+ {test.is_completed && (
+
+ )}
+
+
+ );
+};
+
+export default TestCard;
diff --git a/src/hooks/Tests/useCompleteTest.ts b/src/hooks/Tests/useCompleteTest.ts
index 940dc19..dd49599 100644
--- a/src/hooks/Tests/useCompleteTest.ts
+++ b/src/hooks/Tests/useCompleteTest.ts
@@ -1,12 +1,15 @@
-import { useMutation } from "@tanstack/react-query";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
import { completeUserTest } from "../../api/testApi";
import { toast } from "react-toastify";
export const useCompleteTest = () => {
+ const queryClient = useQueryClient();
return useMutation({
- mutationFn: completeUserTest,
+ mutationFn: completeUserTest,
onSuccess: (data) => {
toast.success(data.message);
+ queryClient.invalidateQueries({ queryKey: ["current-test"] });
+ queryClient.invalidateQueries({ queryKey: ["user-tests"] });
},
onError: (error: any) => {
toast.error(error.response?.data?.message);
diff --git a/src/hooks/Tests/useGetUserTests.ts b/src/hooks/Tests/useGetUserTests.ts
index 7371ce3..417e8db 100644
--- a/src/hooks/Tests/useGetUserTests.ts
+++ b/src/hooks/Tests/useGetUserTests.ts
@@ -4,6 +4,6 @@ import { getUserTests } from "../../api/testApi";
export const useGetUserTests = () => {
return useQuery({
queryKey: ["user-tests"],
- queryFn: () => getUserTests,
+ queryFn: () => getUserTests(),
});
};
diff --git a/src/hooks/Tests/useStartTest.ts b/src/hooks/Tests/useStartTest.ts
index a534e89..bfd7457 100644
--- a/src/hooks/Tests/useStartTest.ts
+++ b/src/hooks/Tests/useStartTest.ts
@@ -1,13 +1,15 @@
-import { useMutation } from "@tanstack/react-query";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
import { startTest } from "../../api/testApi";
import { toast } from "react-toastify";
import { useNavigate } from "react-router-dom";
export const useStartTest = () => {
+ const queryClient = useQueryClient();
const navigate = useNavigate();
return useMutation({
mutationFn: startTest,
onSuccess: (data) => {
+ queryClient.invalidateQueries({ queryKey: ["user-tests"] });
toast.success("Test Started");
navigate(`/tests/${data.id}`);
},
diff --git a/src/hooks/Tests/useSubmitAnswer.ts b/src/hooks/Tests/useSubmitAnswer.ts
index 1410811..ed585f0 100644
--- a/src/hooks/Tests/useSubmitAnswer.ts
+++ b/src/hooks/Tests/useSubmitAnswer.ts
@@ -1,13 +1,14 @@
-import { useMutation } from "@tanstack/react-query";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
import { submitAnswer } from "../../api/testApi";
import { toast } from "react-toastify";
export const useSubmitAnswer = () => {
+ const queryClient = useQueryClient();
return useMutation({
mutationFn: submitAnswer,
onSuccess: (data) => {
toast.success(data.message);
- console.log(data);
+ queryClient.invalidateQueries({ queryKey: ["current-test"] });
},
onError: (error: any) => {
toast.error(error.response?.data?.message || "Something went wrong");
diff --git a/src/pages/IndexPage/IndexPage.tsx b/src/pages/IndexPage/IndexPage.tsx
index 8bc075b..616ca3d 100644
--- a/src/pages/IndexPage/IndexPage.tsx
+++ b/src/pages/IndexPage/IndexPage.tsx
@@ -34,7 +34,7 @@ const IndexPage = () => {
{error && {error}}
{questions.map((q: QuestionType) => (
-
+
))}
{meta && meta.last_page > 1 && (
diff --git a/src/pages/ProfilePage/Profilepage.tsx b/src/pages/ProfilePage/Profilepage.tsx
index deb835f..cd7436a 100644
--- a/src/pages/ProfilePage/Profilepage.tsx
+++ b/src/pages/ProfilePage/Profilepage.tsx
@@ -1,11 +1,25 @@
-import { Box, Button, Typography } from "@mui/material";
+import { Box, Button, Tab, Tabs, Typography } from "@mui/material";
import { useAuth } from "../../context/AuthContext";
import Container from "../../components/shared/Container";
import { useNavigate } from "react-router-dom";
+import { useGetUserTests } from "../../hooks/Tests/useGetUserTests";
+import TestCard from "../../components/TestCard/TestCard";
+import { useState } from "react";
const ProfilePage = () => {
const { user, logout } = useAuth();
const navigate = useNavigate();
+ const { data: tests, isLoading, error } = useGetUserTests();
+
+ const [tabIndex, setTabIndex] = useState(0);
+
+ const activeTests = tests?.filter((t) => t.is_available && !t.is_completed);
+ const completedTests = tests?.filter((t) => t.is_completed);
+ const expiredTests = tests?.filter((t) => !t.is_available && !t.is_completed);
+
+ const handleTabChange = (_: React.SyntheticEvent, newValue: number) => {
+ setTabIndex(newValue);
+ };
return (
@@ -27,7 +41,7 @@ const ProfilePage = () => {
{user?.type === "admin" && (
)}
+
+
+ Your Tests
+
+
+
+
+
+
+
+
+
+ {isLoading && Loading tests...}
+ {error && (
+ Failed to load tests.
+ )}
+
+ {tabIndex === 0 &&
+ (activeTests?.length ? (
+ activeTests.map((t) => )
+ ) : (
+ No active tests
+ ))}
+ {tabIndex === 1 &&
+ (completedTests?.length ? (
+ completedTests.map((t) => )
+ ) : (
+ No completed tests
+ ))}
+ {tabIndex === 2 &&
+ (expiredTests?.length ? (
+ expiredTests.map((t) => )
+ ) : (
+ No expired tests
+ ))}
+
+
);
};
diff --git a/src/pages/TestPage/TestPage.tsx b/src/pages/TestPage/TestPage.tsx
index cc32106..060bc4a 100644
--- a/src/pages/TestPage/TestPage.tsx
+++ b/src/pages/TestPage/TestPage.tsx
@@ -6,6 +6,8 @@ import { Box, Button, Pagination, Typography } from "@mui/material";
import TestQuestion from "../../components/TestQuestion/TestQuestion";
import { useSubmitAnswer } from "../../hooks/Tests/useSubmitAnswer";
import { useCompleteTest } from "../../hooks/Tests/useCompleteTest";
+import LearningAnswers from "../../components/Answers/Answers";
+import { formatDate } from "../../utils/functions";
export const TestPage = () => {
const { id } = useParams();
@@ -13,6 +15,7 @@ export const TestPage = () => {
const [currentQuestion, setCurrentQuestion] = useState(1);
const submitAnswerMutation = useSubmitAnswer();
const completeTestMutation = useCompleteTest();
+ const allAnswered = test?.answers?.every((ans) => ans.answer !== null);
const handleCompleteTest = () => {
if (test) completeTestMutation.mutate(test.id);
@@ -54,15 +57,34 @@ export const TestPage = () => {
);
- if (test.is_completed)
+ if (test.is_completed) {
return (
-
- You have already completed this test.
+
+ Results
+
+ Your score: {test.score}%
+
+
+ {test?.answers?.map((ans) => (
+
+ {ans.question.title}
+
+ {ans.question.description}
+
+
+
+
+ ))}
);
-
+ }
const questionsAndAnswers = test.answers;
const currentQA = questionsAndAnswers?.[currentQuestion - 1];
@@ -78,17 +100,7 @@ export const TestPage = () => {
>
{test.test?.title ?? "User Test"}
- Expires at:{" "}
- {test.closed_at
- ? new Date(test.closed_at).toLocaleString("en-GB", {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- hour12: false,
- })
- : "N/A"}
+ Expires at: {test.closed_at ? formatDate(test.closed_at) : "N/A"}
{currentQA ? (
@@ -120,16 +132,27 @@ export const TestPage = () => {
page={currentQuestion}
onChange={(_, page) => setCurrentQuestion(page)}
/>
+
{currentQuestion == questionsAndAnswers?.length && (
)}
+ {currentQuestion == questionsAndAnswers?.length && !allAnswered && (
+
+ You must answer all questions before completing the test
+
+ )}
);
diff --git a/src/utils/functions.ts b/src/utils/functions.ts
new file mode 100644
index 0000000..fdec8ba
--- /dev/null
+++ b/src/utils/functions.ts
@@ -0,0 +1,29 @@
+export function arraysEqual(a: number[], b: number[]): boolean {
+ if (a === b) return true;
+ if (!a || !b) return false;
+ if (a.length !== b.length) return false;
+
+ a = [...a].sort((x, y) => x - y);
+ b = [...b].sort((x, y) => x - y);
+
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) return false;
+ }
+ return true;
+}
+
+export const formatDate = (dateString: string | undefined) => {
+ if (!dateString) return "N/A";
+
+ const date = new Date(dateString);
+
+ const pad = (num: number) => num.toString().padStart(2, "0");
+
+ const day = pad(date.getDate());
+ const month = pad(date.getMonth() + 1);
+ const year = date.getFullYear();
+ const hours = pad(date.getHours());
+ const minutes = pad(date.getMinutes());
+
+ return `${day}/${month}/${year} ${hours}:${minutes}`;
+};