diff --git a/src/components/Exercises/Writing.tsx b/src/components/Exercises/Writing.tsx
index 6004a198..c8189421 100644
--- a/src/components/Exercises/Writing.tsx
+++ b/src/components/Exercises/Writing.tsx
@@ -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">
diff --git a/src/components/High/Layout.tsx b/src/components/High/Layout.tsx
index 9feadad1..79eb2d2a 100644
--- a/src/components/High/Layout.tsx
+++ b/src/components/High/Layout.tsx
@@ -34,6 +34,7 @@ export default function Layout({user, children, className, navDisabled = false,
onFocusLayerMouseEnter={onFocusLayerMouseEnter}
className="-md:hidden"
userType={user.type}
+ userId={user.id}
/>
state.setHasExamEnded);
+ const {timeSpent} = useExamStore((state) => state);
+
+ useEffect(() => setTimer((prev) => prev - timeSpent), [timeSpent]);
useEffect(() => {
if (!disableTimer) {
diff --git a/src/components/Medium/TopicModal.tsx b/src/components/Medium/TopicModal.tsx
new file mode 100644
index 00000000..ab0bcd17
--- /dev/null
+++ b/src/components/Medium/TopicModal.tsx
@@ -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 (
+
+
+
+
+
Available Topics
+
+ {topics
+ .filter((x) => !selectedTopics.includes(x))
+ .map((x) => (
+
+ {x}
+
+
+ ))}
+
+
+
+
Preferred Topics ({selectedTopics.length || "All"})
+
+ {selectedTopics.map((x) => (
+
+
+ {x}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index cfef0591..041aa7a9 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -1,295 +1,206 @@
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,
- BsPencil,
- BsGraphUp,
- BsChevronBarRight,
- BsChevronBarLeft,
- BsShieldFill,
- BsCloudFill,
- BsCurrencyDollar,
- BsClipboardData,
+ BsFileEarmarkText,
+ BsClockHistory,
+ BsPencil,
+ BsGraphUp,
+ BsChevronBarRight,
+ BsChevronBarLeft,
+ BsShieldFill,
+ BsCloudFill,
+ 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;
- focusMode?: boolean;
- onFocusLayerMouseEnter?: () => void;
- className?: string;
- userType?: Type;
+ path: string;
+ navDisabled?: boolean;
+ focusMode?: boolean;
+ onFocusLayerMouseEnter?: () => void;
+ className?: string;
+ userType?: Type;
+ userId?: string;
}
interface NavProps {
- Icon: IconType;
- label: string;
- path: string;
- keyPath: string;
- disabled?: boolean;
- isMinimized?: boolean;
+ Icon: IconType;
+ label: string;
+ path: string;
+ keyPath: string;
+ disabled?: boolean;
+ isMinimized?: boolean;
+ badge?: number;
}
-const Nav = ({
- Icon,
- label,
- path,
- keyPath,
- disabled = false,
- isMinimized = false,
-}: NavProps) => (
-
-
- {!isMinimized &&
{label}}
-
-);
+const Nav = ({Icon, label, path, keyPath, disabled = false, isMinimized = false, badge}: NavProps) => {
+ return (
+
+
+ {!isMinimized &&
{label}}
+ {!!badge && badge > 0 && (
+
+ {badge}
+
+ )}
+
+ );
+};
-export default function Sidebar({
- path,
- navDisabled = false,
- focusMode = false,
- userType,
- onFocusLayerMouseEnter,
- className,
-}: Props) {
- const router = useRouter();
+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 logout = async () => {
- axios.post("/api/logout").finally(() => {
- setTimeout(() => router.reload(), 500);
- });
- };
+ const {totalAssignedTickets} = useTicketsListener(userId);
- const disableNavigation = preventNavigation(navDisabled, focusMode);
+ useEffect(() => console.log(totalAssignedTickets), [totalAssignedTickets]);
- return (
-
-
-
- {(userType === "student" ||
- userType === "teacher" ||
- userType === "developer") && (
- <>
-
-
- >
- )}
-
-
- {["admin", "developer", "agent", "corporate"].includes(
- userType || "",
- ) && (
-
- )}
- {["admin", "developer", "corporate", "teacher"].includes(
- userType || "",
- ) && (
-
- )}
- {["admin", "developer", "agent"].includes(userType || "") && (
-
- )}
- {userType === "developer" && (
-
- )}
-
-
-
-
-
-
-
- {userType !== "student" && (
-
- )}
- {userType === "developer" && (
-
- )}
-
+ const logout = async () => {
+ axios.post("/api/logout").finally(() => {
+ setTimeout(() => router.reload(), 500);
+ });
+ };
-
-
- {isMinimized ? (
-
- ) : (
-
- )}
- {!isMinimized && (
- Minimize
- )}
-
-
{} : logout}
- 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",
- )}
- >
-
- {!isMinimized && (
- Log Out
- )}
-
-
- {focusMode && (
-
- )}
-
- );
+ const disableNavigation = preventNavigation(navDisabled, focusMode);
+
+ return (
+
+
+
+ {(userType === "student" || userType === "teacher" || userType === "developer") && (
+ <>
+
+
+ >
+ )}
+
+
+ {["admin", "developer", "agent", "corporate"].includes(userType || "") && (
+
+ )}
+ {["admin", "developer", "corporate", "teacher"].includes(userType || "") && (
+
+ )}
+ {["admin", "developer", "agent"].includes(userType || "") && (
+
+ )}
+ {userType === "developer" && (
+
+ )}
+
+
+
+
+
+
+
+ {userType !== "student" && (
+
+ )}
+ {userType === "developer" && (
+
+ )}
+
+
+
+
+ {isMinimized ? : }
+ {!isMinimized && Minimize}
+
+
{} : logout}
+ 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",
+ )}>
+
+ {!isMinimized && Log Out}
+
+
+ {focusMode && }
+
+ );
}
diff --git a/src/exams/Listening.tsx b/src/exams/Listening.tsx
index ab8dfa0d..ef974ab3 100644
--- a/src/exams/Listening.tsx
+++ b/src/exams/Listening.tsx
@@ -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);
diff --git a/src/hooks/useTickets.tsx b/src/hooks/useTickets.tsx
index c90fd639..9a5ba240 100644
--- a/src/hooks/useTickets.tsx
+++ b/src/hooks/useTickets.tsx
@@ -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
([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
- const getData = () => {
+ const getData = useCallback(() => {
setIsLoading(true);
axios
.get(`/api/tickets`)
.then((response) => setTickets(response.data))
.finally(() => setIsLoading(false));
- };
+ }, []);
- useEffect(getData, []);
+ useEffect(getData, [getData]);
return { tickets, isLoading, isError, reload: getData };
}
diff --git a/src/hooks/useTicketsListener.tsx b/src/hooks/useTicketsListener.tsx
new file mode 100644
index 00000000..520ee947
--- /dev/null
+++ b/src/hooks/useTicketsListener.tsx
@@ -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;
diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts
index af431f65..775655d1 100644
--- a/src/interfaces/exam.ts
+++ b/src/interfaces/exam.ts
@@ -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 {
diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts
index c5dc5ad3..bb1ae8ae 100644
--- a/src/interfaces/user.ts
+++ b/src/interfaces/user.ts
@@ -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 {
diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx
index 724a58f7..a8bc3435 100644
--- a/src/pages/(exam)/ExamPage.tsx
+++ b/src/pages/(exam)/ExamPage.tsx
@@ -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,
};
});
diff --git a/src/pages/(generation)/SpeakingGeneration.tsx b/src/pages/(generation)/SpeakingGeneration.tsx
index 33c2431a..1ba762a2 100644
--- a/src/pages/(generation)/SpeakingGeneration.tsx
+++ b/src/pages/(generation)/SpeakingGeneration.tsx
@@ -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 (
diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx
index dac35526..e94d4026 100644
--- a/src/pages/profile.tsx
+++ b/src/pages/profile.tsx
@@ -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
(
user.type === "student" || user.type === "developer" ? user.preferredGender || "varied" : undefined,
);
+ const [preferredTopics, setPreferredTopics] = useState(
+ user.type === "student" || user.type === "developer" ? user.preferredTopics : undefined,
+ );
const [position, setPosition] = useState(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(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,18 +358,41 @@ function UserProfile({user, mutateUser}: Props) {
{preferredGender && ["developer", "student"].includes(user.type) && (
<>
-
-
-
+
+
+
+
+
+
+
+
+
+
+ setIsPreferredTopicsOpen(false)}
+ selectTopics={setPreferredTopics}
+ initialTopics={preferredTopics || []}
+ />
>
)}
diff --git a/src/resources/topics.ts b/src/resources/topics.ts
new file mode 100644
index 00000000..424dbbbf
--- /dev/null
+++ b/src/resources/topics.ts
@@ -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;
diff --git a/src/utils/exams.be.ts b/src/utils/exams.be.ts
index 01b67116..be8d1837 100644
--- a/src/utils/exams.be.ts
+++ b/src/utils/exams.be.ts
@@ -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;
+};