Listening preview and some more patches

This commit is contained in:
Carlos-Mesquita
2024-11-06 09:23:34 +00:00
parent ffa2045a2d
commit b50913eda8
24 changed files with 467 additions and 253 deletions

View File

@@ -1,13 +1,13 @@
import { toast } from "react-toastify";
type TextToken = {
export type TextToken = {
type: 'text';
content: string;
isWhitespace: boolean;
isLineBreak?: boolean;
};
type BlankToken = {
export type BlankToken = {
type: 'blank';
id: number;
};

View File

@@ -1,5 +1,4 @@
import { AlertItem } from "../../Shared/Alert";
import { BlankState } from "../BlanksReducer";
export const validateWriteBlanks = (

View File

@@ -18,7 +18,7 @@ import Alert, { AlertItem } from "../Shared/Alert";
import clsx from "clsx";
import { Card, CardContent } from "@/components/ui/card";
import { Blank, DropZone } from "./DragNDrop";
import { getTextSegments, BlankState, BlanksState, BlanksAction } from "./BlanksReducer";
import { getTextSegments, BlankState, BlanksState, BlanksAction, BlankToken } from "./BlanksReducer";
interface Props {
@@ -104,11 +104,11 @@ const BlanksEditor: React.FC<Props> = ({
const existingBlankIds = getTextSegments(state.text)
.filter(token => token.type === 'blank')
.map(token => token.id);
.map(token => (token as BlankToken).id);
const newBlankIds = getTextSegments(processedText)
.filter(token => token.type === 'blank')
.map(token => token.id);
.map(token => (token as BlankToken).id);
const removedBlankIds = existingBlankIds.filter(id => !newBlankIds.includes(id));

View File

@@ -17,6 +17,7 @@ import setEditingAlert from '../Shared/setEditingAlert';
import { toast } from 'react-toastify';
import { DragEndEvent } from '@dnd-kit/core';
import { handleMatchSentencesReorder } from '@/stores/examEditor/reorder/local';
import PromptEdit from '../Shared/PromptEdit';
const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: number }> = ({ exercise, sectionId }) => {
const { currentModule, dispatch } = useExamEditorStore();
@@ -31,6 +32,11 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
const [showReference, setShowReference] = useState(false);
const [alerts, setAlerts] = useState<AlertItem[]>([]);
const updateLocal = (exercise: MatchSentencesExercise) => {
setLocal(exercise);
setEditing(true);
};
const { editing, setEditing, handleSave, handleDiscard, modeHandle } = useSectionEdit({
sectionId,
onSave: () => {
@@ -73,9 +79,8 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
}, [local.sentences]);
const addHeading = () => {
setEditing(true);
const newId = (parseInt(local.sentences[local.sentences.length - 1].id) + 1).toString();
setLocal({
updateLocal({
...local,
sentences: [
...local.sentences,
@@ -89,7 +94,6 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
};
const updateHeading = (index: number, field: string, value: string) => {
setEditing(true);
const newSentences = [...local.sentences];
if (field === 'solution') {
@@ -100,11 +104,10 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
}
newSentences[index] = { ...newSentences[index], [field]: value };
setLocal({ ...local, sentences: newSentences });
updateLocal({ ...local, sentences: newSentences });
};
const deleteHeading = (index: number) => {
setEditing(true);
if (local.sentences.length <= 1) {
toast.error(`There needs to be at least one ${exercise.variant && exercise.variant == "ideaMatch" ? "idea/opinion" : "heading"}!`);
return;
@@ -116,7 +119,7 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
}
const newSentences = local.sentences.filter((_, i) => i !== index);
setLocal({ ...local, sentences: newSentences });
updateLocal({ ...local, sentences: newSentences });
};
useEffect(() => {
@@ -129,8 +132,7 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
const handleDragEnd = (event: DragEndEvent) => {
setEditing(true);
setLocal(handleMatchSentencesReorder(event, local));
updateLocal(handleMatchSentencesReorder(event, local));
}
return (
@@ -154,6 +156,10 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
<div className="space-y-4">
{alerts.length > 0 && <Alert alerts={alerts} />}
<PromptEdit
value={local.prompt}
onChange={(text) => updateLocal({ ...local, prompt: text })}
/>
<QuestionsList
ids={local.sentences.map(s => s.id)}
handleDragEnd={handleDragEnd}

View File

@@ -0,0 +1,49 @@
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
import { Card, CardContent } from "@/components/ui/card";
import { useState } from "react";
import { MdEdit, MdEditOff } from "react-icons/md";
interface Props {
value: string;
onChange: (text: string) => void;
}
const PromptEdit: React.FC<Props> = ({ value, onChange }) => {
const [editing, setEditing] = useState(false);
return (
<>
<Card className="mb-6">
<CardContent className="p-4">
<div className="flex justify-between items-start gap-4">
{editing ? (
<AutoExpandingTextArea
className="flex-1 p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none min-h-[100px]"
value={value}
onChange={(text) => onChange(text)}
onBlur={()=> setEditing(false)}
/>
) : (
<div className="flex-1">
<h3 className="font-medium text-gray-800 mb-2">Question/Instructions displayed to the student:</h3>
<p className="text-gray-600">{value}</p>
</div>
)}
<button
onClick={() => setEditing(!editing)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
{editing ?
<MdEditOff size={20} className="text-gray-500" /> :
<MdEdit size={20} className="text-gray-500" />
}
</button>
</div>
</CardContent>
</Card>
</>
);
}
export default PromptEdit;

View File

@@ -24,11 +24,20 @@ const Speaking: React.FC<Props> = ({ sectionId, exercise }) => {
const [loading, setLoading] = useState(generating === "context");
const [questions, setQuestions] = useState(() => {
if (sectionId === 1) {
return (exercise as SpeakingExercise).prompts || Array(5).fill("");
if ((exercise as SpeakingExercise).prompts.length > 0) {
return (exercise as SpeakingExercise).prompts;
}
return Array(5).fill("");
} else if (sectionId === 2) {
return [(exercise as SpeakingExercise).text || "", ...(exercise as SpeakingExercise).prompts || Array(3).fill("")];
if ((exercise as SpeakingExercise).text && (exercise as SpeakingExercise).prompts.length > 0) {
return (exercise as SpeakingExercise).prompts;
}
return Array(3).fill("");
} else {
return (exercise as InteractiveSpeakingExercise).prompts?.map(p => p.text) || Array(5).fill("");
if ((exercise as InteractiveSpeakingExercise).prompts.length > 0) {
return (exercise as InteractiveSpeakingExercise).prompts?.map(p => p.text);
}
return Array(5).fill("");
}
});
@@ -42,7 +51,7 @@ const Speaking: React.FC<Props> = ({ sectionId, exercise }) => {
onSave: () => {
let newExercise;
if (sectionId === 1) {
newExercise = {
newExercise = {
...local,
prompts: questions
} as SpeakingExercise;
@@ -53,7 +62,6 @@ const Speaking: React.FC<Props> = ({ sectionId, exercise }) => {
prompts: questions.slice(1),
} as SpeakingExercise;
} else {
// Section 3
newExercise = {
...local,
prompts: questions.map(text => ({
@@ -63,22 +71,25 @@ const Speaking: React.FC<Props> = ({ sectionId, exercise }) => {
} as InteractiveSpeakingExercise;
}
setEditing(false);
dispatch({
type: "UPDATE_SECTION_STATE",
payload: {
sectionId: sectionId,
update: newExercise
}
dispatch({
type: "UPDATE_SECTION_STATE",
payload: {
sectionId: sectionId,
update: newExercise
}
});
},
onDiscard: () => {
setLocal(exercise);
},
onMode: () => {
setLocal(exercise);
if (sectionId === 1) {
setQuestions((exercise as SpeakingExercise).prompts || Array(5).fill(""));
setQuestions(Array(5).fill(""));
} else if (sectionId === 2) {
setQuestions([(exercise as SpeakingExercise).text || "", ...(exercise as SpeakingExercise).prompts || Array(3).fill("")]);
setQuestions(Array(3).fill(""));
} else {
setQuestions((exercise as InteractiveSpeakingExercise).prompts?.map(p => p.text) || Array(5).fill(""));
setQuestions(Array(5).fill(""));
}
},
});
@@ -90,6 +101,7 @@ const Speaking: React.FC<Props> = ({ sectionId, exercise }) => {
if (isLoading) {
updateModule({ edit: Array.from(new Set([...edit, sectionId])) });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [generating, sectionId]);
useEffect(() => {
@@ -102,14 +114,14 @@ const Speaking: React.FC<Props> = ({ sectionId, exercise }) => {
} else {
setQuestions(genResult[0].questions);
}
dispatch({
type: "UPDATE_SECTION_SINGLE_FIELD",
payload: {
sectionId,
module: currentModule,
field: "genResult",
value: undefined
dispatch({
type: "UPDATE_SECTION_SINGLE_FIELD",
payload: {
sectionId,
module: currentModule,
field: "genResult",
value: undefined
}
});
}

View File

@@ -1,9 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import {
MdAdd,
MdEdit,
MdEditOff,
} from 'react-icons/md';
import Alert, { AlertItem } from '../Shared/Alert';
import { ReadingPart, TrueFalseExercise } from '@/interfaces/exam';
@@ -18,6 +15,7 @@ import validateTrueFalseQuestions from './validation';
import setEditingAlert from '../Shared/setEditingAlert';
import { DragEndEvent } from '@dnd-kit/core';
import { handleTrueFalseReorder } from '@/stores/examEditor/reorder/local';
import PromptEdit from '../Shared/PromptEdit';
const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> = ({ exercise, sectionId }) => {
const { currentModule, dispatch } = useExamEditorStore();
@@ -28,7 +26,6 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> =
const section = state as ReadingPart;
const [local, setLocal] = useState(exercise);
const [editingPrompt, setEditingPrompt] = useState(false);
const [alerts, setAlerts] = useState<AlertItem[]>([]);
@@ -134,36 +131,10 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> =
handleDiscard={handleDiscard}
/>
{alerts.length > 0 && <Alert className="mb-6" alerts={alerts} />}
<Card className="mb-6">
<CardContent className="p-4">
<div className="flex justify-between items-start gap-4">
{editingPrompt ? (
<textarea
className="flex-1 p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none min-h-[100px]"
value={local.prompt}
onChange={(e) => updateLocal({ ...local, prompt: e.target.value })}
onBlur={() => setEditingPrompt(false)}
autoFocus
/>
) : (
<div className="flex-1">
<h3 className="font-medium text-gray-800 mb-2">Question/Instructions displayed to the student:</h3>
<p className="text-gray-600">{local.prompt}</p>
</div>
)}
<button
onClick={() => setEditingPrompt(!editingPrompt)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
{editingPrompt ?
<MdEditOff size={20} className="text-gray-500" /> :
<MdEdit size={20} className="text-gray-500" />
}
</button>
</div>
</CardContent>
</Card>
<PromptEdit
value={local.prompt}
onChange={(text) => updateLocal({ ...local, prompt: text })}
/>
<div className="space-y-4">
<QuestionsList
ids={local.questions.map(q => q.id)}

View File

@@ -23,7 +23,7 @@ import { handleWriteBlanksReorder } from '@/stores/examEditor/reorder/local';
import { ParsedQuestion, parseText, reconstructText } from './parsing';
const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise }> = ({ sectionId, exercise }) => {
const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise; title?: string; }> = ({ sectionId, exercise, title = "Write Blanks Exercise" }) => {
const { currentModule, dispatch } = useExamEditorStore();
const { state } = useExamEditorStore(
@@ -198,7 +198,6 @@ const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise }
const handleDragEnd = (event: DragEndEvent) => {
setEditing(true);
console.log("ASOJNFOAI+SHJOIPFAS");
setLocal(handleWriteBlanksReorder(event, local));
}
@@ -223,7 +222,7 @@ const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise }
return (
<div className="p-4">
<Header
title="Write Blanks Exercise"
title={title}
description="Edit questions and their solutions"
editing={editing}
handleSave={handleSave}

View File

@@ -15,15 +15,13 @@ interface Props {
exercise: WritingExercise;
}
const Writing: React.FC<Props> = ({ sectionId }) => {
const Writing: React.FC<Props> = ({ sectionId, exercise }) => {
const { currentModule, dispatch } = useExamEditorStore();
const { edit } = useExamEditorStore((store) => store.modules[currentModule]);
const { generating, genResult, state } = useExamEditorStore(
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
);
const exercise = state as WritingExercise;
const [local, setLocal] = useState(exercise);
const [prompt, setPrompt] = useState(exercise.prompt);
const [loading, setLoading] = useState(generating && generating == "exercises");
@@ -43,7 +41,9 @@ const Writing: React.FC<Props> = ({ sectionId }) => {
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId: sectionId, update: newExercise } });
},
onDiscard: () => {
setEditing(false);
setLocal(exercise);
setPrompt(exercise.prompt);
},
});
@@ -67,10 +67,6 @@ const Writing: React.FC<Props> = ({ sectionId }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [genResult, dispatch, sectionId, setEditing, currentModule]);
useEffect(() => {
setLocal(state as WritingExercise);
}, [state]);
useEffect(() => {
setEditingAlert(prompt !== local.prompt, setAlerts);
}, [prompt, local.prompt]);

View File

@@ -59,7 +59,7 @@ const useSettingsState = <T extends SectionSettings>(
}, [dispatch, module, sectionId]);
const updateLocalAndScheduleGlobal = useCallback((updates: Partial<T>) => {
const updateLocalAndScheduleGlobal = useCallback((updates: Partial<T>, schedule: boolean = true) => {
setLocalSettings(prev => ({
...prev,
...updates
@@ -69,7 +69,9 @@ const useSettingsState = <T extends SectionSettings>(
...pendingUpdatesRef.current,
...updates
};
debouncedUpdateGlobal();
if (schedule) {
debouncedUpdateGlobal();
}
}, [debouncedUpdateGlobal]);
return {

View File

@@ -37,6 +37,7 @@ const ListeningContext: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
setScript(genResult[0].script)
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [genResult, dispatch, sectionId, setEditing, currentModule]);
const renderContent = (editing: boolean) => {

View File

@@ -5,6 +5,7 @@ import { Exercise, WriteBlanksExercise } from '@/interfaces/exam';
import MultipleChoice from '../../Exercises/MultipleChoice';
import WriteBlanksForm from '../../Exercises/WriteBlanksForm';
import WriteBlanksFill from '../../Exercises/Blanks/WriteBlankFill';
import WriteBlanks from '../../Exercises/WriteBlanks';
const writeBlanks = (exercise: WriteBlanksExercise, index: number, sectionId: number, previewLabel: (text: string) => string) => {
@@ -44,6 +45,22 @@ const writeBlanks = (exercise: WriteBlanksExercise, index: number, sectionId: nu
),
content: <WriteBlanksFill exercise={exercise} sectionId={sectionId} />
};
case 'questions':
return {
id: index,
sectionId,
label: (
<ExerciseLabel
label={`Write Blanks: Questions #${firstWordId} ${firstWordId === lastWordId ? '' : `- #${lastWordId}`}`}
preview={
<>
&quot;{previewLabel(exercise.prompt)}...&quot;
</>
}
/>
),
content: <WriteBlanks exercise={exercise} sectionId={sectionId} title='Write Blanks: Questions' />
};
}
}

View File

@@ -80,10 +80,6 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
});
}, [updateLocalAndScheduleGlobal]);
const submitExam = () => {
}
return (
<div className={`flex flex-col gap-8 border bg-ielts-${module}/20 rounded-3xl p-8 w-1/3 h-fit`}>
<div className={`w-full flex justify-center text-ielts-${module} font-bold text-xl`}>{sectionLabel} Settings</div>
@@ -92,7 +88,7 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
title="Category"
module={module}
open={localSettings.isCategoryDropdownOpen}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isCategoryDropdownOpen: isOpen })}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isCategoryDropdownOpen: isOpen }, false)}
>
<Input
key={`section-${sectionId}`}
@@ -108,7 +104,7 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
title="Divider"
module={module}
open={localSettings.isIntroDropdownOpen}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isIntroDropdownOpen: isOpen })}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isIntroDropdownOpen: isOpen }, false)}
>
<div className="flex flex-col gap-3 w-full">
<Select

View File

@@ -74,7 +74,7 @@ const LevelSettings: React.FC = () => {
}
contentWrapperClassName={"pt-4 px-2 bg-white rounded-b-lg shadow-md transition-all duration-300 ease-in-out"}
open={localSettings.isExerciseDropdownOpen}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen })}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen }, false)}
>
<ExercisePicker
module="level"

View File

@@ -8,17 +8,33 @@ import { Generating, ListeningSectionSettings } from "@/stores/examEditor/types"
import Option from "@/interfaces/option";
import useExamEditorStore from "@/stores/examEditor";
import useSettingsState from "../Hooks/useSettingsState";
import { ListeningPart } from "@/interfaces/exam";
import { ListeningExam, ListeningPart } from "@/interfaces/exam";
import Input from "@/components/Low/Input";
import openDetachedTab from "@/utils/popout";
import { useRouter } from "next/router";
import axios from "axios";
import { usePersistentExamStore } from "@/stores/examStore";
import { playSound } from "@/utils/sound";
import { toast } from "react-toastify";
const ListeningSettings: React.FC = () => {
const {currentModule } = useExamEditorStore();
const router = useRouter();
const {currentModule, title } = useExamEditorStore();
const {
focusedSection,
difficulty,
sections,
minTimer,
isPrivate
} = useExamEditorStore(state => state.modules[currentModule]);
const {
setExam,
setExerciseIndex,
setPartIndex,
setQuestionIndex,
} = usePersistentExamStore();
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<ListeningSectionSettings>(
currentModule,
focusedSection
@@ -61,22 +77,90 @@ const ListeningSettings: React.FC = () => {
updateLocalAndScheduleGlobal({ topic });
}, [updateLocalAndScheduleGlobal]);
const submitListening = () => {
if (title === "") {
toast.error("Enter a title for the exam!");
return;
}
const exam: ListeningExam = {
parts: sections.map((s) => {
const exercise = s.state as ListeningPart;
return {
...exercise,
intro: localSettings.currentIntro,
category: localSettings.category
};
}),
isDiagnostic: false,
minTimer,
module: "listening",
id: title,
variant: sections.length === 4 ? "full" : "partial",
difficulty,
private: isPrivate,
};
axios.post(`/api/exam/listening`, exam)
.then((result) => {
playSound("sent");
toast.success(`Submitted Exam ID: ${result.data.id}`);
})
.catch((error) => {
console.log(error);
toast.error(error.response.data.error || "Something went wrong while submitting, please try again later.");
})
}
const preview = () => {
setExam({
parts: sections.map((s) => {
const exercise = s.state as ListeningPart;
return {
...exercise,
intro: localSettings.currentIntro,
category: localSettings.category
};
}),
minTimer,
module: "listening",
id: title,
isDiagnostic: false,
variant: undefined,
difficulty,
private: isPrivate,
} as ListeningExam);
setExerciseIndex(0);
setQuestionIndex(0);
setPartIndex(0);
openDetachedTab("popout?type=Exam&module=listening", router)
}
const canPreview = sections.some(
(s) => (s.state as ListeningPart).exercises && (s.state as ListeningPart).exercises.length > 0
);
const canSubmit = sections.every(
(s) => (s.state as ListeningPart).exercises &&
(s.state as ListeningPart).exercises.length > 0 &&
(s.state as ListeningPart).audio !== undefined
);
return (
<SettingsEditor
sectionLabel={`Section ${focusedSection}`}
sectionId={focusedSection}
module="listening"
introPresets={[defaultPresets[focusedSection - 1]]}
preview={()=> {}}
canPreview={false}
canSubmit={false}
submitModule={()=> {}}
canPreview={canPreview}
canSubmit={canSubmit}
preview={preview}
submitModule={submitListening}
>
<Dropdown
title="Audio Context"
module={currentModule}
open={localSettings.isAudioContextOpen}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isAudioContextOpen: isOpen })}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isAudioContextOpen: isOpen }, false)}
>
<div className="flex flex-row gap-2 items-center px-2 pb-4">
<div className="flex flex-col flex-grow gap-4 px-2">
@@ -105,7 +189,7 @@ const ListeningSettings: React.FC = () => {
title="Add Exercises"
module={currentModule}
open={localSettings.isExerciseDropdownOpen}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen })}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen }, false)}
disabled={currentSection.script === undefined && currentSection.audio === undefined}
>
<ExercisePicker

View File

@@ -85,6 +85,10 @@ const ReadingSettings: React.FC = () => {
);
const submitReading = () => {
if (title === "") {
toast.error("Enter a title for the exam!");
return;
}
const exam: ReadingExam = {
parts: sections.map((s) => {
const exercise = s.state as ReadingPart;
@@ -154,7 +158,7 @@ const ReadingSettings: React.FC = () => {
title="Generate Passage"
module={currentModule}
open={localSettings.isPassageOpen}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isPassageOpen: isOpen })}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isPassageOpen: isOpen }, false)}
>
<div className="flex flex-row gap-2 items-center px-2 pb-4">
<div className="flex flex-col flex-grow gap-4 px-2">
@@ -184,7 +188,7 @@ const ReadingSettings: React.FC = () => {
module={currentModule}
open={localSettings.isExerciseDropdownOpen}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen })}
disabled={currentSection.text.content === "" || currentSection.text.title === ""}
disabled={currentSection.text === undefined || currentSection.text.content === "" || currentSection.text.title === ""}
>
<ExercisePicker
module="reading"

