diff --git a/package.json b/package.json index 28d3f1f6..61673f4e 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "iron-session": "^6.3.1", "lodash": "^4.17.21", "moment": "^2.29.4", + "moment-timezone": "^0.5.44", "next": "13.1.6", "nodemailer": "^6.9.5", "nodemailer-express-handlebars": "^6.1.0", @@ -54,6 +55,7 @@ "react-csv": "^2.2.2", "react-currency-input-field": "^3.6.12", "react-datepicker": "^4.18.0", + "react-diff-viewer": "^3.1.1", "react-dom": "18.2.0", "react-firebase-hooks": "^5.1.1", "react-icons": "^4.8.0", diff --git a/src/components/DemographicInformationInput.tsx b/src/components/DemographicInformationInput.tsx index f8d67320..c97a4aef 100644 --- a/src/components/DemographicInformationInput.tsx +++ b/src/components/DemographicInformationInput.tsx @@ -10,6 +10,8 @@ import axios from "axios"; import {toast} from "react-toastify"; import {KeyedMutator} from "swr"; import CountrySelect from "./Low/CountrySelect"; +import GenderInput from "@/components/High/GenderInput"; +import EmploymentStatusInput from "@/components/High/EmploymentStatusInput"; interface Props { user: User; @@ -92,73 +94,11 @@ export default function DemographicInformationInput({user, mutateUser}: Props) { required /> )} -
- - - - {({checked}) => ( - - Male - - )} - - - {({checked}) => ( - - Female - - )} - - - {({checked}) => ( - - Other - - )} - - -
+ {user.type === "corporate" && ( )} - {user.type !== "corporate" && ( -
- - - {EMPLOYMENT_STATUS.map(({status, label}) => ( - - {({checked}) => ( - - {label} - - )} - - ))} - -
- )} + {user.type !== "corporate" && }
diff --git a/src/components/Exercises/FillBlanks.tsx b/src/components/Exercises/FillBlanks.tsx index 0d7798d3..b0433ac6 100644 --- a/src/components/Exercises/FillBlanks.tsx +++ b/src/components/Exercises/FillBlanks.tsx @@ -53,7 +53,7 @@ function WordsDrawer({words, isOpen, blankId, previouslySelectedWord, onCancel,
+ )}
)} {userSolutions && userSolutions.length > 0 && userSolutions[0].evaluation && typeof userSolutions[0].evaluation !== "string" && ( diff --git a/src/components/Solutions/index.tsx b/src/components/Solutions/index.tsx index d61dc449..2926703a 100644 --- a/src/components/Solutions/index.tsx +++ b/src/components/Solutions/index.tsx @@ -30,20 +30,28 @@ export interface CommonProps { export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: () => void, updateIndex?: (internalIndex: number) => void) => { switch (exercise.type) { case "fillBlanks": - return ; + return ; case "trueFalse": - return ; + return ; case "matchSentences": - return ; + return ; case "multipleChoice": - return ; + return ( + + ); case "writeBlanks": - return ; + return ; case "writing": - return ; + return ; case "speaking": - return ; + return ; case "interactiveSpeaking": - return ; + return ; } }; diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx index 8ded32f4..4c0c48d3 100644 --- a/src/components/UserCard.tsx +++ b/src/components/UserCard.tsx @@ -74,11 +74,11 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, user.type === "corporate" ? user.corporateInformation?.companyInformation.name : user.type === "agent" - ? user.agentInformation.companyName + ? user.agentInformation?.companyName : undefined, ); const [commercialRegistration, setCommercialRegistration] = useState( - user.type === "agent" ? user.agentInformation.commercialRegistration : undefined, + user.type === "agent" ? user.agentInformation?.commercialRegistration : undefined, ); const [userAmount, setUserAmount] = useState(user.type === "corporate" ? user.corporateInformation?.companyInformation.userAmount : undefined); const [paymentValue, setPaymentValue] = useState(user.type === "corporate" ? user.corporateInformation?.payment?.value : undefined); @@ -236,7 +236,10 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, disabled={disabled} /> u.type === "agent").map((x) => ({value: x.id, label: `${x.name} - ${x.email}`})), @@ -293,12 +300,12 @@ const UserCard = ({user, loggedInUser, onClose, onViewStudents, onViewTeachers, }), }} // editing country manager should only be available for dev/admin - isDisabled={!['developer', 'admin'].includes(loggedInUser.type)} + isDisabled={!["developer", "admin"].includes(loggedInUser.type)} /> )}
- {referralAgent !== "" ? ( + {referralAgent !== "" && loggedInUser.type !== "corporate" ? ( <> (); + const {stats} = useStats(user.id); const {assignments, isLoading: isAssignmentsLoading, reload: reloadAssignments} = useAssignments({assignees: user?.id}); @@ -37,6 +41,10 @@ export default function StudentDashboard({user}: Props) { const setSelectedModules = useExamStore((state) => state.setSelectedModules); const setAssignment = useExamStore((state) => state.setAssignment); + useEffect(() => { + getUserCorporate("IXdh9EQziAVXXh0jOiC5cPVlgS82").then(setCorporateUserToShow); + }, [user]); + const startAssignment = (assignment: Assignment) => { const examPromises = assignment.exams.filter((e) => e.assignee === user.id).map((e) => getExamById(e.module, e.id)); @@ -60,6 +68,11 @@ export default function StudentDashboard({user}: Props) { return ( <> + {corporateUserToShow && ( +
+ Linked to: {corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name} +
+ )} (); const [isCreatingAssignment, setIsCreatingAssignment] = useState(false); + const [corporateUserToShow, setCorporateUserToShow] = useState(); const {stats} = useStats(); const {users, reload} = useUsers(); @@ -65,6 +67,10 @@ export default function TeacherDashboard({user}: Props) { setShowModal(!!selectedUser && page === ""); }, [selectedUser, page]); + useEffect(() => { + getUserCorporate(user.id).then(setCorporateUserToShow); + }, [user]); + const studentFilter = (user: User) => user.type === "student" && groups.flatMap((g) => g.participants).includes(user.id); const getStatsByStudent = (user: User) => stats.filter((s) => s.user === user.id); @@ -226,7 +232,7 @@ export default function TeacherDashboard({user}: Props) {

Past Assignments ({assignments.filter(pastFilter).length})

{assignments.filter(pastFilter).map((a) => ( - setSelectedAssignment(a)} key={a.id} allowDownload/> + setSelectedAssignment(a)} key={a.id} allowDownload /> ))}
@@ -236,7 +242,16 @@ export default function TeacherDashboard({user}: Props) { const DefaultDashboard = () => ( <> -
+ {corporateUserToShow && ( +
+ Linked to: {corporateUserToShow?.corporateInformation?.companyInformation.name || corporateUserToShow.name} +
+ )} +
setPage("students")} Icon={BsPersonFill} diff --git a/src/exams/Listening.tsx b/src/exams/Listening.tsx index 5534d09a..56ee61d1 100644 --- a/src/exams/Listening.tsx +++ b/src/exams/Listening.tsx @@ -175,7 +175,7 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
)} - {exerciseIndex === -1 && ( + {exerciseIndex === -1 && partIndex === 0 && ( diff --git a/src/exams/pdf/group.test.report.tsx b/src/exams/pdf/group.test.report.tsx index 4b865494..6d98cd8a 100644 --- a/src/exams/pdf/group.test.report.tsx +++ b/src/exams/pdf/group.test.report.tsx @@ -33,6 +33,7 @@ interface Props { summaryPNG: string; summaryScore: string; groupScoreSummary: any[]; + passportId: string; } const customStyles = StyleSheet.create({ @@ -81,6 +82,7 @@ const GroupTestReport = ({ summaryPNG, summaryScore, groupScoreSummary, + passportId, }: Props) => { const defaultTextStyle = [styles.textFont, { fontSize: 8 }]; return ( @@ -114,6 +116,7 @@ const GroupTestReport = ({ ID: {id} Email: {email} Gender: {gender} + Passport ID: {passportId} Total Number of Students: {numberOfStudents} @@ -203,7 +206,7 @@ const GroupTestReport = ({ percentage={percent} /> - + {percent}% {description} diff --git a/src/exams/pdf/styles.ts b/src/exams/pdf/styles.ts index a8162499..0a0d7e4b 100644 --- a/src/exams/pdf/styles.ts +++ b/src/exams/pdf/styles.ts @@ -28,6 +28,9 @@ export const styles = StyleSheet.create({ fontFamily: "Helvetica-Bold", fontWeight: "bold", }, + textNormal: { + fontWeight: "normal", + }, textColor: { color: "#4e4969", }, @@ -55,7 +58,6 @@ export const styles = StyleSheet.create({ display: "flex", flexDirection: "column", alignItems: "center", - gap: 4, position: "relative", }, radialResultContainer: { diff --git a/src/exams/pdf/test.report.footer.tsx b/src/exams/pdf/test.report.footer.tsx index 884ad2f0..3771fee7 100644 --- a/src/exams/pdf/test.report.footer.tsx +++ b/src/exams/pdf/test.report.footer.tsx @@ -18,18 +18,18 @@ const TestReportFooter = () => ( > - Validity + Validity This report remains valid for a duration of three months from the test date. - Confidential – circulated for concern people + Confidential – circulated for concern people - Declaration + Declaration We hereby declare that exam results on our platform, assessed by AI, are not the sole determinants of candidates' English proficiency diff --git a/src/exams/pdf/test.report.tsx b/src/exams/pdf/test.report.tsx index acd14481..9c409554 100644 --- a/src/exams/pdf/test.report.tsx +++ b/src/exams/pdf/test.report.tsx @@ -25,6 +25,9 @@ interface Props { qrcode: string; renderDetails: React.ReactNode; title: string; + summaryPNG: string; + summaryScore: string; + passportId: string; } const TestReport = ({ @@ -39,6 +42,9 @@ const TestReport = ({ logo, qrcode, renderDetails, + summaryPNG, + summaryScore, + passportId, }: Props) => { const defaultTextStyle = [styles.textFont, { fontSize: 8 }]; const defaultSkillsTextStyle = [styles.textFont, { fontSize: 8 }]; @@ -79,6 +85,7 @@ const TestReport = ({ ID: {id} Email: {email} Gender: {gender} + Passport ID: {passportId} Performance Summary - - {summary} + + + {summary} + + + + + {summaryScore} + + diff --git a/src/hooks/useUser.tsx b/src/hooks/useUser.tsx index df6e9d43..47b9a36e 100644 --- a/src/hooks/useUser.tsx +++ b/src/hooks/useUser.tsx @@ -15,10 +15,10 @@ export default function useUser({redirectTo = "", redirectIfFound = false} = {}) if (!redirectTo || !user) return; if ( - // If redirectTo is set, redirect if the user was not found. - (redirectTo && !redirectIfFound && (!user || (user && !user.isVerified))) || // If redirectIfFound is also set, redirect if the user was found - (redirectIfFound && user && user.isVerified) + (redirectIfFound && user && user.isVerified) || + // If redirectTo is set, redirect if the user was not found. + (redirectTo && !redirectIfFound && (!user || (user && !user.isVerified))) ) { Router.push(redirectTo); } diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index 99185964..bd4802c7 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -1,6 +1,7 @@ import {Module} from "."; export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam; +export type Variant = "diagnostic" | "partial"; export interface ReadingExam { parts: ReadingPart[]; @@ -9,6 +10,7 @@ export interface ReadingExam { minTimer: number; type: "academic" | "general"; isDiagnostic: boolean; + variant?: Variant; } export interface ReadingPart { @@ -25,6 +27,7 @@ export interface LevelExam { exercises: Exercise[]; minTimer: number; isDiagnostic: boolean; + variant?: Variant; } export interface ListeningExam { @@ -33,6 +36,7 @@ export interface ListeningExam { module: "listening"; minTimer: number; isDiagnostic: boolean; + variant?: Variant; } export interface ListeningPart { @@ -63,6 +67,7 @@ export interface WritingExam { exercises: Exercise[]; minTimer: number; isDiagnostic: boolean; + variant?: Variant; } interface WordCounter { @@ -76,6 +81,7 @@ export interface SpeakingExam { exercises: Exercise[]; minTimer: number; isDiagnostic: boolean; + variant?: Variant; } export type Exercise = @@ -104,6 +110,7 @@ interface InteractiveSpeakingEvaluation extends Evaluation { interface CommonEvaluation extends Evaluation { perfect_answer?: string; perfect_answer_1?: string; + fixed_text?: string; } export interface WritingExercise { diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 4a390a3f..dec052ef 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -78,6 +78,7 @@ export interface DemographicInformation { gender: Gender; employment: EmploymentStatus; passport_id?: string; + timezone?: string; } export interface DemographicCorporateInformation { @@ -85,6 +86,7 @@ export interface DemographicCorporateInformation { phone: string; gender: Gender; position: string; + timezone?: string; } export type Gender = "male" | "female" | "other"; diff --git a/src/pages/(admin)/BatchCodeGenerator.tsx b/src/pages/(admin)/BatchCodeGenerator.tsx index e4988a15..4eb1ece0 100644 --- a/src/pages/(admin)/BatchCodeGenerator.tsx +++ b/src/pages/(admin)/BatchCodeGenerator.tsx @@ -14,19 +14,31 @@ import {toast} from "react-toastify"; import ShortUniqueId from "short-unique-id"; import {useFilePicker} from "use-file-picker"; import readXlsxFile from "read-excel-file"; +import Modal from "@/components/Modal"; +import {BsQuestionCircleFill} from "react-icons/bs"; const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/); +const USER_TYPE_PERMISSIONS: {[key in Type]: Type[]} = { + student: [], + teacher: [], + agent: [], + corporate: ["student", "teacher"], + admin: ["student", "teacher", "agent", "corporate", "admin"], + developer: ["student", "teacher", "agent", "corporate", "admin", "developer"], +}; + export default function BatchCodeGenerator({user}: {user: User}) { const [infos, setInfos] = useState<{email: string; name: string; passport_id: string}[]>([]); const [isLoading, setIsLoading] = useState(false); const [expiryDate, setExpiryDate] = useState(null); const [isExpiryDateEnabled, setIsExpiryDateEnabled] = useState(true); const [type, setType] = useState("student"); + const [showHelp, setShowHelp] = useState(false); const {users} = useUsers(); - const {openFilePicker, filesContent} = useFilePicker({ + const {openFilePicker, filesContent, clear} = useFilePicker({ accept: ".xlsx", multiple: false, readAs: "ArrayBuffer", @@ -52,9 +64,9 @@ export default function BatchCodeGenerator({user}: {user: User}) { const [firstName, lastName, country, passport_id, email, phone] = row as string[]; return EMAIL_REGEX.test(email) && !users.map((u) => u.email).includes(email) ? { - email, - name: `${firstName} ${lastName}`, - passport_id, + email: email.toString(), + name: `${firstName ?? ""} ${lastName ?? ""}`.trim(), + passport_id: passport_id.toString(), } : undefined; }) @@ -62,10 +74,12 @@ export default function BatchCodeGenerator({user}: {user: User}) { (x) => x.email, ); - if (information.length === 0) - return toast.error( + if (information.length === 0) { + toast.error( "Please upload an Excel file containing user information, one per line! All already registered e-mails have also been ignored!", ); + return clear(); + } setInfos(information); }); @@ -102,50 +116,85 @@ export default function BatchCodeGenerator({user}: {user: User}) { }; return ( -
- - - {user && (user.type === "developer" || user.type === "admin") && ( - <> -
- - - Enabled - + <> + setShowHelp(false)} title="Excel File Format"> +
+ Please upload an Excel file with the following format: + + + + + + + + + + + +
First NameLast NameCountryPassport/National IDE-mailPhone Number
+ + Notes: +
    +
  • - All incorrect e-mails will be ignored;
  • +
  • - All already registered e-mails will be ignored;
  • +
  • - You may have a header row with the format above, however, it is not necessary;
  • +
  • - All of the e-mails in the file will receive an e-mail to join EnCoach with the role selected below.
  • +
+
+
+
+
+
+ +
setShowHelp(true)}> +
- {isExpiryDateEnabled && ( - moment(date).isAfter(new Date())} - dateFormat="dd/MM/yyyy" - selected={expiryDate} - onChange={(date) => setExpiryDate(date)} - /> - )} - - )} - - {user && ( - - )} - -
+
+ + {user && (user.type === "developer" || user.type === "admin") && ( + <> +
+ + + Enabled + +
+ {isExpiryDateEnabled && ( + moment(date).isAfter(new Date())} + dateFormat="dd/MM/yyyy" + selected={expiryDate} + onChange={(date) => setExpiryDate(date)} + /> + )} + + )} + + {user && ( + + )} + +
+ ); } diff --git a/src/pages/(admin)/CodeGenerator.tsx b/src/pages/(admin)/CodeGenerator.tsx index ba7fb178..bd610954 100644 --- a/src/pages/(admin)/CodeGenerator.tsx +++ b/src/pages/(admin)/CodeGenerator.tsx @@ -12,6 +12,15 @@ import ReactDatePicker from "react-datepicker"; import {toast} from "react-toastify"; import ShortUniqueId from "short-unique-id"; +const USER_TYPE_PERMISSIONS: {[key in Type]: Type[]} = { + student: [], + teacher: [], + agent: [], + corporate: ["student", "teacher"], + admin: ["student", "teacher", "agent", "corporate", "admin"], + developer: ["student", "teacher", "agent", "corporate", "admin", "developer"], +}; + export default function CodeGenerator({user}: {user: User}) { const [generatedCode, setGeneratedCode] = useState(); const [expiryDate, setExpiryDate] = useState(null); @@ -63,11 +72,13 @@ export default function CodeGenerator({user}: {user: User}) { defaultValue="student" onChange={(e) => setType(e.target.value as typeof user.type)} className="p-6 w-full min-w-[350px] min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer bg-white"> - {Object.keys(USER_TYPE_LABELS).map((type) => ( - - ))} + {Object.keys(USER_TYPE_LABELS) + .filter((x) => USER_TYPE_PERMISSIONS[user.type].includes(x as Type)) + .map((type) => ( + + ))} )} {user && (user.type === "developer" || user.type === "admin") && ( diff --git a/src/pages/(admin)/Lists/GroupList.tsx b/src/pages/(admin)/Lists/GroupList.tsx index 9d48254b..1bde57c8 100644 --- a/src/pages/(admin)/Lists/GroupList.tsx +++ b/src/pages/(admin)/Lists/GroupList.tsx @@ -9,16 +9,18 @@ import {Disclosure, Transition} from "@headlessui/react"; import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; import axios from "axios"; import clsx from "clsx"; -import {capitalize} from "lodash"; +import {capitalize, uniq, uniqBy} from "lodash"; import {useEffect, useRef, useState} from "react"; -import {BsCheck, BsDash, BsPencil, BsPlus, BsTrash} from "react-icons/bs"; +import {BsCheck, BsDash, BsPencil, BsPlus, BsQuestionCircleFill, BsTrash} from "react-icons/bs"; import {toast} from "react-toastify"; import Select from "react-select"; import {uuidv4} from "@firebase/util"; import {useFilePicker} from "use-file-picker"; import Modal from "@/components/Modal"; +import readXlsxFile from "read-excel-file"; const columnHelper = createColumnHelper(); +const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/); interface CreateDialogProps { user: User; @@ -31,40 +33,49 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => { const [name, setName] = useState(group?.name || undefined); const [admin, setAdmin] = useState(group?.admin || user.id); const [participants, setParticipants] = useState(group?.participants || []); - const {openFilePicker, filesContent} = useFilePicker({ - accept: ".txt", + const {openFilePicker, filesContent, clear} = useFilePicker({ + accept: ".xlsx", multiple: false, + readAs: "ArrayBuffer", }); useEffect(() => { if (filesContent.length > 0) { const file = filesContent[0]; - const emails = file.content - .toLowerCase() - .split("\n") - .filter((x) => new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/).test(x)); + readXlsxFile(file.content).then((rows) => { + const emails = uniq( + rows + .map((row) => { + const [email] = row as string[]; + return EMAIL_REGEX.test(email) && !users.map((u) => u.email).includes(email) ? email.toString().trim() : undefined; + }) + .filter((x) => !!x), + ); - if (emails.length === 0) { - toast.error("Please upload a .txt file containing e-mails, one per line!"); - return; - } + if (emails.length === 0) { + toast.error("Please upload an Excel file containing e-mails!"); + clear(); + return; + } - const emailUsers = [...new Set(emails)].map((x) => users.find((y) => y.email.toLowerCase() === x)).filter((x) => x !== undefined); - const filteredUsers = emailUsers.filter( - (x) => - ((user.type === "developer" || user.type === "admin" || user.type === "corporate") && - (x?.type === "student" || x?.type === "teacher")) || - (user.type === "teacher" && x?.type === "student"), - ); + const emailUsers = [...new Set(emails)].map((x) => users.find((y) => y.email.toLowerCase() === x)).filter((x) => x !== undefined); + const filteredUsers = emailUsers.filter( + (x) => + ((user.type === "developer" || user.type === "admin" || user.type === "corporate") && + (x?.type === "student" || x?.type === "teacher")) || + (user.type === "teacher" && x?.type === "student"), + ); - setParticipants(filteredUsers.filter((x) => !!x).map((x) => x!.id)); - toast.success( - user.type !== "teacher" - ? "Added all teachers and students found in the file you've provided!" - : "Added all students found in the file you've provided!", - {toastId: "upload-success"}, - ); + setParticipants(filteredUsers.filter((x) => !!x).map((x) => x!.id)); + toast.success( + user.type !== "teacher" + ? "Added all teachers and students found in the file you've provided!" + : "Added all students found in the file you've provided!", + {toastId: "upload-success"}, + ); + }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [filesContent, user.type, users]); const submit = () => { @@ -90,7 +101,12 @@ const CreatePanel = ({user, users, group, onClose}: CreateDialogProps) => {
- +
+ +
+ +
+
- + setPassword(e)} + placeholder="Enter your password" + required + /> + setNewPassword(e)} + placeholder="Enter your new password (optional)" + /> + + ); + + const NameInput = () => ( + setName(e)} placeholder="Enter your name" defaultValue={name} required /> + ); + + const AgentInformationInput = () => ( +
+ null} + placeholder="Enter corporate name" + defaultValue={companyName} + disabled + /> + null} + placeholder="Enter commercial registration" + defaultValue={commercialRegistration} + disabled + /> +
+ ); + + const CountryInput = () => ( +
+ + +
+ ); + + const PhoneInput = () => ( + setPhone(e)} + placeholder="Enter phone number" + defaultValue={phone} + required + /> + ); + + const ExpirationDate = () => ( +
+ + + {!user.subscriptionExpirationDate && "Unlimited"} + {user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")} + +
+ ); + + const TimezoneInput = () => ( +
+ + +
+ ); + return (
@@ -175,16 +262,26 @@ function UserProfile({user, mutateUser}: Props) {

Edit Profile

-
- setName(e)} - placeholder="Enter your name" - defaultValue={name} - required - /> + + {user.type !== "corporate" ? ( + + ) : ( + + setCorporateInformation((prev) => ({ + ...prev!, + companyInformation: {...prev!.companyInformation, name: e}, + })) + } + placeholder="Enter your company's name" + defaultValue={corporateInformation?.companyInformation.name} + required + /> + )} + -
- {user.type === "student" && ( - setPassportID(e)} - placeholder="Enter National ID or Passport number" - value={passport_id} - required - /> - )} -
- setPassword(e)} - placeholder="Enter your password" - required - /> - setNewPassword(e)} - placeholder="Enter your new password (optional)" - /> -
+ + + {user.type === "agent" && } - {user.type === "agent" && ( -
- null} - placeholder="Enter corporate name" - defaultValue={companyName} - disabled - /> - null} - placeholder="Enter commercial registration" - defaultValue={commercialRegistration} - disabled - /> -
- )} + + + + -
-
- - -
- setPhone(e)} - placeholder="Enter phone number" - defaultValue={phone} - required - /> -
-
- {user.type === "corporate" && ( + {user.type === "student" ? ( + setPassportID(e)} + placeholder="Enter National ID or Passport number" + value={passport_id} required /> - )} - {user.type !== "corporate" && ( -
- - - {EMPLOYMENT_STATUS.map(({status, label}) => ( - - {({checked}) => ( - - {label} - - )} - - ))} - + + + ) : ( + + )} + + + + {user.type === "corporate" && ( + <> + + null} + label="Number of users" + defaultValue={user.corporateInformation.companyInformation.userAmount} + disabled + required + /> + null} + label="Pricing" + defaultValue={`${user.corporateInformation.payment?.value} ${user.corporateInformation.payment?.currency}`} + disabled + required + /> + + + + )} + + {user.type === "corporate" && ( + <> + + + + + + + )} + + {user.type === "corporate" && user.corporateInformation.referralAgent && ( + <> + + + null} + defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.name} + type="text" + label="Country Manager's Name" + placeholder="Not available" + required + disabled + /> + null} + defaultValue={users.find((x) => x.id === user.corporateInformation.referralAgent)?.email} + type="text" + label="Country Manager's E-mail" + placeholder="Not available" + required + disabled + /> + + +
+ + x.id === user.corporateInformation.referralAgent)?.demographicInformation + ?.country + } + onChange={() => null} + disabled + /> +
+ + null} + placeholder="Not available" + defaultValue={ + users.find((x) => x.id === user.corporateInformation.referralAgent)?.demographicInformation?.phone + } + disabled + required + /> +
+ + )} + + {user.type !== "corporate" && ( + + + +
+ +
- )} -
-
- - - - {({checked}) => ( - - Male - - )} - - - {({checked}) => ( - - Female - - )} - - - {({checked}) => ( - - Other - - )} - - -
-
- - - {!user.subscriptionExpirationDate && "Unlimited"} - {user.subscriptionExpirationDate && moment(user.subscriptionExpirationDate).format("DD/MM/YYYY")} - -
-
-
+
+ )}
diff --git a/src/pages/record.tsx b/src/pages/record.tsx index 728cbbc5..827c85de 100644 --- a/src/pages/record.tsx +++ b/src/pages/record.tsx @@ -24,7 +24,7 @@ import useGroups from "@/hooks/useGroups"; import {shouldRedirectHome} from "@/utils/navigation.disabled"; import useAssignments from "@/hooks/useAssignments"; import {uuidv4} from "@firebase/util"; -import { usePDFDownload } from "@/hooks/usePDFDownload"; +import {usePDFDownload} from "@/hooks/usePDFDownload"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -175,7 +175,7 @@ export default function History({user}: {user: User}) { level: calculateBandScore(x.correct, x.total, x.module, user.focus), })); - const { timeSpent, session } = dateStats[0]; + const {timeSpent, session} = dateStats[0]; const selectExam = () => { const examPromises = uniqBy(dateStats, "exam").map((stat) => getExamById(stat.module, stat.exam)); @@ -201,7 +201,7 @@ export default function History({user}: {user: User}) { correct / total >= 0.3 && correct / total < 0.7 && "text-mti-red", correct / total < 0.3 && "text-mti-rose", ); - + const content = ( <>
@@ -215,8 +215,7 @@ export default function History({user}: {user: User}) { )}
- + Level{" "} {(aggregatedLevels.reduce((accumulator, current) => accumulator + current.level, 0) / aggregatedLevels.length).toFixed(1)} diff --git a/src/pages/stats.tsx b/src/pages/stats.tsx index a6abe9ef..21661251 100644 --- a/src/pages/stats.tsx +++ b/src/pages/stats.tsx @@ -139,9 +139,9 @@ export default function Stats() { } }, [startDate, endDate]); - const calculateTotalScore = (stats: Stat[]) => { + const calculateTotalScore = (stats: Stat[], divisionFactor: number) => { const moduleScores = calculateModuleScore(stats); - return moduleScores.reduce((acc, curr) => acc + curr.score, 0) / 4; + return moduleScores.reduce((acc, curr) => acc + curr.score, 0) / divisionFactor; }; const calculateScorePerModule = (stats: Stat[], module: Module) => { @@ -278,7 +278,10 @@ export default function Stats() { Level{" "} - {calculateTotalScore(stats.filter((s) => timestampToMoment(s).isBefore(date))).toFixed(1)} + {calculateTotalScore( + stats.filter((s) => timestampToMoment(s).isBefore(date)), + 5, + ).toFixed(1)}
) : null; @@ -364,6 +367,7 @@ export default function Stats() { return date.isValid() ? calculateTotalScore( stats.filter((s) => timestampToMoment(s).isBefore(date)), + 5, ).toFixed(1) : undefined; }) @@ -599,188 +603,47 @@ export default function Stats() { }} />
- {/* Reading Score Band in Interval */} -
- Reading Score Band in Interval - ( +
+ {capitalize(module)} Score Band in Interval + moment(date).format("DD/MM/YYYY")), - datasets: [ - { - type: "line", - label: "Reading", - fill: false, - borderColor: COLORS[0], - backgroundColor: COLORS[0], - borderWidth: 2, - spanGaps: true, - data: intervalDates.map((date) => { - return calculateTotalScore( - stats.filter( - (s) => timestampToMoment(s).isBefore(date) && s.module === "reading", - ), - ).toFixed(1); - }), - }, - ], - }} - /> -
- - {/* Listening Score Band in Interval */} -
- Listening Score Band in Interval - moment(date).format("DD/MM/YYYY")), - datasets: [ - { - type: "line", - label: "Listening", - fill: false, - borderColor: COLORS[1], - backgroundColor: COLORS[1], - borderWidth: 2, - spanGaps: true, - data: intervalDates.map((date) => { - return calculateTotalScore( - stats.filter( - (s) => timestampToMoment(s).isBefore(date) && s.module === "listening", - ), - ).toFixed(1); - }), - }, - ], - }} - /> -
- - {/* Writing Score Band in Interval */} -
- Writing Score Band in Interval - moment(date).format("DD/MM/YYYY")), - datasets: [ - { - type: "line", - label: "Writing", - fill: false, - borderColor: COLORS[2], - backgroundColor: COLORS[2], - borderWidth: 2, - spanGaps: true, - data: intervalDates.map((date) => { - return calculateTotalScore( - stats.filter( - (s) => timestampToMoment(s).isBefore(date) && s.module === "writing", - ), - ).toFixed(1); - }), - }, - ], - }} - /> -
- - {/* Speaking Score Band in Interval */} -
- Speaking Score Band in Interval - moment(date).format("DD/MM/YYYY")), - datasets: [ - { - type: "line", - label: "Speaking", - fill: false, - borderColor: COLORS[3], - backgroundColor: COLORS[3], - borderWidth: 2, - spanGaps: true, - data: intervalDates.map((date) => { - return calculateTotalScore( - stats.filter( - (s) => timestampToMoment(s).isBefore(date) && s.module === "speaking", - ), - ).toFixed(1); - }), - }, - ], - }} - /> -
- - {/* Level Score Band in Interval */} -
- Level Score Band in Interval - moment(date).format("DD/MM/YYYY")), - datasets: [ - { - type: "line", - label: "Level", - fill: false, - borderColor: COLORS[4], - backgroundColor: COLORS[4], - borderWidth: 2, - spanGaps: true, - data: intervalDates.map((date) => { - return calculateTotalScore( - stats.filter((s) => timestampToMoment(s).isBefore(date) && s.module === "level"), - ).toFixed(1); - }), - }, - ], - }} - /> -
+ }} + type="line" + data={{ + labels: intervalDates.map((date) => moment(date).format("DD/MM/YYYY")), + datasets: [ + { + type: "line", + label: capitalize(module), + fill: false, + borderColor: COLORS[index], + backgroundColor: COLORS[index], + borderWidth: 2, + spanGaps: true, + data: intervalDates.map((date) => { + return calculateTotalScore( + stats.filter( + (s) => timestampToMoment(s).isBefore(date) && s.module === module, + ), + 1, + ).toFixed(1); + }), + }, + ], + }} + /> +
+ ))}
diff --git a/src/utils/groups.ts b/src/utils/groups.ts new file mode 100644 index 00000000..5b7c894d --- /dev/null +++ b/src/utils/groups.ts @@ -0,0 +1,18 @@ +import {CorporateUser, Group, User} from "@/interfaces/user"; +import axios from "axios"; + +export const isUserFromCorporate = async (userID: string) => { + const groups = (await axios.get(`/api/groups?participant=${userID}`)).data; + const users = (await axios.get("/api/users/list")).data; + + const adminTypes = groups.map((g) => users.find((u) => u.id === g.admin)?.type); + return adminTypes.includes("corporate"); +}; + +export const getUserCorporate = async (userID: string) => { + const groups = (await axios.get(`/api/groups?participant=${userID}`)).data; + const users = (await axios.get("/api/users/list")).data; + + const admins = groups.map((g) => users.find((u) => u.id === g.admin)).filter((x) => x?.type === "corporate"); + return admins.length > 0 ? (admins[0] as CorporateUser) : undefined; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index c63966f9..cb52f2a1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -12,3 +12,16 @@ export function dateSorter(a: any, b: any, direction: "asc" | "desc", key: strin export function env(key: string) { return (window as any).__ENV[key]; } + +export const convertBase64 = (file: File) => { + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + fileReader.readAsDataURL(file); + fileReader.onload = () => { + resolve(fileReader.result); + }; + fileReader.onerror = (error) => { + reject(error); + }; + }); +}; diff --git a/src/utils/score.ts b/src/utils/score.ts index 08707312..2e191025 100644 --- a/src/utils/score.ts +++ b/src/utils/score.ts @@ -1,5 +1,5 @@ import {Module} from "@/interfaces"; -import { LevelScore } from "@/constants/ielts"; +import {LevelScore} from "@/constants/ielts"; type Type = "academic" | "general"; @@ -96,7 +96,7 @@ const academicMarking: {[key: number]: number} = { const levelMarking: {[key: number]: number} = { 88: 9, // Advanced - 64: 8 , // Upper-Intermediate + 64: 8, // Upper-Intermediate 52: 6, // Intermediate 32: 4, // Pre-Intermediate 16: 2, // Elementary @@ -142,23 +142,24 @@ export const calculateBandScore = (correct: number, total: number, module: Modul }; export const calculateAverageLevel = (levels: {[key in Module]: number}) => { - return Object.keys(levels).reduce((accumulator, current) => levels[current as Module] + accumulator, 0) / 4; + return Object.keys(levels).reduce((accumulator, current) => levels[current as Module] + accumulator, 0) / 5; }; export const getLevelScore = (level: number) => { - switch(level) { + switch (level) { case 0: - return ['Beginner', 'Low A1']; + return ["Beginner", "Low A1"]; case 2: - return ['Elementary', 'High A1/Low A2']; + return ["Elementary", "High A1/Low A2"]; case 4: - return ['Pre-Intermediate', 'High A2/Low B1']; + return ["Pre-Intermediate", "High A2/Low B1"]; case 6: - return ['Intermediate', 'High B1/Low B2']; + return ["Intermediate", "High B1/Low B2"]; case 8: - return ['Upper-Intermediate', 'High B2/Low C1']; + return ["Upper-Intermediate", "High B2/Low C1"]; case 9: - return ['Advanced', 'C1']; - default: return []; + return ["Advanced", "C1"]; + default: + return []; } -} \ No newline at end of file +}; diff --git a/yarn.lock b/yarn.lock index 8646fe4a..f262d9df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,7 +10,7 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" -"@babel/helper-module-imports@^7.16.7": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.16.7": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== @@ -62,6 +62,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.7.2": + version "7.23.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650" + integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/types@^7.22.15": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" @@ -98,6 +105,16 @@ source-map "^0.5.7" stylis "4.2.0" +"@emotion/cache@^10.0.27": + version "10.0.29" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" + integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ== + dependencies: + "@emotion/sheet" "0.9.4" + "@emotion/stylis" "0.8.5" + "@emotion/utils" "0.11.3" + "@emotion/weak-memoize" "0.2.5" + "@emotion/cache@^11.11.0", "@emotion/cache@^11.4.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff" @@ -109,6 +126,11 @@ "@emotion/weak-memoize" "^0.3.1" stylis "4.2.0" +"@emotion/hash@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + "@emotion/hash@^0.9.1": version "0.9.1" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" @@ -145,6 +167,17 @@ "@emotion/weak-memoize" "^0.3.1" hoist-non-react-statics "^3.3.1" +"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16": + version "0.11.16" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad" + integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg== + dependencies: + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/unitless" "0.7.5" + "@emotion/utils" "0.11.3" + csstype "^2.5.7" + "@emotion/serialize@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.2.tgz#017a6e4c9b8a803bd576ff3d52a0ea6fa5a62b51" @@ -156,11 +189,26 @@ "@emotion/utils" "^1.2.1" csstype "^3.0.2" +"@emotion/sheet@0.9.4": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" + integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA== + "@emotion/sheet@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== +"@emotion/stylis@0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== + +"@emotion/unitless@0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + "@emotion/unitless@^0.8.1": version "0.8.1" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" @@ -171,11 +219,21 @@ resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== +"@emotion/utils@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" + integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw== + "@emotion/utils@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== +"@emotion/weak-memoize@0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + "@emotion/weak-memoize@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" @@ -1837,6 +1895,31 @@ axobject-query@^3.1.1: dependencies: deep-equal "^2.0.5" +babel-plugin-emotion@^10.0.27: + version "10.2.2" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz#a1fe3503cff80abfd0bdda14abd2e8e57a79d17d" + integrity sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/serialize" "^0.11.16" + babel-plugin-macros "^2.0.0" + babel-plugin-syntax-jsx "^6.18.0" + convert-source-map "^1.5.0" + escape-string-regexp "^1.0.5" + find-root "^1.1.0" + source-map "^0.5.7" + +babel-plugin-macros@^2.0.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" + integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== + dependencies: + "@babel/runtime" "^7.7.2" + cosmiconfig "^6.0.0" + resolve "^1.12.0" + babel-plugin-macros@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" @@ -1846,6 +1929,11 @@ babel-plugin-macros@^3.1.0: cosmiconfig "^7.0.0" resolve "^1.19.0" +babel-plugin-syntax-jsx@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + integrity sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" @@ -2176,6 +2264,17 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + cosmiconfig@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" @@ -2202,6 +2301,16 @@ country-flag-icons@^1.5.4: resolved "https://registry.yarnpkg.com/country-flag-icons/-/country-flag-icons-1.5.7.tgz#f1f2ddf14f3cbf01cba6746374aeba94db35d4b4" integrity sha512-AdvXhMcmSp7nBSkpGfW4qR/luAdRUutJqya9PuwRbsBzuoknThfultbv7Ib6fWsHXC43Es/4QJ8gzQQdBNm75A== +create-emotion@^10.0.14, create-emotion@^10.0.27: + version "10.0.27" + resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-10.0.27.tgz#cb4fa2db750f6ca6f9a001a33fbf1f6c46789503" + integrity sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg== + dependencies: + "@emotion/cache" "^10.0.27" + "@emotion/serialize" "^0.11.15" + "@emotion/sheet" "0.9.4" + "@emotion/utils" "0.11.3" + cross-fetch@^3.1.5: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" @@ -2247,6 +2356,11 @@ cssesc@^3.0.0: resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +csstype@^2.5.7: + version "2.6.21" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e" + integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w== + csstype@^3.0.2: version "3.1.1" resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz" @@ -2378,6 +2492,11 @@ didyoumean@^1.2.2: resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dijkstrajs@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" @@ -2476,6 +2595,14 @@ emoji-regex@^9.2.2: resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +emotion@^10.0.14: + version "10.0.27" + resolved "https://registry.yarnpkg.com/emotion/-/emotion-10.0.27.tgz#f9ca5df98630980a23c819a56262560562e5d75e" + integrity sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g== + dependencies: + babel-plugin-emotion "^10.0.27" + create-emotion "^10.0.27" + encode-utf8@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda" @@ -3570,7 +3697,7 @@ ignore@^5.2.0: resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -4252,7 +4379,7 @@ media-engine@^1.0.3: resolved "https://registry.yarnpkg.com/media-engine/-/media-engine-1.0.3.tgz#be3188f6cd243ea2a40804a35de5a5b032f58dad" integrity sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg== -memoize-one@^5.1.1: +memoize-one@^5.0.4, memoize-one@^5.1.1: version "5.2.1" resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== @@ -4360,6 +4487,13 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +moment-timezone@^0.5.44: + version "0.5.44" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.44.tgz#a64a4e47b68a43deeab5ae4eb4f82da77cdf595f" + integrity sha512-nv3YpzI/8lkQn0U6RkLd+f0W/zy/JnoR5/EyPz/dNkPTBjA2jNLCVxaiQ8QpeLymhSZvX0wCL5s27NQWdOPwAw== + dependencies: + moment "^2.29.4" + moment@^2.29.4: version "2.29.4" resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz" @@ -5024,6 +5158,18 @@ react-datepicker@^4.18.0: react-onclickoutside "^6.13.0" react-popper "^2.3.0" +react-diff-viewer@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/react-diff-viewer/-/react-diff-viewer-3.1.1.tgz#21ac9c891193d05a3734bfd6bd54b107ee6d46cc" + integrity sha512-rmvwNdcClp6ZWdS11m1m01UnBA4OwYaLG/li0dB781e/bQEzsGyj+qewVd6W5ztBwseQ72pO7nwaCcq5jnlzcw== + dependencies: + classnames "^2.2.6" + create-emotion "^10.0.14" + diff "^4.0.1" + emotion "^10.0.14" + memoize-one "^5.0.4" + prop-types "^15.6.2" + react-dom@18.2.0: version "18.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" @@ -5261,6 +5407,15 @@ resolve@^1.1.7, resolve@^1.22.1: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.12.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^1.19.0: version "1.22.6" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362" @@ -6200,7 +6355,7 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0, yaml@^1.10.2: +yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: version "1.10.2" resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==