Added the speaking module to the application
This commit is contained in:
53
src/components/Exercises/Speaking.tsx
Normal file
53
src/components/Exercises/Speaking.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
||||
import {SpeakingExercise, WritingExercise} from "@/interfaces/exam";
|
||||
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import clsx from "clsx";
|
||||
import {CommonProps} from ".";
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import {toast} from "react-toastify";
|
||||
|
||||
export default function Speaking({id, title, text, prompts, onNext, onBack}: SpeakingExercise & CommonProps) {
|
||||
return (
|
||||
<div className="flex flex-col h-full w-2/3 items-center justify-center gap-8">
|
||||
<div className="flex flex-col max-w-2xl gap-4">
|
||||
<span className="font-bold">{title}</span>
|
||||
<span className="font-regular ml-8">
|
||||
{text.split("\\n").map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
<span>{line}</span>
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
<div className="flex gap-8">
|
||||
<span>You should talk about the following things:</span>
|
||||
<div className="flex flex-col gap-4">
|
||||
{prompts.map((x, index) => (
|
||||
<li className="italic" key={index}>
|
||||
{x}
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="self-end flex gap-8">
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white", errorButtonStyle)} onClick={onBack}>
|
||||
<div className="absolute left-4">
|
||||
<Icon path={mdiArrowLeft} color="white" size={1} />
|
||||
</div>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
className={clsx("btn btn-wide gap-4 relative text-white", infoButtonStyle)}
|
||||
onClick={() => onNext({id, solutions: [], score: {correct: 0, total: 0}})}>
|
||||
Next
|
||||
<div className="absolute right-4">
|
||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {CommonProps} from ".";
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import {toast} from "react-toastify";
|
||||
|
||||
export default function WriteBlanks({id, prompt, info, wordCounter, onNext, onBack}: WritingExercise & CommonProps) {
|
||||
export default function Writing({id, prompt, info, wordCounter, onNext, onBack}: WritingExercise & CommonProps) {
|
||||
const [inputText, setInputText] = useState("");
|
||||
const [isSubmitEnabled, setIsSubmitEnabled] = useState(false);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
FillBlanksExercise,
|
||||
MatchSentencesExercise,
|
||||
MultipleChoiceExercise,
|
||||
SpeakingExercise,
|
||||
UserSolution,
|
||||
WriteBlanksExercise,
|
||||
WritingExercise,
|
||||
@@ -12,6 +13,7 @@ import FillBlanks from "./FillBlanks";
|
||||
import MultipleChoice from "./MultipleChoice";
|
||||
import WriteBlanks from "./WriteBlanks";
|
||||
import Writing from "./Writing";
|
||||
import Speaking from "./Speaking";
|
||||
|
||||
const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), {ssr: false});
|
||||
|
||||
@@ -32,5 +34,7 @@ export const renderExercise = (exercise: Exercise, onNext: (userSolutions: UserS
|
||||
return <WriteBlanks {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
||||
case "writing":
|
||||
return <Writing {...(exercise as WritingExercise)} onNext={onNext} onBack={onBack} />;
|
||||
case "speaking":
|
||||
return <Speaking {...(exercise as SpeakingExercise)} onNext={onNext} onBack={onBack} />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,19 +56,6 @@ export default function Selection({user, onStart}: Props) {
|
||||
<Icon path={mdiHeadphones} color="white" size={3} />
|
||||
<span>Listening</span>
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggleModule("speaking")}
|
||||
className={clsx(
|
||||
"flex flex-col gap-2 items-center justify-center",
|
||||
"border-ielts-speaking hover:bg-ielts-speaking text-white",
|
||||
"border-2 rounded-xl p-4 h-fit w-48 cursor-pointer",
|
||||
selectedModules.includes("speaking") ? "bg-ielts-speaking " : "bg-ielts-speaking-transparent ",
|
||||
)}>
|
||||
<Icon path={mdiAccountVoice} color="white" size={3} />
|
||||
<span>Speaking</span>
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -82,6 +69,19 @@ export default function Selection({user, onStart}: Props) {
|
||||
<Icon path={mdiPen} color="white" size={3} />
|
||||
<span>Writing</span>
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggleModule("speaking")}
|
||||
className={clsx(
|
||||
"flex flex-col gap-2 items-center justify-center",
|
||||
"border-ielts-speaking hover:bg-ielts-speaking text-white",
|
||||
"border-2 rounded-xl p-4 h-fit w-48 cursor-pointer",
|
||||
selectedModules.includes("speaking") ? "bg-ielts-speaking " : "bg-ielts-speaking-transparent ",
|
||||
)}>
|
||||
<Icon path={mdiAccountVoice} color="white" size={3} />
|
||||
<span>Speaking</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex justify-between">
|
||||
<button onClick={() => router.push("/")} className={clsx("btn btn-wide gap-4 relative text-white", errorButtonStyle)}>
|
||||
|
||||
79
src/exams/Speaking.tsx
Normal file
79
src/exams/Speaking.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import {renderExercise} from "@/components/Exercises";
|
||||
import {renderSolution} from "@/components/Solutions";
|
||||
import {infoButtonStyle} from "@/constants/buttonStyles";
|
||||
import {UserSolution, SpeakingExam} from "@/interfaces/exam";
|
||||
import {mdiArrowRight} from "@mdi/js";
|
||||
import Icon from "@mdi/react";
|
||||
import clsx from "clsx";
|
||||
import {Fragment, useEffect, useState} from "react";
|
||||
import {toast} from "react-toastify";
|
||||
|
||||
interface Props {
|
||||
exam: SpeakingExam;
|
||||
showSolutions?: boolean;
|
||||
onFinish: (userSolutions: UserSolution[]) => void;
|
||||
}
|
||||
|
||||
export default function Speaking({exam, showSolutions = false, onFinish}: Props) {
|
||||
const [exerciseIndex, setExerciseIndex] = useState(0);
|
||||
const [timer, setTimer] = useState<number>();
|
||||
const [userSolutions, setUserSolutions] = useState<UserSolution[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimer(exam.minTimer * 60);
|
||||
const timerInterval = setInterval(() => setTimer((prev) => prev && prev - 1), 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timerInterval);
|
||||
};
|
||||
}, [exam.minTimer]);
|
||||
|
||||
const nextExercise = (solution?: UserSolution) => {
|
||||
if (solution) {
|
||||
setUserSolutions((prev) => [...prev.filter((x) => x.id !== solution.id), solution]);
|
||||
}
|
||||
|
||||
if (exerciseIndex + 1 < exam.exercises.length) {
|
||||
setExerciseIndex((prev) => prev + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (solution) {
|
||||
onFinish([...userSolutions.filter((x) => x.id !== solution.id), solution].map((x) => ({...x, module: "speaking", exam: exam.id})));
|
||||
} else {
|
||||
onFinish(userSolutions.map((x) => ({...x, module: "speaking", exam: exam.id})));
|
||||
}
|
||||
};
|
||||
|
||||
const previousExercise = () => {
|
||||
setExerciseIndex((prev) => prev - 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full relative flex flex-col gap-8 items-center justify-center p-8 px-16 overflow-hidden">
|
||||
{timer && (
|
||||
<div className="absolute w-24 top-8 text-center right-8 font-semibold text-lg border-ielts-speaking border-2 p-2 rounded-xl bg-ielts-speaking-transparent text-white">
|
||||
{Math.floor(timer / 60) < 10 ? "0" : ""}
|
||||
{Math.floor(timer / 60)}:{timer % 60 < 10 ? "0" : ""}
|
||||
{timer % 60}
|
||||
</div>
|
||||
)}
|
||||
{exerciseIndex > -1 &&
|
||||
exerciseIndex < exam.exercises.length &&
|
||||
!showSolutions &&
|
||||
renderExercise(exam.exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||
{exerciseIndex > -1 &&
|
||||
exerciseIndex < exam.exercises.length &&
|
||||
showSolutions &&
|
||||
renderSolution(exam.exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||
{exerciseIndex === -1 && (
|
||||
<button className={clsx("btn btn-wide gap-4 relative text-white self-end", infoButtonStyle)} onClick={() => nextExercise()}>
|
||||
Next
|
||||
<div className="absolute right-4">
|
||||
<Icon path={mdiArrowRight} color="white" size={1} />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,6 @@ export default function useUser({redirectTo = "", redirectIfFound = false} = {})
|
||||
const {data: user, mutate: mutateUser, isLoading} = useSWR<User>("/api/user", fetcher);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("HERE!!!");
|
||||
// if no redirect needed, just return (example: already on /dashboard)
|
||||
// if user data not yet there (fetch in progress, logged in or not) then don't do anything yet
|
||||
if (!redirectTo || !user) return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Module} from ".";
|
||||
|
||||
export type Exam = ReadingExam | ListeningExam | WritingExam;
|
||||
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam;
|
||||
|
||||
export interface ReadingExam {
|
||||
text: {
|
||||
@@ -47,7 +47,20 @@ interface WordCounter {
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export type Exercise = FillBlanksExercise | MatchSentencesExercise | MultipleChoiceExercise | WriteBlanksExercise | WritingExercise;
|
||||
export interface SpeakingExam {
|
||||
id: string;
|
||||
module: "speaking";
|
||||
exercises: Exercise[];
|
||||
minTimer: number;
|
||||
}
|
||||
|
||||
export type Exercise =
|
||||
| FillBlanksExercise
|
||||
| MatchSentencesExercise
|
||||
| MultipleChoiceExercise
|
||||
| WriteBlanksExercise
|
||||
| WritingExercise
|
||||
| SpeakingExercise;
|
||||
|
||||
export interface WritingExercise {
|
||||
id: string;
|
||||
@@ -57,6 +70,14 @@ export interface WritingExercise {
|
||||
wordCounter: WordCounter; //* The minimum or maximum amount of words that should be written
|
||||
}
|
||||
|
||||
export interface SpeakingExercise {
|
||||
id: string;
|
||||
type: "speaking";
|
||||
title: string;
|
||||
text: string;
|
||||
prompts: string[];
|
||||
}
|
||||
|
||||
export interface FillBlanksExercise {
|
||||
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
|
||||
type: "fillBlanks";
|
||||
|
||||
@@ -6,7 +6,7 @@ import {Module} from "@/interfaces";
|
||||
|
||||
import Selection from "@/exams/Selection";
|
||||
import Reading from "@/exams/Reading";
|
||||
import {Exam, ListeningExam, ReadingExam, UserSolution, WritingExam} from "@/interfaces/exam";
|
||||
import {Exam, ListeningExam, ReadingExam, SpeakingExam, UserSolution, WritingExam} from "@/interfaces/exam";
|
||||
import Listening from "@/exams/Listening";
|
||||
import Writing from "@/exams/Writing";
|
||||
import {ToastContainer, toast} from "react-toastify";
|
||||
@@ -15,6 +15,7 @@ import axios from "axios";
|
||||
import {withIronSessionSsr} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {User} from "@/interfaces/user";
|
||||
import Speaking from "@/exams/Speaking";
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
const user = req.session.user;
|
||||
@@ -68,9 +69,9 @@ export default function Page({user}: {user: User}) {
|
||||
return newExam.shift() as ListeningExam;
|
||||
case "writing":
|
||||
return newExam.shift() as WritingExam;
|
||||
case "speaking":
|
||||
return newExam.shift() as SpeakingExam;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const updateExamWithUserSolutions = (exam: Exam): Exam => {
|
||||
@@ -122,6 +123,15 @@ export default function Page({user}: {user: User}) {
|
||||
return <Writing exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
||||
}
|
||||
|
||||
if (exam && exam.module === "speaking" && showSolutions) {
|
||||
setModuleIndex((prev) => prev + 1);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (exam && exam.module === "speaking") {
|
||||
return <Speaking exam={exam} onFinish={onFinish} showSolutions={showSolutions} />;
|
||||
}
|
||||
|
||||
return <>Loading...</>;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user