View File

@@ -113,7 +113,7 @@ const SpeakingSettings: React.FC = () => {
title="Generate Script"
module={currentModule}
open={localSettings.isExerciseDropdownOpen}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen })}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen }, false)}
>
<div className={clsx("gap-2 px-2 pb-4", focusedSection === 1 ? "flex flex-col w-full" : "flex flex-row items-center" )}>

View File

@@ -149,7 +149,7 @@ const WritingSettings: React.FC = () => {
title="Generate Instructions"
module={currentModule}
open={localSettings.isExerciseDropdownOpen}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen })}
setIsOpen={(isOpen: boolean) => updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen }, false)}
>
<div className="flex flex-row gap-2 items-center px-2 pb-4">

View File

@@ -1,7 +1,7 @@
import { Module } from "@/interfaces";
import clsx from "clsx";
import { ReactNode } from "react";
import { MdDelete, MdEdit, MdEditOff, MdRefresh, MdSave } from "react-icons/md";
import { MdDelete, MdEdit, MdEditOff, MdRefresh, MdSave, MdGrade, MdOutlineGrade } from "react-icons/md";
interface Props {
title: string;
@@ -11,13 +11,15 @@ interface Props {
handleSave: () => void;
handleDiscard: () => void;
modeHandle?: () => void;
evaluationHandle?: () => void;
isEvaluationEnabled?: boolean;
mode?: "delete" | "edit";
children?: ReactNode;
}
const Header: React.FC<Props> = ({ title, description, editing, handleSave, handleDiscard, modeHandle, children, mode = "delete", module }) => {
const Header: React.FC<Props> = ({ title, description, editing, isEvaluationEnabled, handleSave, handleDiscard, modeHandle, evaluationHandle, children, mode = "delete", module }) => {
return (
<div className="flex justify-between items-center mb-6">
<div className="flex justify-between items-center mb-6 text-sm">
<div>
<h1 className="text-2xl font-bold text-gray-800">{title}</h1>
<p className="text-gray-600 mt-1">{description}</p>
@@ -59,10 +61,24 @@ const Header: React.FC<Props> = ({ title, description, editing, handleSave, hand
onClick={modeHandle}
className={`px-4 py-2 bg-ielts-${module}/80 text-white hover:bg-ielts-${module} rounded-lg transition-all duration-200 flex items-center gap-2`}
>
{ editing ? <MdEditOff size={18} /> : <MdEdit size={18} /> }
{editing ? <MdEditOff size={18} /> : <MdEdit size={18} />}
Edit
</button>
)}
{mode === "delete" &&
<button
onClick={evaluationHandle}
className={clsx(
"px-4 py-2 rounded-lg flex items-center gap-2 transition-all duration-200",
isEvaluationEnabled
? 'bg-amber-500 text-white hover:bg-amber-600'
: 'bg-gray-200 text-gray-600 hover:bg-gray-300'
)}
>
{isEvaluationEnabled ? <MdGrade size={18} /> : <MdOutlineGrade size={18} />}
{isEvaluationEnabled ? 'Graded Exercise' : 'Practice Only'}
</button>
}
</div>
</div>
);

View File

@@ -1,8 +1,9 @@
import Level from "@/exams/Level";
import Listening from "@/exams/Listening";
import Reading from "@/exams/Reading";
import Writing from "@/exams/Writing";
import { usePersistentStorage } from "@/hooks/usePersistentStorage";
import { LevelExam, ReadingExam, WritingExam } from "@/interfaces/exam";
import { LevelExam, ListeningExam, ReadingExam, WritingExam } from "@/interfaces/exam";
import { User } from "@/interfaces/user";
import { usePersistentExamStore } from "@/stores/examStore";
import clsx from "clsx";
@@ -34,6 +35,13 @@ const Popout: React.FC<{ user: User }> = ({ user }) => {
state.setQuestionIndex(0);
}} preview={true} />
}
{state.exam?.module == "listening" && state.exam.parts.length > 0 &&
<Listening exam={state.exam as ListeningExam} onFinish={() => {
state.setPartIndex(0);
state.setExerciseIndex(-1);
state.setQuestionIndex(0);
}} preview={true} />
}
</div>
</div>
);