Merge branch 'develop'
This commit is contained in:
@@ -41,7 +41,7 @@ export default function Writing({
|
||||
if (inputText.length > 0 && saveTimer % 10 === 0) {
|
||||
setUserSolutions([
|
||||
...storeUserSolutions.filter((x) => x.exercise !== id),
|
||||
{exercise: id, solutions: [{id, solution: inputText}], score: {correct: 1, total: 1, missing: 0}, type},
|
||||
{exercise: id, solutions: [{id, solution: inputText}], score: {correct: 1, total: 1, missing: 0}, type, module: "writing"},
|
||||
]);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -64,7 +64,8 @@ export default function Writing({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasExamEnded) onNext({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 1, total: 1, missing: 0}, type});
|
||||
if (hasExamEnded)
|
||||
onNext({exercise: id, solutions: [{id, solution: inputText}], score: {correct: 1, total: 1, missing: 0}, type, module: "writing"});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hasExamEnded]);
|
||||
|
||||
@@ -160,6 +161,7 @@ export default function Writing({
|
||||
solutions: [{id, solution: inputText.replaceAll(/\s{2,}/g, " ")}],
|
||||
score: {correct: 1, total: 1, missing: 0},
|
||||
type,
|
||||
module: "writing",
|
||||
})
|
||||
}
|
||||
className="max-w-[200px] self-end w-full">
|
||||
|
||||
@@ -34,6 +34,7 @@ export default function Layout({user, children, className, navDisabled = false,
|
||||
onFocusLayerMouseEnter={onFocusLayerMouseEnter}
|
||||
className="-md:hidden"
|
||||
userType={user.type}
|
||||
userId={user.id}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
|
||||
@@ -21,7 +21,11 @@ export default function ModuleTitle({minTimer, module, label, exerciseIndex, tot
|
||||
const [timer, setTimer] = useState(minTimer * 60);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [warningMode, setWarningMode] = useState(false);
|
||||
|
||||
const setHasExamEnded = useExamStore((state) => state.setHasExamEnded);
|
||||
const {timeSpent} = useExamStore((state) => state);
|
||||
|
||||
useEffect(() => setTimer((prev) => prev - timeSpent), [timeSpent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!disableTimer) {
|
||||
|
||||
70
src/components/Medium/TopicModal.tsx
Normal file
70
src/components/Medium/TopicModal.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import topics from "@/resources/topics";
|
||||
import {useState} from "react";
|
||||
import {BsArrowLeft, BsArrowRight} from "react-icons/bs";
|
||||
import Button from "../Low/Button";
|
||||
import Modal from "../Modal";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
initialTopics: string[];
|
||||
onClose: VoidFunction;
|
||||
selectTopics: (topics: string[]) => void;
|
||||
}
|
||||
|
||||
export default function TopicModal({isOpen, initialTopics, onClose, selectTopics}: Props) {
|
||||
const [selectedTopics, setSelectedTopics] = useState([...initialTopics]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Preferred Topics">
|
||||
<div className="flex flex-col w-full h-full gap-4 mt-4">
|
||||
<div className="w-full h-full grid grid-cols-2 -md:gap-1 gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="border-b border-b-neutral-400/30">Available Topics</span>
|
||||
<div className=" max-h-[500px] overflow-y-scroll scrollbar-hide">
|
||||
{topics
|
||||
.filter((x) => !selectedTopics.includes(x))
|
||||
.map((x) => (
|
||||
<div key={x} className="odd:bg-mti-purple-ultralight/40 p-2 flex justify-between items-center">
|
||||
<span>{x}</span>
|
||||
<button
|
||||
onClick={() => setSelectedTopics((prev) => [...prev, x])}
|
||||
className="border border-mti-purple-light cursor-pointer p-2 rounded-lg bg-white drop-shadow transition ease-in-out duration-300 hover:bg-mti-purple hover:text-white">
|
||||
<BsArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="border-b border-b-neutral-400/30">Preferred Topics ({selectedTopics.length || "All"})</span>
|
||||
<div className=" max-h-[500px] overflow-y-scroll scrollbar-hide">
|
||||
{selectedTopics.map((x) => (
|
||||
<div key={x} className="odd:bg-mti-purple-ultralight/40 p-2 flex justify-between items-center text-right">
|
||||
<button
|
||||
onClick={() => setSelectedTopics((prev) => [...prev.filter((y) => y !== x)])}
|
||||
className="border border-mti-purple-light cursor-pointer p-2 rounded-lg bg-white drop-shadow transition ease-in-out duration-300 hover:bg-mti-purple hover:text-white">
|
||||
<BsArrowLeft />
|
||||
</button>
|
||||
<span>{x}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex gap-4 items-center justify-end">
|
||||
<Button variant="outline" color="rose" className="w-full max-w-[200px]" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full max-w-[200px]"
|
||||
onClick={() => {
|
||||
selectTopics(selectedTopics);
|
||||
onClose();
|
||||
}}>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import { IconType } from "react-icons";
|
||||
import { MdSpaceDashboard } from "react-icons/md";
|
||||
import {IconType} from "react-icons";
|
||||
import {MdSpaceDashboard} from "react-icons/md";
|
||||
import {
|
||||
BsFileEarmarkText,
|
||||
BsClockHistory,
|
||||
@@ -13,17 +13,18 @@ import {
|
||||
BsCurrencyDollar,
|
||||
BsClipboardData,
|
||||
} from "react-icons/bs";
|
||||
import { RiLogoutBoxFill } from "react-icons/ri";
|
||||
import { SlPencil } from "react-icons/sl";
|
||||
import { FaAward } from "react-icons/fa";
|
||||
import {RiLogoutBoxFill} from "react-icons/ri";
|
||||
import {SlPencil} from "react-icons/sl";
|
||||
import {FaAward} from "react-icons/fa";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import {useRouter} from "next/router";
|
||||
import axios from "axios";
|
||||
import FocusLayer from "@/components/FocusLayer";
|
||||
import { preventNavigation } from "@/utils/navigation.disabled";
|
||||
import { useState } from "react";
|
||||
import {preventNavigation} from "@/utils/navigation.disabled";
|
||||
import {useEffect, useState} from "react";
|
||||
import usePreferencesStore from "@/stores/preferencesStore";
|
||||
import { Type } from "@/interfaces/user";
|
||||
import {Type} from "@/interfaces/user";
|
||||
import useTicketsListener from "@/hooks/useTicketsListener";
|
||||
interface Props {
|
||||
path: string;
|
||||
navDisabled?: boolean;
|
||||
@@ -31,6 +32,7 @@ interface Props {
|
||||
onFocusLayerMouseEnter?: () => void;
|
||||
className?: string;
|
||||
userType?: Type;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
interface NavProps {
|
||||
@@ -40,47 +42,44 @@ interface NavProps {
|
||||
keyPath: string;
|
||||
disabled?: boolean;
|
||||
isMinimized?: boolean;
|
||||
badge?: number;
|
||||
}
|
||||
|
||||
const Nav = ({
|
||||
Icon,
|
||||
label,
|
||||
path,
|
||||
keyPath,
|
||||
disabled = false,
|
||||
isMinimized = false,
|
||||
}: NavProps) => (
|
||||
const Nav = ({Icon, label, path, keyPath, disabled = false, isMinimized = false, badge}: NavProps) => {
|
||||
return (
|
||||
<Link
|
||||
href={!disabled ? keyPath : ""}
|
||||
className={clsx(
|
||||
"flex items-center gap-4 rounded-full p-4 text-gray-500 hover:text-white",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
disabled
|
||||
? "hover:bg-mti-gray-dim cursor-not-allowed"
|
||||
: "hover:bg-mti-purple-light cursor-pointer",
|
||||
"transition-all duration-300 ease-in-out relative",
|
||||
disabled ? "hover:bg-mti-gray-dim cursor-not-allowed" : "hover:bg-mti-purple-light cursor-pointer",
|
||||
path === keyPath && "bg-mti-purple-light text-white",
|
||||
isMinimized ? "w-fit" : "w-full min-w-[200px] px-8 2xl:min-w-[220px]",
|
||||
)}
|
||||
>
|
||||
)}>
|
||||
<Icon size={24} />
|
||||
{!isMinimized && <span className="text-lg font-semibold">{label}</span>}
|
||||
{!!badge && badge > 0 && (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-mti-purple-light h-5 w-5 text-xs rounded-full flex items-center justify-center text-white",
|
||||
"transition ease-in-out duration-300",
|
||||
isMinimized && "absolute right-0 top-0",
|
||||
)}>
|
||||
{badge}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default function Sidebar({
|
||||
path,
|
||||
navDisabled = false,
|
||||
focusMode = false,
|
||||
userType,
|
||||
onFocusLayerMouseEnter,
|
||||
className,
|
||||
}: Props) {
|
||||
export default function Sidebar({path, navDisabled = false, focusMode = false, userType, onFocusLayerMouseEnter, className, userId}: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [
|
||||
state.isSidebarMinimized,
|
||||
state.toggleSidebarMinimized,
|
||||
]);
|
||||
const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [state.isSidebarMinimized, state.toggleSidebarMinimized]);
|
||||
|
||||
const {totalAssignedTickets} = useTicketsListener(userId);
|
||||
|
||||
useEffect(() => console.log(totalAssignedTickets), [totalAssignedTickets]);
|
||||
|
||||
const logout = async () => {
|
||||
axios.post("/api/logout").finally(() => {
|
||||
@@ -96,20 +95,10 @@ export default function Sidebar({
|
||||
"relative flex h-full flex-col justify-between bg-transparent px-4 py-4 pb-8",
|
||||
isMinimized ? "w-fit" : "-xl:w-fit w-1/6",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
)}>
|
||||
<div className="-xl:hidden flex-col gap-3 xl:flex">
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={MdSpaceDashboard}
|
||||
label="Dashboard"
|
||||
path={path}
|
||||
keyPath="/"
|
||||
isMinimized={isMinimized}
|
||||
/>
|
||||
{(userType === "student" ||
|
||||
userType === "teacher" ||
|
||||
userType === "developer") && (
|
||||
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized={isMinimized} />
|
||||
{(userType === "student" || userType === "teacher" || userType === "developer") && (
|
||||
<>
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
@@ -129,25 +118,9 @@ export default function Sidebar({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={BsGraphUp}
|
||||
label="Stats"
|
||||
path={path}
|
||||
keyPath="/stats"
|
||||
isMinimized={isMinimized}
|
||||
/>
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={BsClockHistory}
|
||||
label="Record"
|
||||
path={path}
|
||||
keyPath="/record"
|
||||
isMinimized={isMinimized}
|
||||
/>
|
||||
{["admin", "developer", "agent", "corporate"].includes(
|
||||
userType || "",
|
||||
) && (
|
||||
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} />
|
||||
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
|
||||
{["admin", "developer", "agent", "corporate"].includes(userType || "") && (
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={BsCurrencyDollar}
|
||||
@@ -157,9 +130,7 @@ export default function Sidebar({
|
||||
isMinimized={isMinimized}
|
||||
/>
|
||||
)}
|
||||
{["admin", "developer", "corporate", "teacher"].includes(
|
||||
userType || "",
|
||||
) && (
|
||||
{["admin", "developer", "corporate", "teacher"].includes(userType || "") && (
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={BsShieldFill}
|
||||
@@ -177,6 +148,7 @@ export default function Sidebar({
|
||||
path={path}
|
||||
keyPath="/tickets"
|
||||
isMinimized={isMinimized}
|
||||
badge={totalAssignedTickets}
|
||||
/>
|
||||
)}
|
||||
{userType === "developer" && (
|
||||
@@ -191,65 +163,16 @@ export default function Sidebar({
|
||||
)}
|
||||
</div>
|
||||
<div className="-xl:flex flex-col gap-3 xl:hidden">
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={MdSpaceDashboard}
|
||||
label="Dashboard"
|
||||
path={path}
|
||||
keyPath="/"
|
||||
isMinimized={true}
|
||||
/>
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={BsFileEarmarkText}
|
||||
label="Exams"
|
||||
path={path}
|
||||
keyPath="/exam"
|
||||
isMinimized={true}
|
||||
/>
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={BsPencil}
|
||||
label="Exercises"
|
||||
path={path}
|
||||
keyPath="/exercises"
|
||||
isMinimized={true}
|
||||
/>
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={BsGraphUp}
|
||||
label="Stats"
|
||||
path={path}
|
||||
keyPath="/stats"
|
||||
isMinimized={true}
|
||||
/>
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={BsClockHistory}
|
||||
label="Record"
|
||||
path={path}
|
||||
keyPath="/record"
|
||||
isMinimized={true}
|
||||
/>
|
||||
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized={true} />
|
||||
<Nav disabled={disableNavigation} Icon={BsFileEarmarkText} label="Exams" path={path} keyPath="/exam" isMinimized={true} />
|
||||
<Nav disabled={disableNavigation} Icon={BsPencil} label="Exercises" path={path} keyPath="/exercises" isMinimized={true} />
|
||||
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={true} />
|
||||
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={true} />
|
||||
{userType !== "student" && (
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={BsShieldFill}
|
||||
label="Settings"
|
||||
path={path}
|
||||
keyPath="/settings"
|
||||
isMinimized={true}
|
||||
/>
|
||||
<Nav disabled={disableNavigation} Icon={BsShieldFill} label="Settings" path={path} keyPath="/settings" isMinimized={true} />
|
||||
)}
|
||||
{userType === "developer" && (
|
||||
<Nav
|
||||
disabled={disableNavigation}
|
||||
Icon={BsCloudFill}
|
||||
label="Generation"
|
||||
path={path}
|
||||
keyPath="/generation"
|
||||
isMinimized={true}
|
||||
/>
|
||||
<Nav disabled={disableNavigation} Icon={BsCloudFill} label="Generation" path={path} keyPath="/generation" isMinimized={true} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -261,16 +184,9 @@ export default function Sidebar({
|
||||
className={clsx(
|
||||
"hover:text-mti-rose -xl:hidden flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out",
|
||||
isMinimized ? "w-fit" : "w-full min-w-[250px] px-8",
|
||||
)}
|
||||
>
|
||||
{isMinimized ? (
|
||||
<BsChevronBarRight size={24} />
|
||||
) : (
|
||||
<BsChevronBarLeft size={24} />
|
||||
)}
|
||||
{!isMinimized && (
|
||||
<span className="text-lg font-medium">Minimize</span>
|
||||
)}
|
||||
)}>
|
||||
{isMinimized ? <BsChevronBarRight size={24} /> : <BsChevronBarLeft size={24} />}
|
||||
{!isMinimized && <span className="text-lg font-medium">Minimize</span>}
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
@@ -279,17 +195,12 @@ export default function Sidebar({
|
||||
className={clsx(
|
||||
"hover:text-mti-rose flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out",
|
||||
isMinimized ? "w-fit" : "w-full min-w-[250px] px-8",
|
||||
)}
|
||||
>
|
||||
)}>
|
||||
<RiLogoutBoxFill size={24} />
|
||||
{!isMinimized && (
|
||||
<span className="-xl:hidden text-lg font-medium">Log Out</span>
|
||||
)}
|
||||
{!isMinimized && <span className="-xl:hidden text-lg font-medium">Log Out</span>}
|
||||
</div>
|
||||
</div>
|
||||
{focusMode && (
|
||||
<FocusLayer onFocusLayerMouseEnter={onFocusLayerMouseEnter} />
|
||||
)}
|
||||
{focusMode && <FocusLayer onFocusLayerMouseEnter={onFocusLayerMouseEnter} />}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const INSTRUCTIONS_AUDIO_SRC =
|
||||
"https://firebasestorage.googleapis.com/v0/b/storied-phalanx-349916.appspot.com/o/listening_recordings%2Fgeneric_intro.mp3?alt=media&token=9b9cfdb8-e90d-40d1-854b-51c4378a5c4b";
|
||||
"https://firebasestorage.googleapis.com/v0/b/storied-phalanx-349916.appspot.com/o/generic_listening_intro_v2.mp3?alt=media&token=16769f5f-1e9b-4a72-86a9-45a6f0fa9f82";
|
||||
|
||||
export default function Listening({exam, showSolutions = false, onFinish}: Props) {
|
||||
const [questionIndex, setQuestionIndex] = useState(0);
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { Ticket } from "@/interfaces/ticket";
|
||||
import { Code, Group, User } from "@/interfaces/user";
|
||||
import axios from "axios";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
|
||||
export default function useTickets() {
|
||||
const [tickets, setTickets] = useState<Ticket[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isError, setIsError] = useState(false);
|
||||
|
||||
const getData = () => {
|
||||
const getData = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
axios
|
||||
.get<Ticket[]>(`/api/tickets`)
|
||||
.then((response) => setTickets(response.data))
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(getData, []);
|
||||
useEffect(getData, [getData]);
|
||||
|
||||
return { tickets, isLoading, isError, reload: getData };
|
||||
}
|
||||
|
||||
29
src/hooks/useTicketsListener.tsx
Normal file
29
src/hooks/useTicketsListener.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
import useTickets from "./useTickets";
|
||||
|
||||
const useTicketsListener = (userId?: string) => {
|
||||
const { tickets, reload } = useTickets();
|
||||
|
||||
React.useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
reload();
|
||||
}, 60 * 1000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [reload]);
|
||||
|
||||
if (userId) {
|
||||
const assignedTickets = tickets.filter(
|
||||
(ticket) => ticket.assignedTo === userId && ticket.status === "submitted"
|
||||
);
|
||||
|
||||
return {
|
||||
assignedTickets,
|
||||
totalAssignedTickets: assignedTickets.length,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export default useTicketsListener;
|
||||
@@ -69,7 +69,7 @@ export interface UserSolution {
|
||||
export interface WritingExam {
|
||||
module: "writing";
|
||||
id: string;
|
||||
exercises: Exercise[];
|
||||
exercises: WritingExercise[];
|
||||
minTimer: number;
|
||||
isDiagnostic: boolean;
|
||||
variant?: Variant;
|
||||
@@ -84,7 +84,7 @@ interface WordCounter {
|
||||
export interface SpeakingExam {
|
||||
id: string;
|
||||
module: "speaking";
|
||||
exercises: Exercise[];
|
||||
exercises: (SpeakingExercise | InteractiveSpeakingExercise)[];
|
||||
minTimer: number;
|
||||
isDiagnostic: boolean;
|
||||
variant?: Variant;
|
||||
@@ -148,6 +148,7 @@ export interface WritingExercise {
|
||||
solution: string;
|
||||
evaluation?: CommonEvaluation;
|
||||
}[];
|
||||
topic?: string;
|
||||
}
|
||||
|
||||
export interface SpeakingExercise {
|
||||
@@ -162,6 +163,7 @@ export interface SpeakingExercise {
|
||||
solution: string;
|
||||
evaluation?: SpeakingEvaluation;
|
||||
}[];
|
||||
topic?: string;
|
||||
}
|
||||
|
||||
export interface InteractiveSpeakingExercise {
|
||||
@@ -175,6 +177,7 @@ export interface InteractiveSpeakingExercise {
|
||||
solution: {questionIndex: number; question: string; answer: string}[];
|
||||
evaluation?: InteractiveSpeakingEvaluation;
|
||||
}[];
|
||||
topic?: string;
|
||||
}
|
||||
|
||||
export interface FillBlanksExercise {
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface StudentUser extends BasicUser {
|
||||
type: "student";
|
||||
preferredGender?: InstructorGender;
|
||||
demographicInformation?: DemographicInformation;
|
||||
preferredTopics?: string[];
|
||||
}
|
||||
|
||||
export interface TeacherUser extends BasicUser {
|
||||
@@ -52,6 +53,7 @@ export interface DeveloperUser extends BasicUser {
|
||||
type: "developer";
|
||||
preferredGender?: InstructorGender;
|
||||
demographicInformation?: DemographicInformation;
|
||||
preferredTopics?: string[];
|
||||
}
|
||||
|
||||
export interface CorporateInformation {
|
||||
|
||||
@@ -336,12 +336,13 @@ export default function ExamPage({page}: Props) {
|
||||
};
|
||||
|
||||
answers.forEach((x) => {
|
||||
console.log({x});
|
||||
const examModule =
|
||||
x.module || (x.type === "writing" ? "writing" : x.type === "speaking" || x.type === "interactiveSpeaking" ? "speaking" : undefined);
|
||||
|
||||
scores[x.module!] = {
|
||||
total: scores[x.module!].total + x.score.total,
|
||||
correct: scores[x.module!].correct + x.score.correct,
|
||||
missing: scores[x.module!].missing + x.score.missing,
|
||||
scores[examModule!] = {
|
||||
total: scores[examModule!].total + x.score.total,
|
||||
correct: scores[examModule!].correct + x.score.correct,
|
||||
missing: scores[examModule!].missing + x.score.missing,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ const PartTab = ({
|
||||
|
||||
playSound(isError ? "error" : "check");
|
||||
if (isError) return toast.error("Something went wrong, please try to generate the video again.");
|
||||
setPart({...part, result: result.data, gender, avatar});
|
||||
setPart({...part, result: {...result.data, topic: part?.topic}, gender, avatar});
|
||||
})
|
||||
.catch((e) => {
|
||||
toast.error("Something went wrong!");
|
||||
@@ -79,6 +79,8 @@ const PartTab = ({
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => console.log(part), [part]);
|
||||
|
||||
return (
|
||||
<Tab.Panel className="w-full bg-ielts-speaking/20 min-h-[600px] h-full rounded-xl p-6 flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
|
||||
@@ -16,7 +16,7 @@ import {CorporateUser, EmploymentStatus, EMPLOYMENT_STATUS, Gender, User} from "
|
||||
import CountrySelect from "@/components/Low/CountrySelect";
|
||||
import {shouldRedirectHome} from "@/utils/navigation.disabled";
|
||||
import moment from "moment";
|
||||
import {BsCamera} from "react-icons/bs";
|
||||
import {BsCamera, BsQuestionCircleFill} from "react-icons/bs";
|
||||
import {USER_TYPE_LABELS} from "@/resources/user";
|
||||
import useGroups from "@/hooks/useGroups";
|
||||
import useUsers from "@/hooks/useUsers";
|
||||
@@ -31,6 +31,8 @@ import ModuleLevelSelector from "@/components/Medium/ModuleLevelSelector";
|
||||
import Select from "@/components/Low/Select";
|
||||
import {InstructorGender} from "@/interfaces/exam";
|
||||
import {capitalize} from "lodash";
|
||||
import TopicModal from "@/components/Medium/TopicModal";
|
||||
import {v4} from "uuid";
|
||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
const user = req.session.user;
|
||||
|
||||
@@ -90,6 +92,9 @@ function UserProfile({user, mutateUser}: Props) {
|
||||
const [preferredGender, setPreferredGender] = useState<InstructorGender | undefined>(
|
||||
user.type === "student" || user.type === "developer" ? user.preferredGender || "varied" : undefined,
|
||||
);
|
||||
const [preferredTopics, setPreferredTopics] = useState<string[] | undefined>(
|
||||
user.type === "student" || user.type === "developer" ? user.preferredTopics : undefined,
|
||||
);
|
||||
|
||||
const [position, setPosition] = useState<string | undefined>(user.type === "corporate" ? user.demographicInformation?.position : undefined);
|
||||
const [corporateInformation, setCorporateInformation] = useState(user.type === "corporate" ? user.corporateInformation : undefined);
|
||||
@@ -99,6 +104,8 @@ function UserProfile({user, mutateUser}: Props) {
|
||||
);
|
||||
const [timezone, setTimezone] = useState<string>(user.demographicInformation?.timezone || moment.tz.guess());
|
||||
|
||||
const [isPreferredTopicsOpen, setIsPreferredTopicsOpen] = useState(false);
|
||||
|
||||
const {groups} = useGroups();
|
||||
const {users} = useUsers();
|
||||
|
||||
@@ -156,6 +163,7 @@ function UserProfile({user, mutateUser}: Props) {
|
||||
profilePicture,
|
||||
desiredLevels,
|
||||
preferredGender,
|
||||
preferredTopics,
|
||||
demographicInformation: {
|
||||
phone,
|
||||
country,
|
||||
@@ -350,6 +358,7 @@ function UserProfile({user, mutateUser}: Props) {
|
||||
{preferredGender && ["developer", "student"].includes(user.type) && (
|
||||
<>
|
||||
<Divider />
|
||||
<DoubleColumnRow>
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<label className="font-normal text-base text-mti-gray-dim">Speaking Instructor's Gender</label>
|
||||
<Select
|
||||
@@ -362,6 +371,28 @@ function UserProfile({user, mutateUser}: Props) {
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<label className="font-normal text-base text-mti-gray-dim flex gap-2 items-center">
|
||||
Preferred Topics{" "}
|
||||
<span
|
||||
className="tooltip"
|
||||
data-tip="These topics will be considered for speaking and writing modules, aiming to include at least one exercise containing of the these in the selected exams.">
|
||||
<BsQuestionCircleFill />
|
||||
</span>
|
||||
</label>
|
||||
<Button className="w-full" variant="outline" onClick={() => setIsPreferredTopicsOpen(true)}>
|
||||
Select Topics ({preferredTopics?.length || "All"} selected)
|
||||
</Button>
|
||||
</div>
|
||||
</DoubleColumnRow>
|
||||
|
||||
<TopicModal
|
||||
key={v4()}
|
||||
isOpen={isPreferredTopicsOpen}
|
||||
onClose={() => setIsPreferredTopicsOpen(false)}
|
||||
selectTopics={setPreferredTopics}
|
||||
initialTopics={preferredTopics || []}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
55
src/resources/topics.ts
Normal file
55
src/resources/topics.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
const topics = [
|
||||
"Education",
|
||||
"Technology",
|
||||
"Environment",
|
||||
"Health and Fitness",
|
||||
"Globalization",
|
||||
"Engineering",
|
||||
"Work and Careers",
|
||||
"Travel and Tourism",
|
||||
"Culture and Traditions",
|
||||
"Social Issues",
|
||||
"Arts and Entertainment",
|
||||
"Climate Change",
|
||||
"Social Media",
|
||||
"Sustainable Development",
|
||||
"Health Care",
|
||||
"Immigration",
|
||||
"Artificial Intelligence",
|
||||
"Consumerism",
|
||||
"Online Shopping",
|
||||
"Energy",
|
||||
"Oil and Gas",
|
||||
"Poverty and Inequality",
|
||||
"Cultural Diversity",
|
||||
"Democracy and Governance",
|
||||
"Mental Health",
|
||||
"Ethics and Morality",
|
||||
"Population Growth",
|
||||
"Science and Innovation",
|
||||
"Poverty Alleviation",
|
||||
"Cybersecurity and Privacy",
|
||||
"Human Rights",
|
||||
"Social Justice",
|
||||
"Food and Agriculture",
|
||||
"Cyberbullying and Online Safety",
|
||||
"Linguistic Diversity",
|
||||
"Urbanization",
|
||||
"Artificial Intelligence in Education",
|
||||
"Youth Empowerment",
|
||||
"Disaster Management",
|
||||
"Mental Health Stigma",
|
||||
"Internet Censorship",
|
||||
"Sustainable Fashion",
|
||||
"Indigenous Rights",
|
||||
"Water Scarcity",
|
||||
"Social Entrepreneurship",
|
||||
"Privacy in the Digital Age",
|
||||
"Sustainable Transportation",
|
||||
"Gender Equality",
|
||||
"Automation and Job Displacement",
|
||||
"Digital Divide",
|
||||
"Education Inequality",
|
||||
];
|
||||
|
||||
export default topics;
|
||||
@@ -1,7 +1,7 @@
|
||||
import {collection, getDocs, query, where, setDoc, doc, Firestore, getDoc} from "firebase/firestore";
|
||||
import {shuffle} from "lodash";
|
||||
import {Difficulty, Exam, InstructorGender, Variant} from "@/interfaces/exam";
|
||||
import {Stat, User} from "@/interfaces/user";
|
||||
import {Difficulty, Exam, InstructorGender, SpeakingExam, Variant, WritingExam} from "@/interfaces/exam";
|
||||
import {DeveloperUser, Stat, StudentUser, User} from "@/interfaces/user";
|
||||
import {Module} from "@/interfaces";
|
||||
|
||||
export const getExams = async (
|
||||
@@ -28,9 +28,10 @@ export const getExams = async (
|
||||
})),
|
||||
) as Exam[];
|
||||
|
||||
const variantExams: Exam[] = filterByVariant(allExams, variant);
|
||||
const genderedExams: Exam[] = filterByInstructorGender(variantExams, instructorGender);
|
||||
const difficultyExams: Exam[] = await filterByDifficulty(db, genderedExams, module, userId);
|
||||
let exams: Exam[] = filterByVariant(allExams, variant);
|
||||
exams = filterByInstructorGender(exams, instructorGender);
|
||||
exams = await filterByDifficulty(db, exams, module, userId);
|
||||
exams = await filterByPreference(db, exams, module, userId);
|
||||
|
||||
if (avoidRepeated === "true") {
|
||||
const statsQ = query(collection(db, "stats"), where("user", "==", userId));
|
||||
@@ -40,12 +41,12 @@ export const getExams = async (
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
})) as unknown as Stat[];
|
||||
const filteredExams = difficultyExams.filter((x) => !stats.map((s) => s.exam).includes(x.id));
|
||||
const filteredExams = exams.filter((x) => !stats.map((s) => s.exam).includes(x.id));
|
||||
|
||||
return filteredExams.length > 0 ? filteredExams : difficultyExams;
|
||||
return filteredExams.length > 0 ? filteredExams : exams;
|
||||
}
|
||||
|
||||
return difficultyExams;
|
||||
return exams;
|
||||
};
|
||||
|
||||
const filterByInstructorGender = (exams: Exam[], instructorGender?: InstructorGender) => {
|
||||
@@ -70,3 +71,25 @@ const filterByDifficulty = async (db: Firestore, exams: Exam[], module: Module,
|
||||
const filteredExams = exams.filter((exam) => exam.difficulty === difficulty);
|
||||
return filteredExams.length === 0 ? exams : filteredExams;
|
||||
};
|
||||
|
||||
const filterByPreference = async (db: Firestore, exams: Exam[], module: Module, userID?: string) => {
|
||||
if (!["speaking", "writing"].includes(module)) return exams;
|
||||
|
||||
if (!userID) return exams;
|
||||
const userRef = await getDoc(doc(db, "users", userID));
|
||||
if (!userRef.exists()) return exams;
|
||||
|
||||
const user = {...userRef.data(), id: userRef.id} as StudentUser | DeveloperUser;
|
||||
if (!["developer", "student"].includes(user.type)) return exams;
|
||||
if (!user.preferredTopics || user.preferredTopics.length === 0) return exams;
|
||||
|
||||
const userTopics = user.preferredTopics;
|
||||
const topicalExams = exams.filter((e) => {
|
||||
const exam = e as WritingExam | SpeakingExam;
|
||||
const topics = exam.exercises.map((x) => x.topic).filter((x) => !!x) as string[];
|
||||
|
||||
return topics.some((topic) => userTopics.map((x) => x.toLowerCase()).includes(topic.toLowerCase()));
|
||||
});
|
||||
|
||||
return topicalExams.length > 0 ? shuffle(topicalExams) : exams;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user