diff --git a/public/audio/error.mp3 b/public/audio/error.mp3 new file mode 100644 index 00000000..8e3f03a7 Binary files /dev/null and b/public/audio/error.mp3 differ diff --git a/src/components/Diagnostic.tsx b/src/components/Diagnostic.tsx index a221f1d2..f7b448f4 100644 --- a/src/components/Diagnostic.tsx +++ b/src/components/Diagnostic.tsx @@ -36,7 +36,7 @@ export default function Diagnostic({onFinish}: Props) { }; const selectExam = () => { - const examPromises = MODULE_ARRAY.map((module) => getExam(module, true)); + const examPromises = MODULE_ARRAY.map((module) => getExam(module, true, "partial")); Promise.all(examPromises).then((exams) => { if (exams.every((x) => !!x)) { diff --git a/src/components/Low/CountrySelect.tsx b/src/components/Low/CountrySelect.tsx index 838395da..b9a5a110 100644 --- a/src/components/Low/CountrySelect.tsx +++ b/src/components/Low/CountrySelect.tsx @@ -42,7 +42,9 @@ export default function CountrySelect({value, disabled = false, onChange}: Props displayValue={(code: string) => { const country = countries[code as unknown as keyof TCountries]; - return `${countryCodes.findOne("countryCode" as any, code).flag} ${country.name} (+${country.phone})`; + return `${countryCodes.findOne("countryCode" as any, code)?.flag || ""} ${country?.name || "N/A"} (+${ + country?.phone || "N/A" + })`; }} /> diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index b9da2b6c..19712a67 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -45,8 +45,9 @@ const Nav = ({Icon, label, path, keyPath, disabled = false, isMinimized = false} diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx index 4c0c48d3..73a33110 100644 --- a/src/components/UserCard.tsx +++ b/src/components/UserCard.tsx @@ -243,7 +243,9 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, options={CURRENCIES_OPTIONS} value={CURRENCIES_OPTIONS.find((c) => c.value === paymentCurrency)} onChange={(value) => setPaymentCurrency(value?.value)} + menuPortalTarget={document?.body} styles={{ + menuPortal: (base) => ({...base, zIndex: 9999}), control: (styles) => ({ ...styles, paddingLeft: "4px", @@ -282,8 +284,10 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, value: referralAgent, label: referralAgentLabel, }} + menuPortalTarget={document?.body} onChange={(value) => setReferralAgent(value?.value)} styles={{ + menuPortal: (base) => ({...base, zIndex: 9999}), control: (styles) => ({ ...styles, paddingLeft: "4px", @@ -314,7 +318,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, type="number" defaultValue={commissionValue || 0} className="col-span-3" - disabled={disabled} + disabled={disabled || loggedInUser.type === "agent"} /> ) : ( @@ -520,6 +524,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, o.value === type)} onChange={(value) => setType(value?.value as typeof user.type)} styles={{ @@ -558,6 +565,7 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, outline: "none", }, }), + menuPortal: (base) => ({...base, zIndex: 9999}), option: (styles, state) => ({ ...styles, backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", @@ -574,17 +582,17 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers,
- {onViewCorporate && ( + {onViewCorporate && ["student", "teacher"].includes(user.type) && ( )} - {onViewStudents && ( + {onViewStudents && ["corporate", "teacher"].includes(user.type) && ( )} - {onViewTeachers && ( + {onViewTeachers && ["student", "corporate"].includes(user.type) && ( diff --git a/src/constants/userPermissions.ts b/src/constants/userPermissions.ts index e0251e8c..734e9267 100644 --- a/src/constants/userPermissions.ts +++ b/src/constants/userPermissions.ts @@ -18,8 +18,8 @@ export const PERMISSIONS = { developer: ["developer"], }, updateUser: { - student: ["teacher", "corporate", "developer", "admin"], - teacher: ["corporate", "developer", "admin"], + student: ["developer", "admin"], + teacher: ["developer", "admin"], corporate: ["admin", "developer"], admin: ["developer", "admin"], agent: ["developer", "admin"], diff --git a/src/dashboards/Teacher.tsx b/src/dashboards/Teacher.tsx index e40d5a28..2d181089 100644 --- a/src/dashboards/Teacher.tsx +++ b/src/dashboards/Teacher.tsx @@ -151,8 +151,9 @@ export default function TeacherDashboard({user}: Props) { }; const AssignmentsPage = () => { - const activeFilter = (a: Assignment) => moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()); - const pastFilter = (a: Assignment) => moment(a.endDate).isBefore(moment()); + const activeFilter = (a: Assignment) => + moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment()) && a.assignees.length > a.results.length; + const pastFilter = (a: Assignment) => moment(a.endDate).isBefore(moment()) || a.assignees.length === a.results.length; const futureFilter = (a: Assignment) => moment(a.startDate).isAfter(moment()); return ( diff --git a/src/exams/Selection.tsx b/src/exams/Selection.tsx index 2485bb70..02dcb681 100644 --- a/src/exams/Selection.tsx +++ b/src/exams/Selection.tsx @@ -12,17 +12,19 @@ import {calculateAverageLevel} from "@/utils/score"; import {sortByModuleName} from "@/utils/moduleUtils"; import {capitalize} from "lodash"; import ProfileSummary from "@/components/ProfileSummary"; +import {Variant} from "@/interfaces/exam"; interface Props { user: User; page: "exercises" | "exams"; - onStart: (modules: Module[], avoidRepeated: boolean) => void; + onStart: (modules: Module[], avoidRepeated: boolean, variant: Variant) => void; disableSelection?: boolean; } export default function Selection({user, page, onStart, disableSelection = false}: Props) { const [selectedModules, setSelectedModules] = useState([]); const [avoidRepeatedExams, setAvoidRepeatedExams] = useState(true); + const [variant, setVariant] = useState("full"); const {stats} = useStats(user?.id); const toggleModule = (module: Module) => { @@ -202,20 +204,37 @@ export default function Selection({user, page, onStart, disableSelection = false )}
-
setAvoidRepeatedExams((prev) => !prev)}> - +
- + className="flex gap-3 items-center text-mti-gray-dim text-sm cursor-pointer w-full -md:justify-center" + onClick={() => setAvoidRepeatedExams((prev) => !prev)}> + +
+ +
+ + Avoid Repeated Questions + +
+
setVariant((prev) => (prev === "full" ? "partial" : "full"))}> + +
+ +
+ Full length exams
- Avoid Repeated Questions
)} )} +
{isLoading && (
@@ -59,7 +104,7 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se Generating...
)} - {part && ( + {part && !isLoading && (

{part.topic}

{part.question && {part.question}} @@ -82,6 +127,7 @@ const PartTab = ({part, index, setPart}: {part?: SpeakingPart; index: number; se ))}
)} + {part.result && Video Generated: ✅}
)} @@ -93,27 +139,43 @@ interface SpeakingPart { question?: string; questions?: string[]; topic: string; + result?: SpeakingExercise | InteractiveSpeakingExercise; } const SpeakingGeneration = () => { const [part1, setPart1] = useState(); const [part2, setPart2] = useState(); const [part3, setPart3] = useState(); + const [minTimer, setMinTimer] = useState(14); const [isLoading, setIsLoading] = useState(false); const [resultingExam, setResultingExam] = useState(); + useEffect(() => { + const parts = [part1, part2, part3].filter((x) => !!x); + setMinTimer(parts.length === 0 ? 5 : parts.length * 5); + }, [part1, part2, part3]); + const router = useRouter(); const setExams = useExamStore((state) => state.setExams); const setSelectedModules = useExamStore((state) => state.setSelectedModules); const submitExam = () => { - if (!part1 || !part2 || !part3) return toast.error("Please generate all for tasks!"); + if (!part1?.result && !part2?.result && !part3?.result) return toast.error("Please generate at least one task!"); setIsLoading(true); + const exam: SpeakingExam = { + id: v4(), + isDiagnostic: false, + exercises: [part1?.result, part2?.result, part3?.result].filter((x) => !!x) as (SpeakingExercise | InteractiveSpeakingExercise)[], + minTimer, + variant: minTimer >= 14 ? "full" : "partial", + module: "speaking", + }; + axios - .post(`/api/exam/speaking/generate/speaking`, {exercises: [part1, part2, part3]}) + .post(`/api/exam/speaking`, exam) .then((result) => { playSound("sent"); console.log(`Generated Exam ID: ${result.data.id}`); @@ -123,10 +185,11 @@ const SpeakingGeneration = () => { setPart1(undefined); setPart2(undefined); setPart3(undefined); + setMinTimer(14); }) .catch((error) => { console.log(error); - toast.error("Something went wrong!"); + toast.error("Something went wrong while generating, please try again later."); }) .finally(() => setIsLoading(false)); }; @@ -149,40 +212,51 @@ const SpeakingGeneration = () => { return ( <> +
+ + setMinTimer(parseInt(e) < 5 ? 5 : parseInt(e))} + value={minTimer} + className="max-w-[300px]" + /> +
+ clsx( - "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70", + "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70 flex gap-2 items-center justify-center", "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking 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-speaking", ) }> - Task 1 + Exercise 1 {part1 && part1.result && } clsx( - "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70", + "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70 flex gap-2 items-center justify-center", "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking 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-speaking", ) }> - Task 2 + Exercise 2 {part2 && part2.result && } clsx( - "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70", + "w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-speaking/70 flex gap-2 items-center justify-center", "ring-white ring-opacity-60 ring-offset-2 ring-offset-ielts-speaking 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-speaking", ) }> - Task 3 + Interactive {part3 && part3.result && } @@ -209,14 +283,14 @@ const SpeakingGeneration = () => { )} )}