ENCOA-260, ENCOA-259
This commit is contained in:
@@ -155,11 +155,11 @@ const Templates: React.FC<Props> = ({ module, state, setState }) => {
|
|||||||
</li>
|
</li>
|
||||||
{["reading", "level"].includes(module) && (
|
{["reading", "level"].includes(module) && (
|
||||||
<li className="text-gray-700 list-disc">
|
<li className="text-gray-700 list-disc">
|
||||||
a part must only contain a reading passage and it must be between the part delineator (e.g. Part 1) and the part exercises.
|
a part must only contain a single reading passage and it must be between the part delineator (e.g. Part 1) and the part exercises.
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
<li className="text-gray-700 list-disc">
|
<li className="text-gray-700 list-disc">
|
||||||
if solutions are going to be uploaded the exercise numbers/id's must match the ones in the solutions.
|
if solutions are going to be uploaded, the exercise numbers/id's must match the ones in the solutions.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
116
src/components/ExamEditor/ResetModule.tsx
Normal file
116
src/components/ExamEditor/ResetModule.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import Button from "@/components/Low/Button";
|
||||||
|
import { Module } from "@/interfaces";
|
||||||
|
import useExamEditorStore from "@/stores/examEditor";
|
||||||
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
|
||||||
|
import { capitalize } from "lodash";
|
||||||
|
import React, { Fragment, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
module: Module;
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setNumberOfLevelParts: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResetModule: React.FC<Props> = ({ module, isOpen, setIsOpen, setNumberOfLevelParts }) => {
|
||||||
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const { dispatch } = useExamEditorStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setMounted(true);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen && mounted) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setMounted(false);
|
||||||
|
setIsClosing(false);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isOpen, mounted]);
|
||||||
|
|
||||||
|
const blockMultipleClicksClose = useCallback(() => {
|
||||||
|
if (isClosing) return;
|
||||||
|
setIsClosing(true);
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsClosing(false);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isClosing, setIsOpen, isOpen]);
|
||||||
|
|
||||||
|
if (!mounted && !isOpen) return null;
|
||||||
|
|
||||||
|
const handleResetModule = () => {
|
||||||
|
dispatch({ type: 'RESET_MODULE', payload: { module } });
|
||||||
|
setIsOpen(false);
|
||||||
|
setNumberOfLevelParts(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition
|
||||||
|
show={isOpen}
|
||||||
|
as={Fragment}
|
||||||
|
beforeEnter={() => setIsClosing(false)}
|
||||||
|
beforeLeave={() => setIsClosing(true)}
|
||||||
|
afterLeave={() => {
|
||||||
|
setIsClosing(false);
|
||||||
|
setMounted(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog onClose={() => blockMultipleClicksClose()} className="relative z-50">
|
||||||
|
<TransitionChild
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0">
|
||||||
|
<div className="fixed inset-0 bg-black/30" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
<TransitionChild
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95">
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
|
<DialogPanel className={`bg-ielts-${module}-light w-full max-w-xl h-fit p-8 rounded-xl flex flex-col gap-4`}>
|
||||||
|
<DialogTitle className="flex font-bold text-xl justify-center text-gray-700"><span>Reset {capitalize(module)} Module</span></DialogTitle>
|
||||||
|
<div className="flex flex-col w-full mt-4 gap-6">
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4">
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Do you want to reset the {module} module?
|
||||||
|
</p>
|
||||||
|
<br/>
|
||||||
|
<p className="text-gray-600 font-bold">This will reset all the current data in the {module} module and cannot be undone.</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex justify-between mt-4 gap-8">
|
||||||
|
<Button color="purple" onClick={blockMultipleClicksClose} variant="outline" className="self-end w-full bg-white">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button color="purple" onClick={handleResetModule} variant="solid" className="self-end w-full">
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</TransitionChild>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResetModule;
|
||||||
@@ -34,7 +34,9 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
|||||||
canPreview,
|
canPreview,
|
||||||
canSubmit
|
canSubmit
|
||||||
}) => {
|
}) => {
|
||||||
|
const { dispatch } = useExamEditorStore()
|
||||||
const examLabel = useExamEditorStore((state) => state.modules[module].examLabel) || '';
|
const examLabel = useExamEditorStore((state) => state.modules[module].examLabel) || '';
|
||||||
|
const type = useExamEditorStore((s) => s.modules[module].type);
|
||||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SectionSettings>(
|
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SectionSettings>(
|
||||||
module,
|
module,
|
||||||
sectionId
|
sectionId
|
||||||
@@ -50,6 +52,18 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
|||||||
updateLocalAndScheduleGlobal({ category: text });
|
updateLocalAndScheduleGlobal({ category: text });
|
||||||
}, [updateLocalAndScheduleGlobal]);
|
}, [updateLocalAndScheduleGlobal]);
|
||||||
|
|
||||||
|
const typeOptions = [
|
||||||
|
{ value: 'general', label: 'General' },
|
||||||
|
{ value: 'academic', label: 'Academic' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const onTypeChange = useCallback((option: { value: string | null, label: string }) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_MODULE',
|
||||||
|
payload: { module, updates: { type: option.value as "academic" | "general" | undefined } }
|
||||||
|
});
|
||||||
|
}, [dispatch, module]);
|
||||||
|
|
||||||
const onIntroOptionChange = useCallback((option: { value: string | null, label: string }) => {
|
const onIntroOptionChange = useCallback((option: { value: string | null, label: string }) => {
|
||||||
let updates: Partial<SectionSettings> = { introOption: option };
|
let updates: Partial<SectionSettings> = { introOption: option };
|
||||||
|
|
||||||
@@ -100,6 +114,18 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
|||||||
value={localSettings.category || ''}
|
value={localSettings.category || ''}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
{["reading", "writing"].includes(module) && <Dropdown
|
||||||
|
title="Type"
|
||||||
|
module={module}
|
||||||
|
open={localSettings.isTypeDropdownOpen}
|
||||||
|
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isTypeDropdownOpen: isOpen }, false)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={typeOptions}
|
||||||
|
onChange={(o) => onTypeChange({ value: o!.value, label: o!.label })}
|
||||||
|
value={typeOptions.find(o => o.value === type)}
|
||||||
|
/>
|
||||||
|
</Dropdown>}
|
||||||
<Dropdown
|
<Dropdown
|
||||||
title="Divider"
|
title="Divider"
|
||||||
module={module}
|
module={module}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const ReadingSettings: React.FC = () => {
|
|||||||
sections,
|
sections,
|
||||||
minTimer,
|
minTimer,
|
||||||
isPrivate,
|
isPrivate,
|
||||||
|
type,
|
||||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||||
|
|
||||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<ReadingSectionSettings>(
|
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<ReadingSectionSettings>(
|
||||||
@@ -78,10 +79,10 @@ const ReadingSettings: React.FC = () => {
|
|||||||
minTimer,
|
minTimer,
|
||||||
module: "reading",
|
module: "reading",
|
||||||
id: title,
|
id: title,
|
||||||
type: "academic",
|
|
||||||
variant: sections.length === 3 ? "full" : "partial",
|
variant: sections.length === 3 ? "full" : "partial",
|
||||||
difficulty,
|
difficulty,
|
||||||
private: isPrivate,
|
private: isPrivate,
|
||||||
|
type: type!
|
||||||
};
|
};
|
||||||
|
|
||||||
axios.post(`/api/exam/reading`, exam)
|
axios.post(`/api/exam/reading`, exam)
|
||||||
@@ -112,6 +113,7 @@ const ReadingSettings: React.FC = () => {
|
|||||||
variant: undefined,
|
variant: undefined,
|
||||||
difficulty,
|
difficulty,
|
||||||
private: isPrivate,
|
private: isPrivate,
|
||||||
|
type: type!
|
||||||
} as ReadingExam);
|
} as ReadingExam);
|
||||||
setExerciseIndex(0);
|
setExerciseIndex(0);
|
||||||
setQuestionIndex(0);
|
setQuestionIndex(0);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const WritingSettings: React.FC = () => {
|
|||||||
isPrivate,
|
isPrivate,
|
||||||
sections,
|
sections,
|
||||||
focusedSection,
|
focusedSection,
|
||||||
|
type
|
||||||
} = useExamEditorStore((store) => store.modules["writing"]);
|
} = useExamEditorStore((store) => store.modules["writing"]);
|
||||||
|
|
||||||
const states = sections.flatMap((s) => s.state) as WritingExercise[];
|
const states = sections.flatMap((s) => s.state) as WritingExercise[];
|
||||||
@@ -72,6 +73,7 @@ const WritingSettings: React.FC = () => {
|
|||||||
variant: undefined,
|
variant: undefined,
|
||||||
difficulty,
|
difficulty,
|
||||||
private: isPrivate,
|
private: isPrivate,
|
||||||
|
type: type!
|
||||||
});
|
});
|
||||||
setExerciseIndex(0);
|
setExerciseIndex(0);
|
||||||
openDetachedTab("popout?type=Exam&module=writing", router)
|
openDetachedTab("popout?type=Exam&module=writing", router)
|
||||||
@@ -94,6 +96,7 @@ const WritingSettings: React.FC = () => {
|
|||||||
variant: undefined,
|
variant: undefined,
|
||||||
difficulty,
|
difficulty,
|
||||||
private: isPrivate,
|
private: isPrivate,
|
||||||
|
type: type!
|
||||||
};
|
};
|
||||||
|
|
||||||
axios
|
axios
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import ListeningSettings from "./SettingsEditor/listening";
|
|||||||
import SpeakingSettings from "./SettingsEditor/speaking";
|
import SpeakingSettings from "./SettingsEditor/speaking";
|
||||||
import ImportOrStartFromScratch from "./ImportExam/ImportOrFromScratch";
|
import ImportOrStartFromScratch from "./ImportExam/ImportOrFromScratch";
|
||||||
import { defaultSectionSettings } from "@/stores/examEditor/defaults";
|
import { defaultSectionSettings } from "@/stores/examEditor/defaults";
|
||||||
|
import Button from "../Low/Button";
|
||||||
|
import ResetModule from "./ResetModule";
|
||||||
|
|
||||||
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
|
|||||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||||
|
|
||||||
const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== 0 ? levelParts : 1);
|
const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== 0 ? levelParts : 1);
|
||||||
|
const [isResetModuleOpen, setIsResetModuleOpen] = useState(false);
|
||||||
|
|
||||||
// For exam edits
|
// For exam edits
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -129,6 +132,7 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
|
|||||||
<>
|
<>
|
||||||
{showImport ? <ImportOrStartFromScratch module={currentModule} setNumberOfLevelParts={updateLevelParts} /> : (
|
{showImport ? <ImportOrStartFromScratch module={currentModule} setNumberOfLevelParts={updateLevelParts} /> : (
|
||||||
<>
|
<>
|
||||||
|
{isResetModuleOpen && <ResetModule module={currentModule} isOpen={isResetModuleOpen} setIsOpen={setIsResetModuleOpen} setNumberOfLevelParts={setNumberOfLevelParts}/>}
|
||||||
<div className="flex gap-4 w-full items-center">
|
<div className="flex gap-4 w-full items-center">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
<label className="font-normal text-base text-mti-gray-dim">Timer</label>
|
||||||
@@ -183,7 +187,8 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
|
|||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 w-full">
|
<div className="flex flex-row gap-3 w-full">
|
||||||
|
<div className="flex flex-col gap-3 flex-grow">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Exam Label *</label>
|
<label className="font-normal text-base text-mti-gray-dim">Exam Label *</label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -195,6 +200,10 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{["reading", "listening", "level"].includes(currentModule) && <Button onClick={() => setIsResetModuleOpen(true)} customColor={`bg-ielts-${currentModule}/70 hover:bg-ielts-${currentModule} border-ielts-${currentModule}`} className={`text-white self-end`}>
|
||||||
|
Reset Module
|
||||||
|
</Button>}
|
||||||
|
</div>
|
||||||
<div className="flex flex-row gap-8">
|
<div className="flex flex-row gap-8">
|
||||||
<Settings />
|
<Settings />
|
||||||
<div className="flex-grow max-w-[66%]">
|
<div className="flex-grow max-w-[66%]">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface Props {
|
|||||||
padding?: string;
|
padding?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
type?: "button" | "reset" | "submit";
|
type?: "button" | "reset" | "submit";
|
||||||
|
customColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Button({
|
export default function Button({
|
||||||
@@ -19,6 +20,7 @@ export default function Button({
|
|||||||
variant = "solid",
|
variant = "solid",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
customColor = undefined,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
type,
|
type,
|
||||||
@@ -65,7 +67,7 @@ export default function Button({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"rounded-full transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer select-none",
|
"rounded-full transition ease-in-out duration-300 disabled:cursor-not-allowed cursor-pointer select-none",
|
||||||
padding,
|
padding,
|
||||||
colorClassNames[color][variant],
|
customColor ? customColor : colorClassNames[color][variant],
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
disabled={disabled || isLoading}>
|
disabled={disabled || isLoading}>
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export interface WritingExam extends ExamBase {
|
|||||||
module: "writing";
|
module: "writing";
|
||||||
enableNavigation?: boolean;
|
enableNavigation?: boolean;
|
||||||
exercises: WritingExercise[];
|
exercises: WritingExercise[];
|
||||||
|
type?: "academic" | "general";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WordCounter {
|
interface WordCounter {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { uniqBy } from "lodash";
|
import { capitalize, uniqBy } from "lodash";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { useFilePicker } from "use-file-picker";
|
import { useFilePicker } from "use-file-picker";
|
||||||
@@ -19,6 +19,9 @@ import { Type, UserImport } from "../../../interfaces/IUserImport";
|
|||||||
import UserTable from "../../../components/UserTable";
|
import UserTable from "../../../components/UserTable";
|
||||||
import { EntityWithRoles } from "@/interfaces/entity";
|
import { EntityWithRoles } from "@/interfaces/entity";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
|
import { IoInformationCircleOutline } from "react-icons/io5";
|
||||||
|
import { FaFileDownload } from "react-icons/fa";
|
||||||
|
import { HiOutlineDocumentText } from "react-icons/hi";
|
||||||
|
|
||||||
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
|
const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/);
|
||||||
|
|
||||||
@@ -210,12 +213,42 @@ export default function BatchCreateUser({ user, entities = [], permissions, onFi
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleTemplateDownload = () => {
|
||||||
|
const fileName = "UsersTemplate.xlsx";
|
||||||
|
const url = `https://firebasestorage.googleapis.com/v0/b/encoach-staging.appspot.com/o/import_templates%2F${fileName}?alt=media&token=b771a535-bf95-4060-889c-a086df65d480`;
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
link.download = fileName;
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal isOpen={showHelp} onClose={() => setShowHelp(false)} title="Excel File Format">
|
<Modal isOpen={showHelp} onClose={() => setShowHelp(false)}>
|
||||||
<div className="mt-4 flex flex-col gap-2">
|
<>
|
||||||
<span>Please upload an Excel file with the following format:</span>
|
<div className="flex font-bold text-xl justify-center text-gray-700"><span>Excel File Format</span></div>
|
||||||
<table className="w-full">
|
<div className="mt-4 flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-3 bg-gray-100 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HiOutlineDocumentText className={`w-5 h-5 text-mti-purple-light`} />
|
||||||
|
<h2 className="text-lg font-semibold">
|
||||||
|
The uploaded document must:
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<ul className="flex flex-col pl-10 gap-2">
|
||||||
|
<li className="text-gray-700 list-disc">
|
||||||
|
be an Excel .xlsx document.
|
||||||
|
</li>
|
||||||
|
<li className="text-gray-700 list-disc">
|
||||||
|
only have a single spreadsheet with only the following 8 columns in the <b>same order</b>:
|
||||||
|
<div className="py-4 pr-4">
|
||||||
|
<table className="w-full bg-white">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="border border-neutral-200 px-2 py-1">First Name</th>
|
<th className="border border-neutral-200 px-2 py-1">First Name</th>
|
||||||
@@ -229,25 +262,64 @@ export default function BatchCreateUser({ user, entities = [], permissions, onFi
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
<span className="mt-4">
|
|
||||||
<b>Notes:</b>
|
|
||||||
<ul>
|
|
||||||
<li>- All incorrect e-mails will be ignored;</li>
|
|
||||||
<li>- All already registered e-mails will be ignored;</li>
|
|
||||||
<li>- You may have a header row with the format above, however, it is not necessary;</li>
|
|
||||||
<li>- All of the e-mails in the file will receive an e-mail to join EnCoach with the role selected below.</li>
|
|
||||||
</ul>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 bg-gray-100 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IoInformationCircleOutline className={`w-5 h-5 text-mti-purple-light`} />
|
||||||
|
<h2 className="text-lg font-semibold">
|
||||||
|
Note that:
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<ul className="flex flex-col pl-10 gap-2">
|
||||||
|
<li className="text-gray-700 list-disc">
|
||||||
|
all incorrect e-mails will be ignored.
|
||||||
|
</li>
|
||||||
|
<li className="text-gray-700 list-disc">
|
||||||
|
all already registered e-mails will be ignored.
|
||||||
|
</li>
|
||||||
|
<li className="text-gray-700 list-disc">
|
||||||
|
the spreadsheet may have a header row with the format above, however, it is not necessary as long the columns are in the <b>right order</b>.
|
||||||
|
</li>
|
||||||
|
<li className="text-gray-700 list-disc">
|
||||||
|
all of the e-mails in the file will receive an e-mail to join EnCoach with the role selected below.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-100 rounded-lg p-4">
|
||||||
|
<p className="text-gray-600">
|
||||||
|
{`The downloadable template is an example of a file that can be imported. Your document doesn't need to be a carbon copy of the template - it can have different styling but it must adhere to the previous requirements.`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex justify-between mt-6 gap-8">
|
||||||
|
<Button color="purple" onClick={() => setShowHelp(false)} variant="outline" className="self-end w-full bg-white">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button color="purple" onClick={handleTemplateDownload} variant="solid" className="self-end w-full">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FaFileDownload size={24} />
|
||||||
|
Download Template
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
</Modal>
|
</Modal>
|
||||||
<div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4">
|
<div className="border-mti-gray-platinum flex flex-col gap-4 rounded-xl border p-4">
|
||||||
<div className="grid grid-cols-2 -md:grid-cols-1 gap-4">
|
<div className="grid grid-cols-2 -md:grid-cols-1 gap-4">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-end justify-between">
|
<div className="flex items-end justify-between">
|
||||||
<label className="text-mti-gray-dim text-base font-normal">Choose an Excel file</label>
|
<label className="text-mti-gray-dim text-base font-normal">Choose an Excel file</label>
|
||||||
<div className="tooltip cursor-pointer" data-tip="Excel File Format" onClick={() => setShowHelp(true)}>
|
<button
|
||||||
<BsQuestionCircleFill />
|
onClick={() => setShowHelp(true)}
|
||||||
</div>
|
className="tooltip cursor-pointer p-1.5 hover:bg-gray-200 rounded-full transition-colors duration-200"
|
||||||
|
data-tip="Excel File Format"
|
||||||
|
>
|
||||||
|
<IoInformationCircleOutline size={24} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={openFilePicker} isLoading={isLoading} disabled={isLoading}>
|
<Button onClick={openFilePicker} isLoading={isLoading} disabled={isLoading}>
|
||||||
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
|
{filesContent.length > 0 ? filesContent[0].name : "Choose a file"}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const defaultSettings = (module: Module) => {
|
|||||||
isCategoryDropdownOpen: false,
|
isCategoryDropdownOpen: false,
|
||||||
isIntroDropdownOpen: false,
|
isIntroDropdownOpen: false,
|
||||||
isExerciseDropdownOpen: false,
|
isExerciseDropdownOpen: false,
|
||||||
|
isTypeDropdownOpen: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (module) {
|
switch (module) {
|
||||||
@@ -163,6 +164,9 @@ const defaultModuleSettings = (module: Module, minTimer: number): ModuleState =>
|
|||||||
importing: false,
|
importing: false,
|
||||||
edit: [],
|
edit: [],
|
||||||
};
|
};
|
||||||
|
if (["reading", "writing"].includes(module)) {
|
||||||
|
state["type"] = "general";
|
||||||
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,12 @@ import { Module } from "@/interfaces";
|
|||||||
import { updateExamWithUserSolutions } from "@/stores/exam/utils";
|
import { updateExamWithUserSolutions } from "@/stores/exam/utils";
|
||||||
import { defaultExamUserSolutions } from "@/utils/exams";
|
import { defaultExamUserSolutions } from "@/utils/exams";
|
||||||
|
|
||||||
type UpdateRoot = {
|
type RootActions = { type: 'FULL_RESET' } |
|
||||||
type: 'UPDATE_ROOT';
|
{ type: 'INIT_EXAM_EDIT', payload: { exam: Exam; examModule: Module; id: string } } |
|
||||||
payload: {
|
{ type: 'UPDATE_ROOT'; payload: { updates: Partial<ExamEditorStore> } } |
|
||||||
updates: Partial<ExamEditorStore>
|
{ type: 'RESET_MODULE'; payload: { module: Module } };
|
||||||
}
|
|
||||||
};
|
|
||||||
type RootActions = { type: 'FULL_RESET' } | { type: "INIT_EXAM_EDIT", payload: { exam: Exam; examModule: Module; id: string } };
|
|
||||||
|
|
||||||
export type Action = ModuleActions | SectionActions | UpdateRoot | RootActions;
|
export type Action = ModuleActions | SectionActions | RootActions;
|
||||||
|
|
||||||
export const rootReducer = (
|
export const rootReducer = (
|
||||||
state: ExamEditorStore,
|
state: ExamEditorStore,
|
||||||
@@ -55,6 +52,16 @@ export const rootReducer = (
|
|||||||
...state,
|
...state,
|
||||||
...updates
|
...updates
|
||||||
};
|
};
|
||||||
|
case 'RESET_MODULE':
|
||||||
|
const { module } = action.payload;
|
||||||
|
const timer = ["reading", "writing", "level"].includes(module) ?
|
||||||
|
60 : (module === "speaking" ? 14 : 30);
|
||||||
|
return {
|
||||||
|
modules: {
|
||||||
|
...state.modules,
|
||||||
|
[module]: defaultModuleSettings(module, timer),
|
||||||
|
},
|
||||||
|
};
|
||||||
case 'FULL_RESET':
|
case 'FULL_RESET':
|
||||||
return {
|
return {
|
||||||
title: "",
|
title: "",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface SectionSettings {
|
|||||||
currentIntro: string | undefined;
|
currentIntro: string | undefined;
|
||||||
isCategoryDropdownOpen: boolean;
|
isCategoryDropdownOpen: boolean;
|
||||||
isIntroDropdownOpen: boolean;
|
isIntroDropdownOpen: boolean;
|
||||||
|
isTypeDropdownOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpeakingSectionSettings extends SectionSettings {
|
export interface SpeakingSectionSettings extends SectionSettings {
|
||||||
@@ -114,6 +115,7 @@ export interface ModuleState {
|
|||||||
importModule: boolean;
|
importModule: boolean;
|
||||||
importing: boolean;
|
importing: boolean;
|
||||||
edit: number[];
|
edit: number[];
|
||||||
|
type?: "general" | "academic";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Avatar {
|
export interface Avatar {
|
||||||
|
|||||||
Reference in New Issue
Block a user