bugsfixed and design changes for generation 13'' screen

This commit is contained in:
José Lima
2025-02-23 18:47:57 +00:00
parent fec3b51553
commit 340ff5a30a
29 changed files with 2292 additions and 1680 deletions

View File

@@ -1,250 +1,325 @@
/* eslint-disable @next/next/no-img-element */
import Head from "next/head";
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {ToastContainer} from "react-toastify";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import {Radio, RadioGroup} from "@headlessui/react";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { ToastContainer } from "react-toastify";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { Radio, RadioGroup } from "@headlessui/react";
import clsx from "clsx";
import {MODULE_ARRAY} from "@/utils/moduleUtils";
import {capitalize} from "lodash";
import { MODULE_ARRAY } from "@/utils/moduleUtils";
import { capitalize } from "lodash";
import Input from "@/components/Low/Input";
import {findAllowedEntities} from "@/utils/permissions";
import {User} from "@/interfaces/user";
import {
findAllowedEntities,
findAllowedEntitiesSomePermissions,
groupAllowedEntitiesByPermissions,
} from "@/utils/permissions";
import { User } from "@/interfaces/user";
import useExamEditorStore from "@/stores/examEditor";
import ExamEditorStore from "@/stores/examEditor/types";
import ExamEditor from "@/components/ExamEditor";
import {mapBy, redirect, serialize} from "@/utils";
import {requestUser} from "@/utils/api";
import {Module} from "@/interfaces";
import {getExam} from "@/utils/exams.be";
import {Exam, Exercise, InteractiveSpeakingExercise, ListeningPart, SpeakingExercise} from "@/interfaces/exam";
import {useEffect, useState} from "react";
import {getEntitiesWithRoles} from "@/utils/entities.be";
import {isAdmin} from "@/utils/users";
import { mapBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import { Module } from "@/interfaces";
import { getExam } from "@/utils/exams.be";
import {
Exam,
Exercise,
InteractiveSpeakingExercise,
ListeningPart,
SpeakingExercise,
} from "@/interfaces/exam";
import { useEffect, useState } from "react";
import { getEntitiesWithRoles } from "@/utils/entities.be";
import { isAdmin } from "@/utils/users";
import axios from "axios";
import {EntityWithRoles} from "@/interfaces/entity";
import { EntityWithRoles } from "@/interfaces/entity";
type Permission = {[key in Module]: boolean};
type Permission = { [key in Module]: boolean };
export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => {
const user = await requestUser(req, res);
if (!user) return redirect("/login");
export const getServerSideProps = withIronSessionSsr(
async ({ req, res, query }) => {
const user = await requestUser(req, res);
if (!user) return redirect("/login");
if (shouldRedirectHome(user)) return redirect("/");
if (shouldRedirectHome(user)) return redirect("/");
const entityIDs = mapBy(user.entities, "id");
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDs);
const entityIDs = mapBy(user.entities, "id");
const permissions: Permission = {
reading: findAllowedEntities(user, entities, `generate_reading`).length > 0,
listening: findAllowedEntities(user, entities, `generate_listening`).length > 0,
writing: findAllowedEntities(user, entities, `generate_writing`).length > 0,
speaking: findAllowedEntities(user, entities, `generate_speaking`).length > 0,
level: findAllowedEntities(user, entities, `generate_level`).length > 0,
};
const entities = await getEntitiesWithRoles(
isAdmin(user) ? undefined : entityIDs
);
const entitiesAllowEditPrivacy = findAllowedEntities(user, entities, "update_exam_privacy");
console.log(entitiesAllowEditPrivacy);
const generatePermissions = groupAllowedEntitiesByPermissions(
user,
entities,
[
"generate_reading",
"generate_listening",
"generate_writing",
"generate_speaking",
"generate_level",
]
);
if (Object.keys(permissions).every((p) => !permissions[p as Module])) return redirect("/");
const permissions: Permission = {
reading: generatePermissions["generate_reading"].length > 0,
listening: generatePermissions["generate_listening"].length > 0,
writing: generatePermissions["generate_writing"].length > 0,
speaking: generatePermissions["generate_speaking"].length > 0,
level: generatePermissions["generate_level"].length > 0,
};
const {id, module: examModule} = query as {id?: string; module?: Module};
if (!id || !examModule) return {props: serialize({user, permissions})};
const {
["update_exam_privacy"]: entitiesAllowEditPrivacy,
["create_confidential_exams"]: entitiesAllowConfExams,
["create_public_exams"]: entitiesAllowPublicExams,
} = groupAllowedEntitiesByPermissions(user, entities, [
"update_exam_privacy",
"create_confidential_exams",
"create_public_exams",
]);
//if (!permissions[module]) return redirect("/generation")
if (Object.keys(permissions).every((p) => !permissions[p as Module]))
return redirect("/");
const exam = await getExam(examModule, id);
if (!exam) return redirect("/generation");
const { id, module: examModule } = query as {
id?: string;
module?: Module;
};
if (!id || !examModule) return { props: serialize({ user, permissions }) };
return {
props: serialize({id, user, exam, examModule, permissions, entitiesAllowEditPrivacy}),
};
}, sessionOptions);
//if (!permissions[module]) return redirect("/generation")
const exam = await getExam(examModule, id);
if (!exam) return redirect("/generation");
return {
props: serialize({
id,
user,
exam,
examModule,
permissions,
entitiesAllowEditPrivacy,
entitiesAllowConfExams,
entitiesAllowPublicExams,
}),
};
},
sessionOptions
);
export default function Generation({
id,
user,
exam,
examModule,
permissions,
entitiesAllowEditPrivacy,
id,
user,
exam,
examModule,
permissions,
entitiesAllowEditPrivacy,
entitiesAllowConfExams,
entitiesAllowPublicExams,
}: {
id: string;
user: User;
exam?: Exam;
examModule?: Module;
permissions: Permission;
entitiesAllowEditPrivacy: EntityWithRoles[];
id: string;
user: User;
exam?: Exam;
examModule?: Module;
permissions: Permission;
entitiesAllowEditPrivacy: EntityWithRoles[];
entitiesAllowPublicExams: EntityWithRoles[];
entitiesAllowConfExams: EntityWithRoles[];
}) {
const {title, currentModule, modules, dispatch} = useExamEditorStore();
const [examLevelParts, setExamLevelParts] = useState<number | undefined>(undefined);
const { title, currentModule, modules, dispatch } = useExamEditorStore();
const [examLevelParts, setExamLevelParts] = useState<number | undefined>(
undefined
);
const updateRoot = (updates: Partial<ExamEditorStore>) => {
dispatch({type: "UPDATE_ROOT", payload: {updates}});
};
const updateRoot = (updates: Partial<ExamEditorStore>) => {
dispatch({ type: "UPDATE_ROOT", payload: { updates } });
};
useEffect(() => {
if (id && exam && examModule) {
if (examModule === "level" && exam.module === "level") {
setExamLevelParts(exam.parts.length);
}
updateRoot({currentModule: examModule});
dispatch({type: "INIT_EXAM_EDIT", payload: {exam, id, examModule}});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, exam, module]);
useEffect(() => {
if (id && exam && examModule) {
if (examModule === "level" && exam.module === "level") {
setExamLevelParts(exam.parts.length);
}
updateRoot({ currentModule: examModule });
dispatch({ type: "INIT_EXAM_EDIT", payload: { exam, id, examModule } });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, exam, module]);
useEffect(() => {
const fetchAvatars = async () => {
const response = await axios.get("/api/exam/avatars");
updateRoot({speakingAvatars: response.data});
};
useEffect(() => {
const fetchAvatars = async () => {
const response = await axios.get("/api/exam/avatars");
updateRoot({ speakingAvatars: response.data });
};
fetchAvatars();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
fetchAvatars();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// media cleanup on unmount
useEffect(() => {
return () => {
const state = modules;
// media cleanup on unmount
useEffect(() => {
return () => {
const state = modules;
if (state.writing.academic_url) {
URL.revokeObjectURL(state.writing.academic_url);
}
if (state.writing.academic_url) {
URL.revokeObjectURL(state.writing.academic_url);
}
state.listening.sections.forEach((section) => {
const listeningPart = section.state as ListeningPart;
if (listeningPart.audio?.source) {
URL.revokeObjectURL(listeningPart.audio.source);
dispatch({
type: "UPDATE_SECTION_SINGLE_FIELD",
payload: {
sectionId: section.sectionId,
module: "listening",
field: "state",
value: {...listeningPart, audio: undefined},
},
});
}
});
state.listening.sections.forEach((section) => {
const listeningPart = section.state as ListeningPart;
if (listeningPart.audio?.source) {
URL.revokeObjectURL(listeningPart.audio.source);
dispatch({
type: "UPDATE_SECTION_SINGLE_FIELD",
payload: {
sectionId: section.sectionId,
module: "listening",
field: "state",
value: { ...listeningPart, audio: undefined },
},
});
}
});
if (state.listening.instructionsState.customInstructionsURL.startsWith("blob:")) {
URL.revokeObjectURL(state.listening.instructionsState.customInstructionsURL);
}
if (
state.listening.instructionsState.customInstructionsURL.startsWith(
"blob:"
)
) {
URL.revokeObjectURL(
state.listening.instructionsState.customInstructionsURL
);
}
state.speaking.sections.forEach((section) => {
const sectionState = section.state as Exercise;
if (sectionState.type === "speaking") {
const speakingExercise = sectionState as SpeakingExercise;
URL.revokeObjectURL(speakingExercise.video_url);
dispatch({
type: "UPDATE_SECTION_SINGLE_FIELD",
payload: {
sectionId: section.sectionId,
module: "listening",
field: "state",
value: {...speakingExercise, video_url: undefined},
},
});
}
if (sectionState.type === "interactiveSpeaking") {
const interactiveSpeaking = sectionState as InteractiveSpeakingExercise;
interactiveSpeaking.prompts.forEach((prompt) => {
URL.revokeObjectURL(prompt.video_url);
});
dispatch({
type: "UPDATE_SECTION_SINGLE_FIELD",
payload: {
sectionId: section.sectionId,
module: "listening",
field: "state",
value: {
...interactiveSpeaking,
prompts: interactiveSpeaking.prompts.map((p) => ({...p, video_url: undefined})),
},
},
});
}
});
dispatch({type: "FULL_RESET"});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
state.speaking.sections.forEach((section) => {
const sectionState = section.state as Exercise;
if (sectionState.type === "speaking") {
const speakingExercise = sectionState as SpeakingExercise;
URL.revokeObjectURL(speakingExercise.video_url);
dispatch({
type: "UPDATE_SECTION_SINGLE_FIELD",
payload: {
sectionId: section.sectionId,
module: "listening",
field: "state",
value: { ...speakingExercise, video_url: undefined },
},
});
}
if (sectionState.type === "interactiveSpeaking") {
const interactiveSpeaking =
sectionState as InteractiveSpeakingExercise;
interactiveSpeaking.prompts.forEach((prompt) => {
URL.revokeObjectURL(prompt.video_url);
});
dispatch({
type: "UPDATE_SECTION_SINGLE_FIELD",
payload: {
sectionId: section.sectionId,
module: "listening",
field: "state",
value: {
...interactiveSpeaking,
prompts: interactiveSpeaking.prompts.map((p) => ({
...p,
video_url: undefined,
})),
},
},
});
}
});
dispatch({ type: "FULL_RESET" });
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<Head>
<title>Exam Generation | EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
{user && (
<>
<h1 className="text-2xl font-semibold">Exam Editor</h1>
<div className="flex flex-col gap-3">
<Input
type="text"
placeholder="Insert a title here"
name="title"
label="Title"
onChange={(title) => updateRoot({title})}
roundness="xl"
value={title}
defaultValue={title}
required
/>
<label className="font-normal text-base text-mti-gray-dim">Module</label>
<RadioGroup
value={currentModule}
onChange={(currentModule) => updateRoot({currentModule})}
className="flex flex-row flex-wrap w-full gap-4 -md:justify-center justify-between">
{[...MODULE_ARRAY]
.filter((m) => permissions[m])
.map((x) => (
<Radio value={x} key={x}>
{({checked}) => (
<span
className={clsx(
"px-6 py-4 w-64 h-[72px] flex justify-center items-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"transition duration-300 ease-in-out",
x === "reading" &&
(!checked
? "bg-white border-mti-gray-platinum"
: "bg-ielts-reading/70 border-ielts-reading text-white"),
x === "listening" &&
(!checked
? "bg-white border-mti-gray-platinum"
: "bg-ielts-listening/70 border-ielts-listening text-white"),
x === "writing" &&
(!checked
? "bg-white border-mti-gray-platinum"
: "bg-ielts-writing/70 border-ielts-writing text-white"),
x === "speaking" &&
(!checked
? "bg-white border-mti-gray-platinum"
: "bg-ielts-speaking/70 border-ielts-speaking text-white"),
x === "level" &&
(!checked
? "bg-white border-mti-gray-platinum"
: "bg-ielts-level/70 border-ielts-level text-white"),
)}>
{capitalize(x)}
</span>
)}
</Radio>
))}
</RadioGroup>
</div>
<ExamEditor levelParts={examLevelParts} entitiesAllowEditPrivacy={entitiesAllowEditPrivacy} />
</>
)}
</>
);
return (
<>
<Head>
<title>Exam Generation | EnCoach</title>
<meta
name="description"
content="A training platform for the IELTS exam provided by the Muscat Training Institute and developed by eCrop."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ToastContainer />
{user && (
<>
<h1 className="text-2xl font-semibold">Exam Editor</h1>
<div className="flex flex-col gap-3">
<Input
type="text"
placeholder="Insert a title here"
name="title"
label="Title"
onChange={(title) => updateRoot({ title })}
roundness="xl"
value={title}
defaultValue={title}
required
/>
<label className="font-normal text-base text-mti-gray-dim">
Module
</label>
<RadioGroup
value={currentModule}
onChange={(currentModule) => updateRoot({ currentModule })}
className="flex flex-row flex-wrap w-full gap-4 -md:justify-center justify-between"
>
{[...MODULE_ARRAY].reduce((acc, x) => {
if (permissions[x])
acc.push(
<Radio value={x} key={x}>
{({ checked }) => (
<span
className={clsx(
"px-6 py-4 w-64 h-[72px] flex justify-center items-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"transition duration-300 ease-in-out",
x === "reading" &&
(!checked
? "bg-white border-mti-gray-platinum"
: "bg-ielts-reading/70 border-ielts-reading text-white"),
x === "listening" &&
(!checked
? "bg-white border-mti-gray-platinum"
: "bg-ielts-listening/70 border-ielts-listening text-white"),
x === "writing" &&
(!checked
? "bg-white border-mti-gray-platinum"
: "bg-ielts-writing/70 border-ielts-writing text-white"),
x === "speaking" &&
(!checked
? "bg-white border-mti-gray-platinum"
: "bg-ielts-speaking/70 border-ielts-speaking text-white"),
x === "level" &&
(!checked
? "bg-white border-mti-gray-platinum"
: "bg-ielts-level/70 border-ielts-level text-white")
)}
>
{capitalize(x)}
</span>
)}
</Radio>
);
return acc;
}, [] as JSX.Element[])}
</RadioGroup>
</div>
<ExamEditor
levelParts={examLevelParts}
entitiesAllowEditPrivacy={entitiesAllowEditPrivacy}
entitiesAllowConfExams={entitiesAllowConfExams}
entitiesAllowPublicExams={entitiesAllowPublicExams}
/>
</>
)}
</>
);
}