diff --git a/package.json b/package.json
index 0b92e7ff..b8266c65 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"formidable": "^3.5.0",
"formidable-serverless": "^1.1.1",
"framer-motion": "^9.0.2",
+ "howler": "^2.2.4",
"iron-session": "^6.3.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
@@ -73,6 +74,7 @@
},
"devDependencies": {
"@types/formidable": "^3.4.0",
+ "@types/howler": "^2.2.11",
"@types/lodash": "^4.14.191",
"@types/nodemailer": "^6.4.11",
"@types/nodemailer-express-handlebars": "^4.0.3",
diff --git a/public/audio/check.mp3 b/public/audio/check.mp3
new file mode 100644
index 00000000..dde3b8af
Binary files /dev/null and b/public/audio/check.mp3 differ
diff --git a/src/pages/(generation)/ListeningGeneration.tsx b/src/pages/(generation)/ListeningGeneration.tsx
index e0a6b012..88a990c6 100644
--- a/src/pages/(generation)/ListeningGeneration.tsx
+++ b/src/pages/(generation)/ListeningGeneration.tsx
@@ -1,9 +1,13 @@
import Input from "@/components/Low/Input";
-import {ListeningPart} from "@/interfaces/exam";
+import {Exercise, ListeningExam} from "@/interfaces/exam";
+import useExamStore from "@/stores/examStore";
+import {getExamById} from "@/utils/exams";
+import {checkSound} from "@/utils/sound";
import {convertCamelCaseToReadable} from "@/utils/string";
import {Tab} from "@headlessui/react";
import axios from "axios";
import clsx from "clsx";
+import {useRouter} from "next/router";
import {useState} from "react";
import {BsArrowRepeat} from "react-icons/bs";
import {toast} from "react-toastify";
@@ -20,8 +24,11 @@ const PartTab = ({part, types, index, setPart}: {part?: ListeningPart; types: st
setPart(undefined);
setIsLoading(true);
axios
- .get(`/api/exam/listening/generate/listening_passage_${index}${topic || types ? `?${url.toString()}` : ""}`)
- .then((result) => setPart(result.data))
+ .get(`/api/exam/listening/generate/listening_section_${index}${topic || types ? `?${url.toString()}` : ""}`)
+ .then((result) => {
+ checkSound();
+ setPart(result.data);
+ })
.catch((error) => {
console.log(error);
toast.error("Something went wrong!");
@@ -67,16 +74,44 @@ const PartTab = ({part, types, index, setPart}: {part?: ListeningPart; types: st
))}
+ {typeof part.text === "string" && {part.text.replaceAll("\n\n", " ")}}
+ {typeof part.text !== "string" && (
+
+ {part.text.conversation.map((x, index) => (
+
+ {x.name}:
+ {x.text.replaceAll("\n\n", " ")}
+
+ ))}
+
+ )}
)}
);
};
+interface ListeningPart {
+ exercises: Exercise[];
+ text:
+ | {
+ conversation: {
+ gender: string;
+ name: string;
+ text: string;
+ voice: string;
+ }[];
+ }
+ | string;
+}
+
const ListeningGeneration = () => {
const [part1, setPart1] = useState();
const [part2, setPart2] = useState();
const [part3, setPart3] = useState();
+ const [part4, setPart4] = useState();
+ const [isLoading, setIsLoading] = useState(false);
+ const [resultingExam, setResultingExam] = useState();
const [types, setTypes] = useState([]);
const availableTypes = [
@@ -86,8 +121,47 @@ const ListeningGeneration = () => {
{type: "writeBlanks", label: "Write the Blanks"},
];
+ const router = useRouter();
+
+ const setExams = useExamStore((state) => state.setExams);
+ const setSelectedModules = useExamStore((state) => state.setSelectedModules);
+
const toggleType = (type: string) => setTypes((prev) => (prev.includes(type) ? [...prev.filter((x) => x !== type)] : [...prev, type]));
+ const submitExam = () => {
+ if (!part1 || !part2 || !part3 || !part4) return toast.error("Please generate all for sections!");
+
+ setIsLoading(true);
+
+ axios
+ .post(`/api/exam/listening/generate/listening`, {parts: [part1, part2, part3, part4]})
+ .then((result) => {
+ checkSound();
+ setResultingExam(result.data);
+ })
+ .catch((error) => {
+ console.log(error);
+ toast.error("Something went wrong!");
+ })
+ .finally(() => setIsLoading(false));
+ };
+
+ const loadExam = async (examId: string) => {
+ const exam = await getExamById("listening", examId.trim());
+ if (!exam) {
+ toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", {
+ toastId: "invalid-exam-id",
+ });
+
+ return;
+ }
+
+ setExams([exam]);
+ setSelectedModules(["listening"]);
+
+ router.push("/exercises");
+ };
+
return (
<>
@@ -121,7 +195,7 @@ const ListeningGeneration = () => {
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
)
}>
- Passage 1
+ Section 1
@@ -132,7 +206,7 @@ const ListeningGeneration = () => {
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
)
}>
- Passage 2
+ Section 2
@@ -143,7 +217,18 @@ const ListeningGeneration = () => {
selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
)
}>
- Passage 3
+ Section 3
+
+
+ clsx(
+ "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-listening/70",
+ "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-listening focus:outline-none focus:ring-2",
+ "transition duration-300 ease-in-out",
+ selected ? "bg-white shadow" : "text-blue-100 hover:bg-white/[0.12] hover:text-ielts-listening",
+ )
+ }>
+ Section 4
@@ -151,22 +236,44 @@ const ListeningGeneration = () => {
{part: part1, setPart: setPart1},
{part: part2, setPart: setPart2},
{part: part3, setPart: setPart3},
+ {part: part4, setPart: setPart4},
].map(({part, setPart}, index) => (
))}
-
+
+ {resultingExam && (
+
+ )}
+
+
>
);
};
diff --git a/src/pages/api/exam/[module]/generate/[endpoint].ts b/src/pages/api/exam/[module]/generate/[endpoint].ts
index 1a6b4118..a84a3933 100644
--- a/src/pages/api/exam/[module]/generate/[endpoint].ts
+++ b/src/pages/api/exam/[module]/generate/[endpoint].ts
@@ -15,15 +15,15 @@ const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
- if (!req.session.user) {
- res.status(401).json({ok: false});
- return;
- }
+ if (req.method === "GET") return get(req, res);
+ if (req.method === "POST") return post(req, res);
- if (req.session.user.type !== "developer") {
- res.status(403).json({ok: false});
- return;
- }
+ return res.status(404).json({ok: false});
+}
+
+async function get(req: NextApiRequest, res: NextApiResponse) {
+ if (!req.session.user) return res.status(401).json({ok: false});
+ if (req.session.user.type !== "developer") return res.status(403).json({ok: false});
const {endpoint, topic, exercises} = req.query as {module: Module; endpoint: string; topic?: string; exercises?: string[]};
const url = `${process.env.BACKEND_URL}/${endpoint}`;
@@ -34,3 +34,21 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json(result.data);
}
+
+async function post(req: NextApiRequest, res: NextApiResponse) {
+ if (!req.session.user) return res.status(401).json({ok: false});
+ if (req.session.user.type !== "developer") return res.status(403).json({ok: false});
+
+ const {endpoint, topic, exercises} = req.query as {module: Module; endpoint: string; topic?: string; exercises?: string[]};
+ const url = `${process.env.BACKEND_URL}/${endpoint}`;
+
+ const result = await axios.post(
+ `${url}${topic && exercises ? `?topic=${topic.toLowerCase()}&exercises=${exercises.join("&exercises=")}` : ""}`,
+ req.body,
+ {
+ headers: {Authorization: `Bearer ${process.env.BACKEND_JWT}`},
+ },
+ );
+
+ res.status(200).json(result.data);
+}
diff --git a/src/utils/sound.ts b/src/utils/sound.ts
new file mode 100644
index 00000000..dcaae082
--- /dev/null
+++ b/src/utils/sound.ts
@@ -0,0 +1,9 @@
+import {Howl, Howler} from "howler";
+
+export const checkSound = () => {
+ const sound = new Howl({
+ src: ["audio/check.mp3"],
+ });
+
+ sound.play();
+};
diff --git a/yarn.lock b/yarn.lock
index 4d9d4192..6b4aa13c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1144,6 +1144,11 @@
"@types/minimatch" "^5.1.2"
"@types/node" "*"
+"@types/howler@^2.2.11":
+ version "2.2.11"
+ resolved "https://registry.yarnpkg.com/@types/howler/-/howler-2.2.11.tgz#a75c4ab5666aee5fcfbd5de15d35dbaaa3d3f070"
+ integrity sha512-7aBoUL6RbSIrqKnpEgfa1wSNUBK06mn08siP2QI0zYk7MXfEJAaORc4tohamQYqCqVESoDyRWSdQn2BOKWj2Qw==
+
"@types/http-assert@*":
version "1.5.3"
resolved "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.3.tgz"
@@ -3149,6 +3154,11 @@ hoist-non-react-statics@^3.3.1:
dependencies:
react-is "^16.7.0"
+howler@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/howler/-/howler-2.2.4.tgz#bd3df4a4f68a0118a51e4bd84a2bfc2e93e6e5a1"
+ integrity sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==
+
http-parser-js@>=0.5.1:
version "0.5.8"
resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz"