From 2d46bad40f50a13d933eaf25fe63c3c18d5120fb Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 15 Jun 2023 14:43:29 +0100 Subject: [PATCH] Implemented the Reading and Listening initial screens according to the new designs, creating new components as needed --- src/components/High/Layout.tsx | 11 +++- src/components/Low/AudioPlayer.tsx | 87 +++++++++++++++++++++++++++ src/components/Low/ProgressBar.tsx | 23 +++++-- src/components/Medium/ModuleTitle.tsx | 64 ++++++++++++++++++++ src/exams/Listening.tsx | 65 +++++++++----------- src/exams/Reading.tsx | 59 +++++++----------- src/pages/_app.tsx | 13 ++++ src/pages/exam.tsx | 6 +- src/pages/test.tsx | 4 +- src/stores/examStore.ts | 8 ++- src/styles/globals.css | 4 +- src/utils/string.ts | 12 ++++ tailwind.config.js | 1 + 13 files changed, 272 insertions(+), 85 deletions(-) create mode 100644 src/components/Low/AudioPlayer.tsx create mode 100644 src/components/Medium/ModuleTitle.tsx diff --git a/src/components/High/Layout.tsx b/src/components/High/Layout.tsx index b6925d17..85c99237 100644 --- a/src/components/High/Layout.tsx +++ b/src/components/High/Layout.tsx @@ -1,19 +1,24 @@ import {User} from "@/interfaces/user"; +import clsx from "clsx"; import Navbar from "../Navbar"; import Sidebar from "../Sidebar"; interface Props { user: User; children: React.ReactNode; + className?: string; } -export default function Layout({user, children}: Props) { +export default function Layout({user, children, className}: Props) { return ( -
+
-
{children}
+
+ {children} +
); diff --git a/src/components/Low/AudioPlayer.tsx b/src/components/Low/AudioPlayer.tsx new file mode 100644 index 00000000..6c6f249f --- /dev/null +++ b/src/components/Low/AudioPlayer.tsx @@ -0,0 +1,87 @@ +import {Module} from "@/interfaces"; +import {formatTimeInMinutes} from "@/utils/string"; +import clsx from "clsx"; +import {useEffect, useRef, useState} from "react"; +import {BsPauseFill, BsPlayFill} from "react-icons/bs"; +import ProgressBar from "./ProgressBar"; + +interface Props { + src: string; + color: "blue" | "orange" | "green" | Module; + autoPlay?: boolean; + disabled?: boolean; + onEnd?: () => void; +} + +export default function AudioPlayer({src, color, autoPlay = false, disabled = false, onEnd}: Props) { + const [isPlaying, setIsPlaying] = useState(false); + const [duration, setDuration] = useState(0); + const [currentTime, setCurrentTime] = useState(0); + + const audioPlayerRef = useRef(null); + + useEffect(() => { + if (audioPlayerRef && audioPlayerRef.current) { + const seconds = Math.floor(audioPlayerRef.current.duration); + setDuration(seconds); + } + }, [audioPlayerRef?.current?.readyState]); + + useEffect(() => { + let playingInterval: NodeJS.Timer | undefined = undefined; + if (isPlaying) { + playingInterval = setInterval(() => setCurrentTime((prev) => prev + 1), 1000); + } else if (playingInterval) { + clearInterval(playingInterval); + } + + return () => { + if (playingInterval) clearInterval(playingInterval); + }; + }, [isPlaying]); + + const togglePlayPause = () => { + const prevValue = isPlaying; + setIsPlaying(!prevValue); + if (!prevValue) { + audioPlayerRef?.current?.play(); + } else { + audioPlayerRef?.current?.pause(); + } + }; + + return ( +
+ {isPlaying && ( + + )} + {!isPlaying && ( + + )} +
+ ); +} diff --git a/src/components/Low/ProgressBar.tsx b/src/components/Low/ProgressBar.tsx index 54e1db02..f9bbb005 100644 --- a/src/components/Low/ProgressBar.tsx +++ b/src/components/Low/ProgressBar.tsx @@ -1,22 +1,37 @@ +import {Module} from "@/interfaces"; import clsx from "clsx"; interface Props { label: string; percentage: number; - color: "blue" | "orange" | "green"; + color: "blue" | "orange" | "green" | Module; + useColor?: boolean; className?: string; } -export default function ProgressBar({label, percentage, color, className}: Props) { +export default function ProgressBar({label, percentage, color, useColor = false, className}: Props) { const progressColorClass: {[key in typeof color]: string} = { blue: "bg-mti-blue-light", orange: "bg-mti-orange-light", green: "bg-mti-green-light", + reading: "bg-ielts-reading", + listening: "bg-ielts-listening", + writing: "bg-ielts-writing", + speaking: "bg-ielts-speaking", }; return ( -
-
+
+
{label}
); diff --git a/src/components/Medium/ModuleTitle.tsx b/src/components/Medium/ModuleTitle.tsx new file mode 100644 index 00000000..9a6ccf71 --- /dev/null +++ b/src/components/Medium/ModuleTitle.tsx @@ -0,0 +1,64 @@ +import {Module} from "@/interfaces"; +import {ReactNode, useEffect, useState} from "react"; +import {BsBook, BsHeadphones, BsPen, BsStopwatch} from "react-icons/bs"; +import ProgressBar from "../Low/ProgressBar"; + +interface Props { + minTimer: number; + module: Module; + exerciseIndex: number; + totalExercises: number; +} + +export default function ModuleTitle({minTimer, module, exerciseIndex, totalExercises}: Props) { + const [timer, setTimer] = useState(minTimer * 60); + + useEffect(() => { + const timerInterval = setInterval(() => setTimer((prev) => prev - 1), 1000); + + return () => { + clearInterval(timerInterval); + }; + }, [minTimer]); + + const moduleIcon: {[key in Module]: ReactNode} = { + reading: , + listening: , + writing: , + speaking: , + }; + + return ( + <> +
+ + + {timer > 0 && ( + <> + {Math.floor(timer / 60) + .toString(10) + .padStart(2, "0")} + : + {Math.floor(timer % 60) + .toString(10) + .padStart(2, "0")} + + )} + {timer <= 0 && <>00:00} + +
+
+
{moduleIcon[module]}
+
+
+ Reading exam N.19 + + Question {exerciseIndex}/{totalExercises} + +
+ +
+
+ + ); +} diff --git a/src/exams/Listening.tsx b/src/exams/Listening.tsx index 9b08d07e..65599c64 100644 --- a/src/exams/Listening.tsx +++ b/src/exams/Listening.tsx @@ -6,6 +6,9 @@ import clsx from "clsx"; import {infoButtonStyle} from "@/constants/buttonStyles"; import {renderExercise} from "@/components/Exercises"; import {renderSolution} from "@/components/Solutions"; +import ModuleTitle from "@/components/Medium/ModuleTitle"; +import AudioPlayer from "@/components/Low/AudioPlayer"; +import Button from "@/components/Low/Button"; interface Props { exam: ListeningExam; @@ -42,37 +45,31 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props }; const renderAudioPlayer = () => ( - <> - {exerciseIndex === -1 && ( -
- Please listen to the following audio attentively. - {exam.audio.repeatableTimes > 0 ? ( - - You will only be allowed to listen to the audio {exam.audio.repeatableTimes} time(s). - - ) : ( - You may listen to the audio as many times as you would like. - )} -
- )} -
- {exam.audio.repeatableTimes > 0 && ( - <>{exam.audio.repeatableTimes <= timesListened && You are no longer allowed to listen to the audio again.} - )} - {exam.audio.repeatableTimes > 0 && timesListened < exam.audio.repeatableTimes && ( - - )} +
+
+

Please listen to the following audio attentively.

+ + {exam.audio.repeatableTimes > 0 + ? `You will only be allowed to listen to the audio ${exam.audio.repeatableTimes - timesListened} time(s).` + : "You may listen to the audio as many times as you would like."} +
- +
+ setTimesListened((prev) => prev + 1)} + disabled={timesListened === exam.audio.repeatableTimes} + /> +
+
); return ( <> -
- {renderAudioPlayer()} +
+ + {exerciseIndex === -1 && renderAudioPlayer()} {exerciseIndex > -1 && exerciseIndex < exam.exercises.length && !showSolutions && @@ -81,17 +78,13 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props exerciseIndex < exam.exercises.length && showSolutions && renderSolution(exam.exercises[exerciseIndex], nextExercise, previousExercise)} - {exerciseIndex === -1 && ( - - )}
+ + {exerciseIndex === -1 && ( + + )} ); } diff --git a/src/exams/Reading.tsx b/src/exams/Reading.tsx index 0d4f1697..31a608bf 100644 --- a/src/exams/Reading.tsx +++ b/src/exams/Reading.tsx @@ -9,6 +9,11 @@ import {renderExercise} from "@/components/Exercises"; import {renderSolution} from "@/components/Solutions"; import {Panel} from "primereact/panel"; import {Steps} from "primereact/steps"; +import {BsAlarm, BsBook, BsClock, BsStopwatch} from "react-icons/bs"; +import ProgressBar from "@/components/Low/ProgressBar"; +import ModuleTitle from "@/components/Medium/ModuleTitle"; +import {Divider} from "primereact/divider"; +import Button from "@/components/Low/Button"; interface Props { exam: ReadingExam; @@ -102,27 +107,30 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) }; const renderText = () => ( -
+
- +

Please read the following excerpt attentively, you will then be asked questions about the text you've read. - - You will be allowed to read the text while doing the exercises +

+ You will be allowed to read the text while doing the exercises
- -

+

+

{exam.text.title}

+
+ {exam.text.content.split("\\n").map((line, index) => (

{line}

))} -

- +
+
); return ( <> - setShowTextModal(false)} /> -
+
+ setShowTextModal(false)} /> + {exerciseIndex === -1 && renderText()} {exerciseIndex > -1 && exerciseIndex < exam.exercises.length && @@ -132,33 +140,12 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) exerciseIndex < exam.exercises.length && showSolutions && renderSolution(exam.exercises[exerciseIndex], nextExercise, previousExercise)} -
-1 ? "w-full justify-center md:justify-between" : "self-end w-full md:w-fit flex")}> - {exerciseIndex > -1 && ( - - )} - {exerciseIndex === -1 && ( - - )} -
+ {exerciseIndex === -1 && ( + + )} ); } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 8b694866..0875bf16 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -5,7 +5,20 @@ import type {AppProps} from "next/app"; import "primereact/resources/themes/lara-light-indigo/theme.css"; import "primereact/resources/primereact.min.css"; import "primeicons/primeicons.css"; +import {useRouter} from "next/router"; +import {useEffect} from "react"; +import useExamStore from "@/stores/examStore"; export default function App({Component, pageProps}: AppProps) { + const reset = useExamStore((state) => state.reset); + + const router = useRouter(); + + useEffect(() => { + if (router.pathname !== "/exam") { + reset(); + } + }, [router.pathname, reset]); + return ; } diff --git a/src/pages/exam.tsx b/src/pages/exam.tsx index 1c6ad024..3262b1ae 100644 --- a/src/pages/exam.tsx +++ b/src/pages/exam.tsx @@ -209,7 +209,11 @@ export default function Page() { - {user && {renderScreen()}} + {user && ( + + {renderScreen()} + + )} ); } diff --git a/src/pages/test.tsx b/src/pages/test.tsx index bce36bfc..43b0f5d5 100644 --- a/src/pages/test.tsx +++ b/src/pages/test.tsx @@ -127,11 +127,11 @@ export default function Page() { <>
- {Math.round(recordingDuration / 60) + {Math.floor(recordingDuration / 60) .toString(10) .padStart(2, "0")} : - {Math.round(recordingDuration % 60) + {Math.floor(recordingDuration % 60) .toString(10) .padStart(2, "0")} diff --git a/src/stores/examStore.ts b/src/stores/examStore.ts index e074fe2c..4fbc297f 100644 --- a/src/stores/examStore.ts +++ b/src/stores/examStore.ts @@ -12,17 +12,23 @@ export interface ExamState { setExams: (exams: Exam[]) => void; setShowSolutions: (showSolutions: boolean) => void; setSelectedModules: (modules: Module[]) => void; + reset: () => void; } -const useExamStore = create((set) => ({ +export const initialState = { exams: [], userSolutions: [], showSolutions: false, selectedModules: [], +}; + +const useExamStore = create((set) => ({ + ...initialState, setUserSolutions: (userSolutions: UserSolution[]) => set(() => ({userSolutions})), setExams: (exams: Exam[]) => set(() => ({exams})), setShowSolutions: (showSolutions: boolean) => set(() => ({showSolutions})), setSelectedModules: (modules: Module[]) => set(() => ({selectedModules: modules})), + reset: () => set(() => initialState), })); export default useExamStore; diff --git a/src/styles/globals.css b/src/styles/globals.css index 2fe483f8..1e1987bd 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -9,8 +9,8 @@ "Fira Mono", "Droid Sans Mono", "Courier New", monospace; --foreground-rgb: 53, 51, 56; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; + --background-start-rgb: 245, 245, 245; + --background-end-rgb: 245, 245, 245; --primary-glow: conic-gradient(from 180deg at 50% 50%, #16abff33 0deg, #0885ff33 55deg, #54d6ff33 120deg, #0071ff33 160deg, transparent 360deg); --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); diff --git a/src/utils/string.ts b/src/utils/string.ts index fe9f629c..a715455a 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -7,3 +7,15 @@ export function convertCamelCaseToReadable(camelCaseString: string): string { return readableString; } + +export function formatTimeInMinutes(time: number) { + if (time === 0) { + return "00:00"; + } + + return `${Math.floor(time / 60) + .toString(10) + .padStart(2, "0")}:${Math.floor(time % 60) + .toString(10) + .padStart(2, "0")}`; +} diff --git a/tailwind.config.js b/tailwind.config.js index ea4551a4..3acd9603 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -17,6 +17,7 @@ module.exports = { cool: "#8692A6", platinum: "#DBDBDB", "anti-flash": "#EAEBEC", + davy: "#595959", }, black: "#353338", },