From 5165b6ae6dc4641e155921d719e1bbbce5570e07 Mon Sep 17 00:00:00 2001 From: Carlos-Mesquita Date: Wed, 6 Nov 2024 19:43:06 +0000 Subject: [PATCH 1/3] Listening Convo Edit and a bunch of ts errors --- .../ExamEditor/Exercises/Script/Message.tsx | 86 +++++ .../ExamEditor/Exercises/Script/index.tsx | 330 ++++++++++++++++++ .../ExamEditor/Exercises/Shared/Script.tsx | 134 ------- .../SectionContext/listening.tsx | 51 +-- .../SectionExercises/index.tsx | 82 +++-- .../SectionExercises/level.tsx | 10 +- .../SectionExercises/listening.tsx | 44 +-- .../SectionExercises/reading.tsx | 14 +- .../SectionRenderer/SectionExercises/types.ts | 11 +- .../Shared/ExercisePicker/index.tsx | 4 +- .../Shared/ExercisePicker/templates.ts | 0 src/components/Low/AutoExpandingTextarea.tsx | 3 + src/interfaces/exam.ts | 4 +- 13 files changed, 555 insertions(+), 218 deletions(-) create mode 100644 src/components/ExamEditor/Exercises/Script/Message.tsx create mode 100644 src/components/ExamEditor/Exercises/Script/index.tsx delete mode 100644 src/components/ExamEditor/Exercises/Shared/Script.tsx delete mode 100644 src/components/ExamEditor/Shared/ExercisePicker/templates.ts diff --git a/src/components/ExamEditor/Exercises/Script/Message.tsx b/src/components/ExamEditor/Exercises/Script/Message.tsx new file mode 100644 index 00000000..b2e3fd4c --- /dev/null +++ b/src/components/ExamEditor/Exercises/Script/Message.tsx @@ -0,0 +1,86 @@ +import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea"; +import { useState } from "react"; +import { FaEdit, FaFemale, FaMale } from "react-icons/fa"; +import { FaTrash } from "react-icons/fa6"; +import { ScriptLine } from "."; + +interface MessageProps { + message: ScriptLine & { position: 'left' | 'right' }; + color: string; + editing: boolean; + onEdit?: (text: string) => void; + onDelete?: () => void; +} + +const Message: React.FC = ({ message, color, editing, onEdit, onDelete }) => { + const [isEditing, setIsEditing] = useState(false); + const [editText, setEditText] = useState(message.text); + + return ( +
+
+
+ {message.gender === 'male' ? ( + + ) : ( + + )} + {message.name} +
+
+ {isEditing ? ( +
+ +
+ + +
+
+ ) : ( +
+

{message.text}

+ {editing && ( +
+
+ + +
+
+ )} +
+ )} +
+
+
+ ); +}; + +export default Message; diff --git a/src/components/ExamEditor/Exercises/Script/index.tsx b/src/components/ExamEditor/Exercises/Script/index.tsx new file mode 100644 index 00000000..3fc586a1 --- /dev/null +++ b/src/components/ExamEditor/Exercises/Script/index.tsx @@ -0,0 +1,330 @@ +import React, { useState, useMemo } from 'react'; +import { Script } from "@/interfaces/exam"; +import Message from './Message'; +import AutoExpandingTextArea from '@/components/Low/AutoExpandingTextarea'; +import { Card, CardContent } from '@/components/ui/card'; +import Input from '@/components/Low/Input'; +import { FaFemale, FaMale, FaPlus } from 'react-icons/fa'; +import clsx from 'clsx'; + +export interface Speaker { + id: number; + name: string; + gender: 'male' | 'female'; + color: string; + position: 'left' | 'right'; +} + +type Gender = 'male' | 'female'; + +export interface ScriptLine { + name: string; + gender: Gender; + text: string; + voice?: string; +} + +interface MessageWithPosition extends ScriptLine { + position: 'left' | 'right'; +} + +interface Props { + section: number; + editing?: boolean; + local: Script; + setLocal: (script: Script) => void; +} + +const colorOptions = [ + 'red', 'blue', 'green', 'purple', 'pink', 'indigo', 'teal', 'orange', + 'cyan', 'emerald', 'sky', 'violet', 'fuchsia', 'rose', 'lime', 'slate' +]; + +const ScriptEditor: React.FC = ({ section, editing = false, local, setLocal }) => { + const isConversation = [1, 3].includes(section); + + const [selectedSpeaker, setSelectedSpeaker] = useState(''); + const [newMessage, setNewMessage] = useState(''); + + const speakerCount = section === 1 ? 2 : 4; + + const [speakers, setSpeakers] = useState(() => { + const existingScript = local as ScriptLine[]; + const existingSpeakers = new Set(); + const speakerGenders = new Map(); + + if (Array.isArray(existingScript)) { + existingScript.forEach(line => { + existingSpeakers.add(line.name); + speakerGenders.set(line.name, line.gender.toLowerCase() === 'female' ? 'female' : 'male' as 'male' | 'female'); + }); + } + + const speakerArray = Array.from(existingSpeakers); + const totalNeeded = Math.max(speakerCount, speakerArray.length); + + return Array.from({ length: totalNeeded }, (_, index) => { + if (index < speakerArray.length) { + return { + id: index, + name: speakerArray[index], + gender: speakerGenders.get(speakerArray[index]) || 'male', + color: colorOptions[index], + position: index % 2 === 0 ? 'left' : 'right' + }; + } + return { + id: index, + name: '', + gender: 'male', + color: colorOptions[index], + position: index % 2 === 0 ? 'left' : 'right' + }; + }); + }); + + const speakerProperties = useMemo(() => { + return speakers.reduce((acc, speaker) => { + if (speaker.name) { + acc[speaker.name] = { + color: speaker.color, + gender: speaker.gender + }; + } + return acc; + }, {} as Record); + }, [speakers]); + + const allSpeakersConfigured = useMemo(() => { + return speakers.every(speaker => speaker.name.trim() !== ''); + }, [speakers]); + + const updateSpeaker = (index: number, updates: Partial) => { + const updatedSpeakers = speakers.map((speaker, i) => { + if (i === index) { + return { ...speaker, ...updates }; + } + return speaker; + }); + setSpeakers(updatedSpeakers); + + if (Array.isArray(local)) { + if ('name' in updates && speakers[index].name) { + const oldName = speakers[index].name; + const newName = updates.name || ''; + const updatedScript = local.map(line => { + if (line.name === oldName) { + return { ...line, name: newName }; + } + return line; + }); + setLocal(updatedScript); + } + + if ('gender' in updates && speakers[index].name && updates.gender) { + const name = speakers[index].name; + const newGender = updates.gender; + const updatedScript = local.map(line => { + if (line.name === name) { + return { ...line, gender: newGender }; + } + return line; + }); + setLocal(updatedScript); + } + } + + if ('name' in updates && speakers[index].name === selectedSpeaker) { + setSelectedSpeaker(updates.name || ''); + } + }; + + + const addMessage = () => { + if (!isConversation || !selectedSpeaker || !newMessage.trim()) return; + if (!Array.isArray(local)) return; + + const speaker = speakers.find(s => s.name === selectedSpeaker); + if (!speaker) return; + + const newLine: ScriptLine = { + name: selectedSpeaker, + gender: speaker.gender, + text: newMessage.trim() + }; + + const updatedScript = [...local, newLine]; + setLocal(updatedScript); + setNewMessage(''); + }; + + const updateMessage = (index: number, newText: string) => { + if (!Array.isArray(local)) return; + + const updatedScript = [...local]; + updatedScript[index] = { ...updatedScript[index], text: newText }; + setLocal(updatedScript); + }; + + const deleteMessage = (index: number) => { + if (!Array.isArray(local)) return; + + const updatedScript = local.filter((_, i) => i !== index); + setLocal(updatedScript); + }; + + const updateMonologue = (text: string) => { + setLocal(text); + }; + + const messages = useMemo(() => { + if (typeof local === 'string' || !Array.isArray(local)) return []; + + return local.reduce((acc, line, index) => { + const normalizedLine = { + ...line, + gender: line.gender.toLowerCase() === 'female' ? 'female' : 'male' + } as ScriptLine; + + if (index === 0) { + acc.push({ ...normalizedLine, position: 'left' }); + } else { + const prevMsg = acc[index - 1]; + const position = line.name === prevMsg.name + ? prevMsg.position + : (prevMsg.position === 'left' ? 'right' : 'left'); + acc.push({ ...normalizedLine, position }); + } + return acc; + }, []); + }, [local]); + + if (!isConversation) { + return ( + + +
+ {editing ? ( + + ) : ( +
+ {(local as string) || "Edit, generate or import your own audio."} +
+ )} +
+
+
+ ); + } + + return ( + + +
+ {editing && ( +
+

Edit Conversation

+
+ {speakers.map((speaker, index) => ( +
+
+ updateSpeaker(index, { name: text })} + placeholder="Speaker name" + /> +
+ +
+ +
+ {speaker.gender === 'male' ? ( + + ) : ( + + )} +
+
+
+ ))} +
+
+
+ + +
+ +
+ +
+
+
+ )} + +
+ {messages.map((message, index) => { + const properties = speakerProperties[message.name]; + if (!properties) return null; + + return ( + updateMessage(index, text)} + onDelete={() => deleteMessage(index)} + /> + ); + })} +
+
+
+
+ ); +}; + +export default ScriptEditor; diff --git a/src/components/ExamEditor/Exercises/Shared/Script.tsx b/src/components/ExamEditor/Exercises/Shared/Script.tsx deleted file mode 100644 index 759150c4..00000000 --- a/src/components/ExamEditor/Exercises/Shared/Script.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { Script } from '@/interfaces/exam'; -import { FaFemale, FaMale } from "react-icons/fa"; -import AutoExpandingTextArea from '@/components/Low/AutoExpandingTextarea'; -import clsx from 'clsx'; - -const colorOptions = [ - 'red', 'blue', 'green', 'purple', 'pink', 'indigo', 'teal', 'orange', - 'cyan', 'emerald', 'sky', 'violet', 'fuchsia', 'rose', 'lime', 'slate' -]; - -interface Speaker { - id: number; - name: string; - gender: 'male' | 'female'; - color: string; - position: 'left' | 'right'; -} - -interface Props { - script?: Script; - setScript: React.Dispatch>; - editing?: boolean; -} - -const ScriptRender: React.FC = ({ script, setScript, editing = false }) => { - const [speakers, setSpeakers] = useState(() => { - if (!script || typeof script === 'string') return []; - - const uniqueSpeakers = new Map(); - const usedColors = new Set(); - let isLeft = true; - - script.forEach((line, index) => { - if (!uniqueSpeakers.has(line.name)) { - const availableColors = colorOptions.filter(color => !usedColors.has(color)); - if (availableColors.length === 0) { - usedColors.clear(); - } - const randomColor = availableColors[Math.floor(Math.random() * availableColors.length)]; - usedColors.add(randomColor); - - uniqueSpeakers.set(line.name, { - id: index, - name: line.name, - gender: line.gender, - color: randomColor, - position: isLeft ? 'left' : 'right' - }); - isLeft = !isLeft; - } - }); - return Array.from(uniqueSpeakers.values()); - }); - - const speakerProperties = useMemo(() => { - return speakers.reduce((acc, speaker) => { - acc[speaker.name] = { - color: speaker.color, - position: speaker.position - }; - return acc; - }, {} as Record); - }, [speakers]); - - if (script === undefined) return null; - - if (typeof script === 'string') { - return ( -
- {editing ? ( - setScript(text)} - /> - ) : ( -

") }} /> - )} -

- ); - } - - const updateMessage = (index: number, newText: string) => { - setScript([ - ...script.slice(0, index), - { ...script[index], text: newText }, - ...script.slice(index + 1) - ]); - }; - - return ( -
-
- {script.map((line, index) => { - const { color, position } = speakerProperties[line.name]; - - return ( -
-
-
- {line.gender === 'male' ? ( - - ) : ( - - )} - - {line.name} - -
-
- {editing ? ( - updateMessage(index, text)} - /> - ) : ( -

{line.text}

- )} -
-
-
- ); - })} -
-
- ); -}; - -export default ScriptRender; \ No newline at end of file diff --git a/src/components/ExamEditor/SectionRenderer/SectionContext/listening.tsx b/src/components/ExamEditor/SectionRenderer/SectionContext/listening.tsx index 88bcd95d..032c89a4 100644 --- a/src/components/ExamEditor/SectionRenderer/SectionContext/listening.tsx +++ b/src/components/ExamEditor/SectionRenderer/SectionContext/listening.tsx @@ -2,10 +2,10 @@ import { useEffect, useState } from "react"; import { ListeningPart } from "@/interfaces/exam"; import SectionContext from "."; import useExamEditorStore from "@/stores/examEditor"; -import { FaFemale, FaMale } from "react-icons/fa"; import useSectionEdit from "../../Hooks/useSectionEdit"; -import ScriptRender from "../../Exercises/Shared/Script"; +import ScriptRender from "../../Exercises/Script"; import { Card, CardContent } from "@/components/ui/card"; +import Dropdown from "@/components/Dropdown"; const ListeningContext: React.FC<{ sectionId: number; }> = ({ sectionId }) => { @@ -15,50 +15,55 @@ const ListeningContext: React.FC<{ sectionId: number; }> = ({ sectionId }) => { ); const listeningPart = state as ListeningPart; - const [script, setScript] = useState(listeningPart.script); + const [scriptLocal, setScriptLocal] = useState(listeningPart.script); const { editing, handleSave, handleDiscard, modeHandle, setEditing } = useSectionEdit({ sectionId, mode: "edit", onSave: () => { const newState = { ...listeningPart }; - newState.script = script; + newState.script = scriptLocal; dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } }) setEditing(false); }, onDiscard: () => { - setScript(listeningPart.script); + setScriptLocal(listeningPart.script); }, }); useEffect(() => { if (genResult !== undefined && generating === "context") { setEditing(true); - setScript(genResult[0].script) + setScriptLocal(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 + // eslint-disable-next-line react-hooks/exhaustive-deps }, [genResult, dispatch, sectionId, setEditing, currentModule]); const renderContent = (editing: boolean) => { - - if (script === undefined && !editing) { - return (

- Generate or import audio to add exercises! -

- ) + if (scriptLocal === undefined) { + return ( + + + Edit, generate or import your own audio. + + + ); } - return ( - - - - - + + + + ); }; diff --git a/src/components/ExamEditor/SectionRenderer/SectionExercises/index.tsx b/src/components/ExamEditor/SectionRenderer/SectionExercises/index.tsx index d635b04d..7f40e22e 100644 --- a/src/components/ExamEditor/SectionRenderer/SectionExercises/index.tsx +++ b/src/components/ExamEditor/SectionRenderer/SectionExercises/index.tsx @@ -1,13 +1,13 @@ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import SortableSection from "../../Shared/SortableSection"; import getReadingQuestions from '../SectionExercises/reading'; -import { Exercise, LevelPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam"; -import { ReadingExercise } from "./types"; +import { Exercise, LevelPart, ListeningPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam"; +import ExerciseItem, { ReadingExercise } from "./types"; import Dropdown from "@/components/Dropdown"; import useExamEditorStore from "@/stores/examEditor"; import Writing from "../../Exercises/Writing"; import Speaking from "../../Exercises/Speaking"; -import { ReactNode, useEffect, useState } from "react"; +import { ReactElement, ReactNode, useEffect, useState } from "react"; import { DndContext, PointerSensor, @@ -15,15 +15,18 @@ import { useSensors, DragEndEvent, closestCenter, + UniqueIdentifier, } from '@dnd-kit/core'; import GenLoader from "../../Exercises/Shared/GenLoader"; import { ExamPart } from "@/stores/examEditor/types"; import getListeningItems from "./listening"; import getLevelQuestionItems from "./level"; +import React from "react"; -export interface Props { - sectionId: number; +interface QuestionItemsResult { + ids: string[]; + items: ExerciseItem[]; } const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { @@ -49,7 +52,7 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { }) dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } }) } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [genResult, dispatch, sectionId, currentModule]); const currentSection = sections.find((s) => s.sectionId === sectionId)!; @@ -58,27 +61,44 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { useSensor(PointerSensor), ); - const questionItems = () => { - let ids, items; + const questionItems = (): QuestionItemsResult => { + let result: QuestionItemsResult = { + ids: [], + items: [] + }; + switch (currentModule) { - case "reading": - items = getReadingQuestions((currentSection.state as ReadingPart).exercises as ReadingExercise[], sectionId); - ids = items.map(q => q.id.toString()); + case "reading": { + const items = getReadingQuestions( + (currentSection.state as ReadingPart).exercises as ReadingExercise[], + sectionId + ); + result.items = items.filter((item): item is ExerciseItem => item !== undefined); + result.ids = result.items.map(item => item.id); break; - case "listening": - items = getListeningItems((currentSection.state as ReadingPart).exercises as ReadingExercise[], sectionId); - ids = items.map(q => q?.id.toString()); + } + case "listening": { + const items = getListeningItems( + (currentSection.state as ListeningPart).exercises as Exercise[], + sectionId + ); + result.items = items.filter((item): item is ExerciseItem => item !== undefined); + result.ids = result.items.map(item => item.id); break; - case "level": - items = getLevelQuestionItems((currentSection.state as LevelPart).exercises as Exercise[], sectionId); - ids = items.map(q => q.id.toString()); + } + case "level": { + const items = getLevelQuestionItems( + (currentSection.state as LevelPart).exercises as Exercise[], + sectionId + ); + result.items = items.filter((item): item is ExerciseItem => item !== undefined); + result.ids = result.items.map(item => item.id); break; + } } - return { ids, items } - } - - const questions = questionItems(); + return result; + }; const background = (component: ReactNode) => { return ( @@ -91,6 +111,20 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => { if (currentModule == "writing") return background(); if (currentModule == "speaking") return background(); + const questions = questionItems(); + + const filteredIds = (questions.ids ?? []).filter(Boolean); + + function isValidItem(item: ExerciseItem | undefined): item is ExerciseItem { + return item !== undefined && + typeof item.id === 'string' && + typeof item.sectionId === 'number' && + React.isValidElement(item.label) && + React.isValidElement(item.content); + } + + const filteredItems = (questions.items ?? []).filter(isValidItem); + return ( = ({ sectionId }) => { questions.ids.length > 0 && (
- {questions.items.map(item => ( - + {filteredItems.map(item => ( + item !== undefined); + }).filter(isExerciseItem); - return items || []; + return items; }; diff --git a/src/components/ExamEditor/SectionRenderer/SectionExercises/listening.tsx b/src/components/ExamEditor/SectionRenderer/SectionExercises/listening.tsx index 9420ee73..9b520eca 100644 --- a/src/components/ExamEditor/SectionRenderer/SectionExercises/listening.tsx +++ b/src/components/ExamEditor/SectionRenderer/SectionExercises/listening.tsx @@ -1,4 +1,4 @@ -import ExerciseItem from './types'; +import ExerciseItem, { isExerciseItem } from './types'; import ExerciseLabel from '../../Shared/ExerciseLabel'; import FillBlanksLetters from '../../Exercises/Blanks/Letters'; import { Exercise, WriteBlanksExercise } from '@/interfaces/exam'; @@ -7,15 +7,14 @@ 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) => { +const writeBlanks = (exercise: WriteBlanksExercise, index: number, sectionId: number, previewLabel: (text: string) => string): ExerciseItem => { const firstWordId = exercise.solutions[0].id; const lastWordId = exercise.solutions[exercise.solutions.length - 1].id; switch (exercise.variant) { case 'form': return { - id: index, + id: index.toString(), sectionId, label: ( }; } -} - - -const getListeningItems = (exercises: Exercise[], sectionId: number) => { + throw new Error(`Just so that typescript doesnt complain`); +}; +const getListeningItems = (exercises: Exercise[], sectionId: number): ExerciseItem[] => { const previewLabel = (text: string) => { return text !== undefined ? text.replaceAll('\\n', ' ').split(' ').slice(0, 15).join(' ') : ""; - } + }; - const items = exercises.map((exercise, index) => { + const mappedItems = exercises.map((exercise, index): ExerciseItem | null => { let firstWordId, lastWordId; + switch (exercise.type) { case "fillBlanks": firstWordId = exercise.solutions[0].id; lastWordId = exercise.solutions[exercise.solutions.length - 1].id; return { - id: index, + id: index.toString(), sectionId, label: ( { /> ), content: - }; + case "writeBlanks": return writeBlanks(exercise, index, sectionId, previewLabel); + case "multipleChoice": firstWordId = exercise.questions[0].id; lastWordId = exercise.questions[exercise.questions.length - 1].id; return { - id: index, + id: index.toString(), sectionId, label: ( { ), content: }; - } - }).filter((item) => item !== undefined); - return items || []; + default: + return null; + } + }); + + return mappedItems.filter((item): item is ExerciseItem => + item !== null && isExerciseItem(item) + ); }; - -export default getListeningItems; +export default getListeningItems; \ No newline at end of file diff --git a/src/components/ExamEditor/SectionRenderer/SectionExercises/reading.tsx b/src/components/ExamEditor/SectionRenderer/SectionExercises/reading.tsx index 3264fb85..7b31d770 100644 --- a/src/components/ExamEditor/SectionRenderer/SectionExercises/reading.tsx +++ b/src/components/ExamEditor/SectionRenderer/SectionExercises/reading.tsx @@ -1,4 +1,4 @@ -import ExerciseItem, { ReadingExercise } from './types'; +import ExerciseItem, { isExerciseItem, ReadingExercise } from './types'; import WriteBlanks from "@/editor/Exercises/WriteBlanks"; import ExerciseLabel from '../../Shared/ExerciseLabel'; import MatchSentences from '../../Exercises/MatchSentences'; @@ -19,7 +19,7 @@ const getExerciseItems = (exercises: ReadingExercise[], sectionId: number): Exer firstWordId = exercise.solutions[0].id; lastWordId = exercise.solutions[exercise.solutions.length - 1].id; return { - id: index, + id: index.toString(), sectionId, label: ( item !== undefined); + }).filter(isExerciseItem); - return items || []; + return items; }; diff --git a/src/components/ExamEditor/SectionRenderer/SectionExercises/types.ts b/src/components/ExamEditor/SectionRenderer/SectionExercises/types.ts index bea1509f..0a06164b 100644 --- a/src/components/ExamEditor/SectionRenderer/SectionExercises/types.ts +++ b/src/components/ExamEditor/SectionRenderer/SectionExercises/types.ts @@ -1,10 +1,19 @@ import { FillBlanksExercise, MatchSentencesExercise, TrueFalseExercise, WriteBlanksExercise } from "@/interfaces/exam"; export default interface ExerciseItem { - id: number; + id: string; sectionId: number; label: React.ReactNode; content: React.ReactNode; } export type ReadingExercise = FillBlanksExercise | TrueFalseExercise | MatchSentencesExercise | WriteBlanksExercise; + +export function isExerciseItem(item: unknown): item is ExerciseItem { + return item !== undefined && + item !== null && + typeof (item as ExerciseItem).id === 'string' && + typeof (item as ExerciseItem).sectionId === 'number' && + (item as ExerciseItem).label !== undefined && + (item as ExerciseItem).content !== undefined; +} diff --git a/src/components/ExamEditor/Shared/ExercisePicker/index.tsx b/src/components/ExamEditor/Shared/ExercisePicker/index.tsx index 46b11cdf..33e3cb8e 100644 --- a/src/components/ExamEditor/Shared/ExercisePicker/index.tsx +++ b/src/components/ExamEditor/Shared/ExercisePicker/index.tsx @@ -7,7 +7,7 @@ import ExerciseWizard, { ExerciseConfig } from "./ExerciseWizard"; import { generate } from "../../SettingsEditor/Shared/Generate"; import { Module } from "@/interfaces"; import useExamEditorStore from "@/stores/examEditor"; -import { Dialog, ListeningPart, ReadingPart } from "@/interfaces/exam"; +import { ListeningPart, Message, ReadingPart } from "@/interfaces/exam"; interface ExercisePickerProps { module: string; @@ -81,7 +81,7 @@ const ExercisePicker: React.FC = ({ switch (sectionId) { case 1: case 3: - dialog = script as Dialog[]; + dialog = script as Message[]; text = dialog.map((d) => `${d.name}: ${d.text}`).join("\n"); context = { text: text } break; diff --git a/src/components/ExamEditor/Shared/ExercisePicker/templates.ts b/src/components/ExamEditor/Shared/ExercisePicker/templates.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/Low/AutoExpandingTextarea.tsx b/src/components/Low/AutoExpandingTextarea.tsx index 5614049e..39877246 100755 --- a/src/components/Low/AutoExpandingTextarea.tsx +++ b/src/components/Low/AutoExpandingTextarea.tsx @@ -5,6 +5,7 @@ interface Props { onChange: (value: string) => void; className?: string; placeholder?: string; + disabled?: boolean; onBlur?: () => void; } @@ -12,6 +13,7 @@ const AutoExpandingTextArea: React.FC = ({ value, className = 'w-full cursor-text px-7 py-8 input border-2 border-mti-gray-platinum bg-white rounded-3xl', placeholder = "Enter text here...", + disabled = false, onChange, onBlur, }) => { @@ -45,6 +47,7 @@ const AutoExpandingTextArea: React.FC = ({ placeholder={placeholder} style={{ overflow: 'hidden', resize: 'none' }} onBlur={onBlur} + disabled={disabled} autoFocus /> ); diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index 77846f21..0a5e3df9 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -62,8 +62,8 @@ export interface ListeningExam extends ExamBase { module: "listening"; } -export type Dialog = { "name": string; "gender": string; "text": string; }; -export type Script = Dialog[] | string; +export type Message = { name: string; gender: string; text: string; voice?: string; }; +export type Script = Message[] | string; export interface ListeningPart extends Section { audio?: { From 7045b4e3c724eb05b051ee9b344fac55393a51e6 Mon Sep 17 00:00:00 2001 From: Carlos-Mesquita Date: Wed, 6 Nov 2024 19:49:02 +0000 Subject: [PATCH 2/3] Ts changes weren't staged, it compiles still doesnt build building --- .../Underline/UnderlineQuestion.tsx | 1 + .../ExamEditor/Hooks/useSectionEdit.tsx | 4 +++ .../Shared/ImportExam/WordUploader.tsx | 31 ++++++++++--------- src/components/ExamEditor/index.tsx | 7 +++-- src/components/Solutions/TrueFalse.tsx | 2 +- src/components/UserDisplayList.tsx | 1 + .../UserTable.tsx | 2 +- src/exams/Listening.tsx | 16 ++++++---- src/exams/Writing.tsx | 1 + .../IUserImport.ts | 0 src/pages/(admin)/BatchCreateUser/index.tsx | 4 +-- src/pages/(admin)/Lists/ExamList.tsx | 3 +- src/pages/api/exam/generate/[...module].ts | 2 +- 13 files changed, 45 insertions(+), 29 deletions(-) rename src/{pages/(admin)/BatchCreateUser => components}/UserTable.tsx (99%) rename src/{pages/(admin)/BatchCreateUser => interfaces}/IUserImport.ts (100%) diff --git a/src/components/ExamEditor/Exercises/MultipleChoice/Underline/UnderlineQuestion.tsx b/src/components/ExamEditor/Exercises/MultipleChoice/Underline/UnderlineQuestion.tsx index 5ed4ce33..12365ae3 100644 --- a/src/components/ExamEditor/Exercises/MultipleChoice/Underline/UnderlineQuestion.tsx +++ b/src/components/ExamEditor/Exercises/MultipleChoice/Underline/UnderlineQuestion.tsx @@ -78,6 +78,7 @@ export const UnderlineQuestion: React.FC = ({ useEffect(() => { validateQuestion(question); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [question]); const handlePromptChange = (value: string) => { diff --git a/src/components/ExamEditor/Hooks/useSectionEdit.tsx b/src/components/ExamEditor/Hooks/useSectionEdit.tsx index 4100c4fc..f43056a6 100644 --- a/src/components/ExamEditor/Hooks/useSectionEdit.tsx +++ b/src/components/ExamEditor/Hooks/useSectionEdit.tsx @@ -32,6 +32,7 @@ const useSectionEdit = ({ const handleEdit = useCallback(() => { setEditing(true); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [sectionId, setEditing, updateRoot]); const handleSave = useCallback(() => { @@ -41,17 +42,20 @@ const useSectionEdit = ({ setEditing(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [setEditing, updateRoot, onSave, sectionId]); const handleDiscard = useCallback(() => { setEditing(false); onDiscard?.(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [setEditing, updateRoot, onDiscard, sectionId]); const modeHandle = useCallback(() => { setEditing(!editing); onMode?.(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [setEditing, editing, updateRoot, onMode, sectionId]); return { diff --git a/src/components/ExamEditor/Shared/ImportExam/WordUploader.tsx b/src/components/ExamEditor/Shared/ImportExam/WordUploader.tsx index 1e40744a..23ed7d01 100644 --- a/src/components/ExamEditor/Shared/ImportExam/WordUploader.tsx +++ b/src/components/ExamEditor/Shared/ImportExam/WordUploader.tsx @@ -10,7 +10,7 @@ import { ReadingPart } from '@/interfaces/exam'; import { defaultSectionSettings } from '@/stores/examEditor/defaults'; const WordUploader: React.FC<{ module: Module }> = ({ module }) => { - const {currentModule, dispatch} = useExamEditorStore(); + const { currentModule, dispatch } = useExamEditorStore(); const examInputRef = useRef(null); const solutionsInputRef = useRef(null); @@ -38,14 +38,14 @@ const WordUploader: React.FC<{ module: Module }> = ({ module }) => { } }; - const handleImport = useCallback( async () => { + const handleImport = useCallback(async () => { try { if (!examFile) { toast.error('Exam file is required'); return; } - dispatch({type: "UPDATE_MODULE", payload: {updates: {importing: true}, module}}) + dispatch({ type: "UPDATE_MODULE", payload: { updates: { importing: true }, module } }) const formData = new FormData(); formData.append('exercises', examFile); @@ -76,27 +76,30 @@ const WordUploader: React.FC<{ module: Module }> = ({ module }) => { const newSectionsStates = data.parts.map( (part: ReadingPart, index: number) => defaultSectionSettings(module, index + 1, part) ); - dispatch({type: "UPDATE_MODULE", payload: { - updates: { - sections: newSectionsStates, - minTimer: data.minTimer, - importModule: false, - importing: false, - }, - module - }}); + dispatch({ + type: "UPDATE_MODULE", payload: { + updates: { + sections: newSectionsStates, + minTimer: data.minTimer, + importModule: false, + importing: false, + }, + module + } + }); break; } } catch (error) { toast.error(`An unknown error has occured while import ${module} exam!`); } finally { - dispatch({type: "UPDATE_MODULE", payload: {updates: {importing: false}, module}}) + dispatch({ type: "UPDATE_MODULE", payload: { updates: { importing: false }, module } }) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ examFile, solutionsFile, dispatch, - module + currentModule ]); return ( diff --git a/src/components/ExamEditor/index.tsx b/src/components/ExamEditor/index.tsx index 84151bd7..aa2e0ca8 100644 --- a/src/components/ExamEditor/index.tsx +++ b/src/components/ExamEditor/index.tsx @@ -7,7 +7,7 @@ import { capitalize } from "lodash"; import { Difficulty } from "@/interfaces/exam"; import { useCallback, useEffect, useState } from "react"; import { toast } from "react-toastify"; -import { ModuleState } from "@/stores/examEditor/types"; +import { ModuleState, SectionState } from "@/stores/examEditor/types"; import { Module } from "@/interfaces"; import useExamEditorStore from "@/stores/examEditor"; import WritingSettings from "./SettingsEditor/writing"; @@ -38,8 +38,8 @@ const ExamEditor: React.FC = () => { useEffect(() => { const currentSections = sections; const currentLabels = sectionLabels; - let updatedSections; - let updatedLabels; + let updatedSections: SectionState[]; + let updatedLabels: any; if (numberOfParts > currentSections.length) { const newSections = [...currentSections]; @@ -76,6 +76,7 @@ const ExamEditor: React.FC = () => { } } }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [numberOfParts]); diff --git a/src/components/Solutions/TrueFalse.tsx b/src/components/Solutions/TrueFalse.tsx index 0c0e21d1..31905dc6 100644 --- a/src/components/Solutions/TrueFalse.tsx +++ b/src/components/Solutions/TrueFalse.tsx @@ -88,7 +88,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu {userSolutions && questions.map((question, index) => { const userSolution = userSolutions.find((x) => x.id === question.id.toString()); - const solution = question.solution.toString().toLowerCase() as Solution; + const solution = question?.solution?.toString().toLowerCase() as Solution; return (
diff --git a/src/components/UserDisplayList.tsx b/src/components/UserDisplayList.tsx index 8f1e803f..bf0b58f5 100644 --- a/src/components/UserDisplayList.tsx +++ b/src/components/UserDisplayList.tsx @@ -8,6 +8,7 @@ interface Props { const UserDisplay = (displayUser: User) => (
+ {/* eslint-disable-next-line @next/next/no-img-element */} {displayUser.name}
{displayUser.name} diff --git a/src/pages/(admin)/BatchCreateUser/UserTable.tsx b/src/components/UserTable.tsx similarity index 99% rename from src/pages/(admin)/BatchCreateUser/UserTable.tsx rename to src/components/UserTable.tsx index ec93e8f6..79d597df 100644 --- a/src/pages/(admin)/BatchCreateUser/UserTable.tsx +++ b/src/components/UserTable.tsx @@ -9,7 +9,7 @@ import { getFilteredRowModel, FilterFn, } from '@tanstack/react-table'; -import { UserImport } from "./IUserImport"; +import { UserImport } from "../interfaces/IUserImport"; const globalFilterFn: FilterFn = (row, columnId, filterValue: string) => { const value = row.getValue(columnId); diff --git a/src/exams/Listening.tsx b/src/exams/Listening.tsx index 28f17cb7..3a006586 100644 --- a/src/exams/Listening.tsx +++ b/src/exams/Listening.tsx @@ -198,23 +198,27 @@ export default function Listening({ exam, showSolutions = false, preview = false const renderAudioPlayer = () => (
- {exam.parts[partIndex].audio ? ( + {exam?.parts[partIndex]?.audio?.source ? ( <>

Please listen to the following audio attentively.

- {exam.parts[partIndex].audio.repeatableTimes > 0 - ? `You will only be allowed to listen to the audio ${exam.parts[partIndex].audio.repeatableTimes - timesListened} time(s).` - : "You may listen to the audio as many times as you would like."} + {(() => { + const audioRepeatTimes = exam?.parts[partIndex]?.audio?.repeatableTimes; + return audioRepeatTimes && audioRepeatTimes > 0 + ? `You will only be allowed to listen to the audio ${audioRepeatTimes - timesListened} time(s).` + : "You may listen to the audio as many times as you would like."; + })()}
setTimesListened((prev) => prev + 1)} - disabled={timesListened === exam.parts[partIndex].audio.repeatableTimes} + disabled={exam?.parts[partIndex]?.audio?.repeatableTimes != null && + timesListened === exam.parts[partIndex]?.audio?.repeatableTimes} disablePause />
diff --git a/src/exams/Writing.tsx b/src/exams/Writing.tsx index 352cafcd..acfea4fb 100644 --- a/src/exams/Writing.tsx +++ b/src/exams/Writing.tsx @@ -23,6 +23,7 @@ export default function Writing({ exam, showSolutions = false, preview = false, const { userSolutions, exerciseIndex, + hasExamEnded, setBgColor, setUserSolutions, setHasExamEnded, diff --git a/src/pages/(admin)/BatchCreateUser/IUserImport.ts b/src/interfaces/IUserImport.ts similarity index 100% rename from src/pages/(admin)/BatchCreateUser/IUserImport.ts rename to src/interfaces/IUserImport.ts diff --git a/src/pages/(admin)/BatchCreateUser/index.tsx b/src/pages/(admin)/BatchCreateUser/index.tsx index 9eae0799..b7c6d264 100644 --- a/src/pages/(admin)/BatchCreateUser/index.tsx +++ b/src/pages/(admin)/BatchCreateUser/index.tsx @@ -15,8 +15,8 @@ import ReactDatePicker from "react-datepicker"; import clsx from "clsx"; import countryCodes from "country-codes-list"; import { User, Type as UserType } from "@/interfaces/user"; -import { Type, UserImport } from "./IUserImport"; -import UserTable from "./UserTable"; +import { Type, UserImport } from "../../../interfaces/IUserImport"; +import UserTable from "../../../components/UserTable"; import { EntityWithRoles } from "@/interfaces/entity"; import Select from "@/components/Low/Select"; diff --git a/src/pages/(admin)/Lists/ExamList.tsx b/src/pages/(admin)/Lists/ExamList.tsx index 3e3ffe6f..a7891939 100644 --- a/src/pages/(admin)/Lists/ExamList.tsx +++ b/src/pages/(admin)/Lists/ExamList.tsx @@ -20,6 +20,7 @@ import Modal from "@/components/Modal"; import {checkAccess} from "@/utils/permissions"; import useGroups from "@/hooks/useGroups"; import Button from "@/components/Low/Button"; +import { EntityWithRoles } from "@/interfaces/entity"; const searchFields = [["module"], ["id"], ["createdBy"]]; @@ -56,7 +57,7 @@ const ExamOwnerSelector = ({options, exam, onSave}: {options: User[]; exam: Exam ); }; -export default function ExamList({user}: {user: User}) { +export default function ExamList({user, entities}: {user: User; entities: EntityWithRoles[];}) { const [selectedExam, setSelectedExam] = useState(); const {exams, reload} = useExams(); diff --git a/src/pages/api/exam/generate/[...module].ts b/src/pages/api/exam/generate/[...module].ts index 8bf95b4e..71ca7a68 100644 --- a/src/pages/api/exam/generate/[...module].ts +++ b/src/pages/api/exam/generate/[...module].ts @@ -21,7 +21,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { queryParams.delete('module'); - const result = await axios.get(`${process.env.BACKEND_URL}/${endpoint}${queryParams.size > 0 ? `?${queryParams.toString()}` : ""}`, { + const result = await axios.get(`${process.env.BACKEND_URL}/${endpoint}${Array.from(queryParams.entries()).length > 0 ? `?${queryParams.toString()}` : ""}`, { headers: { Authorization: `Bearer ${process.env.BACKEND_JWT}` }, }); res.status(200).json(result.data); From dc8f00c3180a43a9478ad7203093ae7cbc94c0c1 Mon Sep 17 00:00:00 2001 From: Carlos-Mesquita Date: Wed, 6 Nov 2024 20:22:21 +0000 Subject: [PATCH 3/3] When merging forgot to place entities as optional and = [] --- .../{BatchCreateUser/index.tsx => Lists/BatchCreateUser.tsx} | 4 ++-- src/pages/settings.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/pages/(admin)/{BatchCreateUser/index.tsx => Lists/BatchCreateUser.tsx} (98%) diff --git a/src/pages/(admin)/BatchCreateUser/index.tsx b/src/pages/(admin)/Lists/BatchCreateUser.tsx similarity index 98% rename from src/pages/(admin)/BatchCreateUser/index.tsx rename to src/pages/(admin)/Lists/BatchCreateUser.tsx index b7c6d264..f753c105 100644 --- a/src/pages/(admin)/BatchCreateUser/index.tsx +++ b/src/pages/(admin)/Lists/BatchCreateUser.tsx @@ -64,11 +64,11 @@ const USER_TYPE_PERMISSIONS: { interface Props { user: User; permissions: PermissionType[]; - entities: EntityWithRoles[] + entities?: EntityWithRoles[] onFinish: () => void; } -export default function BatchCreateUser({ user, entities, permissions, onFinish }: Props) { +export default function BatchCreateUser({ user, entities = [], permissions, onFinish }: Props) { const [infos, setInfos] = useState([]); const [duplicatedUsers, setDuplicatedUsers] = useState([]); diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 141c69cf..fa7399b9 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -12,7 +12,7 @@ import Lists from "./(admin)/Lists"; import BatchCodeGenerator from "./(admin)/BatchCodeGenerator"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; import ExamGenerator from "./(admin)/ExamGenerator"; -import BatchCreateUser from "./(admin)/BatchCreateUser"; +import BatchCreateUser from "./(admin)/Lists/BatchCreateUser"; import { checkAccess, getTypesOfUser } from "@/utils/permissions"; import usePermissions from "@/hooks/usePermissions"; import { useState } from "react";