From 39f308e9bf622788f3db3d14802ed62f31cc81ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Katar=C3=ADna=20Platkov=C3=A1?= <xplatkov@fi.muni.cz> Date: Mon, 16 Sep 2024 14:55:29 +0200 Subject: [PATCH] Editor - final pages --- frontend/src/editor/Checklist/index.tsx | 44 +++++++++++++ frontend/src/editor/ConclusionForm/index.tsx | 30 +++++++++ frontend/src/editor/EditorPage/index.tsx | 4 +- .../editor/ExerciseInformationForm/index.tsx | 4 +- .../FinalInformationForm/EmailChannelForm.tsx | 63 +++++++++++++++++++ .../ExerciseDurationForm.tsx | 48 ++++++++++++++ .../FinalInformationForm/InfoChannelForm.tsx | 34 ++++++++++ .../FinalInformationForm/ToolChannelForm.tsx | 34 ++++++++++ .../src/editor/FinalInformationForm/index.tsx | 49 +++++++++++++++ .../src/editor/IntroductionForm/index.tsx | 32 +++------- frontend/src/editor/Navbar/index.tsx | 5 ++ frontend/src/editor/useEditorStorage.tsx | 10 ++- frontend/src/editor/utils.tsx | 6 ++ .../[activityId]/index.tsx | 1 - .../src/pages/editor/create/conclusion.tsx | 16 +++++ .../pages/editor/create/final-information.tsx | 25 ++++++++ frontend/src/router.ts | 2 + 17 files changed, 379 insertions(+), 28 deletions(-) create mode 100644 frontend/src/editor/Checklist/index.tsx create mode 100644 frontend/src/editor/ConclusionForm/index.tsx create mode 100644 frontend/src/editor/FinalInformationForm/EmailChannelForm.tsx create mode 100644 frontend/src/editor/FinalInformationForm/ExerciseDurationForm.tsx create mode 100644 frontend/src/editor/FinalInformationForm/InfoChannelForm.tsx create mode 100644 frontend/src/editor/FinalInformationForm/ToolChannelForm.tsx create mode 100644 frontend/src/editor/FinalInformationForm/index.tsx create mode 100644 frontend/src/pages/editor/create/conclusion.tsx create mode 100644 frontend/src/pages/editor/create/final-information.tsx diff --git a/frontend/src/editor/Checklist/index.tsx b/frontend/src/editor/Checklist/index.tsx new file mode 100644 index 000000000..9763ae529 --- /dev/null +++ b/frontend/src/editor/Checklist/index.tsx @@ -0,0 +1,44 @@ +import { CheckboxCard, Classes } from '@blueprintjs/core' +import { memo, useEffect, useState, type FC } from 'react' + +interface ChecklistProps { + conditions: { name: string; description: string }[] + initChecked: boolean[] + onInputChange: (conditions: boolean[]) => void +} + +const Checklist: FC<ChecklistProps> = ({ + conditions, + initChecked, + onInputChange, +}) => { + const [conditionChecked, setConditionChecked] = useState(initChecked) + + useEffect(() => { + onInputChange(conditionChecked) + }, [conditionChecked]) + + return ( + <div> + {conditions.map((condition, i) => ( + <CheckboxCard + key={i} + checked={conditionChecked[i]} + showAsSelectedWhenChecked={false} + onChange={() => + setConditionChecked(prev => [ + ...prev.slice(0, i), + !prev[i], + ...prev.slice(i + 1), + ]) + } + > + {condition.name} -{' '} + <span className={Classes.TEXT_MUTED}>{condition.description}</span> + </CheckboxCard> + ))} + </div> + ) +} + +export default memo(Checklist) diff --git a/frontend/src/editor/ConclusionForm/index.tsx b/frontend/src/editor/ConclusionForm/index.tsx new file mode 100644 index 000000000..69f22eee7 --- /dev/null +++ b/frontend/src/editor/ConclusionForm/index.tsx @@ -0,0 +1,30 @@ +import Checklist from '@/editor/Checklist' +import useEditorStorage from '@/editor/useEditorStorage' +import { memo, useEffect, useState } from 'react' +import { CONCLUSION_CONDITIONS } from '../utils' + +const ConclusionForm = () => { + const [config, setConfig] = useEditorStorage() + const [conditionChecked, setConditionChecked] = useState( + config?.conclusionChecked || CONCLUSION_CONDITIONS.map(() => false) + ) + + useEffect(() => { + setConfig({ + ...config, + conclusionChecked: conditionChecked, + }) + }, [conditionChecked]) + + return ( + <Checklist + conditions={CONCLUSION_CONDITIONS} + initChecked={conditionChecked} + onInputChange={(conditionChecked: boolean[]) => + setConditionChecked(conditionChecked) + } + /> + ) +} + +export default memo(ConclusionForm) diff --git a/frontend/src/editor/EditorPage/index.tsx b/frontend/src/editor/EditorPage/index.tsx index b7454efba..c56fe028d 100644 --- a/frontend/src/editor/EditorPage/index.tsx +++ b/frontend/src/editor/EditorPage/index.tsx @@ -17,7 +17,7 @@ interface EditorPageProps { description: string children: ReactNode prevPath: Path - nextPath: Path + nextPath?: Path nextDisabled?: boolean nextVisible?: boolean } @@ -57,7 +57,7 @@ const EditorPage: FC<EditorPageProps> = ({ {nextVisible && ( <Button type='button' - onClick={() => nav(nextPath)} + onClick={() => nav(nextPath || '/')} text='Next' intent='primary' rightIcon='arrow-right' diff --git a/frontend/src/editor/ExerciseInformationForm/index.tsx b/frontend/src/editor/ExerciseInformationForm/index.tsx index 782e721f1..37b6c61e1 100644 --- a/frontend/src/editor/ExerciseInformationForm/index.tsx +++ b/frontend/src/editor/ExerciseInformationForm/index.tsx @@ -6,7 +6,7 @@ interface ExerciseInformationFormProps { onInputChange: (name: string, description: string, trainee: string) => void } -const ExerciseInformationPage: FC<ExerciseInformationFormProps> = ({ +const ExerciseInformationForm: FC<ExerciseInformationFormProps> = ({ onInputChange, }) => { const [config, setConfig] = useEditorStorage() @@ -60,4 +60,4 @@ const ExerciseInformationPage: FC<ExerciseInformationFormProps> = ({ ) } -export default memo(ExerciseInformationPage) +export default memo(ExerciseInformationForm) diff --git a/frontend/src/editor/FinalInformationForm/EmailChannelForm.tsx b/frontend/src/editor/FinalInformationForm/EmailChannelForm.tsx new file mode 100644 index 000000000..21f12d40e --- /dev/null +++ b/frontend/src/editor/FinalInformationForm/EmailChannelForm.tsx @@ -0,0 +1,63 @@ +import useEditorStorage from '@/editor/useEditorStorage' +import { + Checkbox, + InputGroup, + Label, + Section, + SectionCard, +} from '@blueprintjs/core' +import { memo, useEffect, useState } from 'react' + +const EmailChannelForm = () => { + const [config, setConfig] = useEditorStorage() + const [emailChannelName, setEmailChannelName] = useState( + config?.emailChannelName || '' + ) + const [emailBetweenTeams, setEmailBetweenTeams] = useState( + config?.emailBetweenTeams || false + ) + const [customEmailSuffix, setCustomEmailSuffix] = useState( + config?.customEmailSuffix || '' + ) + + useEffect(() => { + setConfig({ + ...config, + emailChannelName, + emailBetweenTeams, + customEmailSuffix, + }) + }, [emailChannelName, emailBetweenTeams, customEmailSuffix]) + + return ( + <Section title='Email'> + <SectionCard> + <Label> + Custom email channel name + <InputGroup + placeholder='Input text' + value={emailChannelName} + onChange={e => setEmailChannelName(e.target.value)} + /> + </Label> + <Checkbox + checked={emailBetweenTeams} + onChange={() => setEmailBetweenTeams(prev => !prev)} + label={'Enable emails between teams'} + /> + {emailBetweenTeams && ( + <Label> + Custom email suffix + <InputGroup + placeholder='Input text' + value={customEmailSuffix} + onChange={e => setCustomEmailSuffix(e.target.value)} + /> + </Label> + )} + </SectionCard> + </Section> + ) +} + +export default memo(EmailChannelForm) diff --git a/frontend/src/editor/FinalInformationForm/ExerciseDurationForm.tsx b/frontend/src/editor/FinalInformationForm/ExerciseDurationForm.tsx new file mode 100644 index 000000000..d62f9fe7b --- /dev/null +++ b/frontend/src/editor/FinalInformationForm/ExerciseDurationForm.tsx @@ -0,0 +1,48 @@ +import useEditorStorage from '@/editor/useEditorStorage' +import { Checkbox, Label, NumericInput } from '@blueprintjs/core' +import { memo, useEffect, useState, type FC } from 'react' + +interface ExerciseDurationFormProps { + onInputChange: (exerciseDuration: number) => void +} + +const ExerciseDurationForm: FC<ExerciseDurationFormProps> = ({ + onInputChange, +}) => { + const [config, setConfig] = useEditorStorage() + const [exerciseDuration, setExerciseDuration] = useState( + config?.exerciseDuration || 0 + ) + const [showExerciseTime, setShowExerciseTime] = useState( + config?.showExerciseTime || false + ) + + useEffect(() => { + setConfig({ + ...config, + exerciseDuration, + showExerciseTime, + }) + onInputChange(exerciseDuration) + }, [exerciseDuration, showExerciseTime]) + + return ( + <div> + <Label> + Exercise duration in minutes + <NumericInput + placeholder='Input number' + value={exerciseDuration} + onValueChange={(value: number) => setExerciseDuration(value)} + /> + </Label> + <Checkbox + checked={showExerciseTime} + onChange={() => setShowExerciseTime(prev => !prev)} + label={'Show exercise time to trainees?'} + /> + </div> + ) +} + +export default memo(ExerciseDurationForm) diff --git a/frontend/src/editor/FinalInformationForm/InfoChannelForm.tsx b/frontend/src/editor/FinalInformationForm/InfoChannelForm.tsx new file mode 100644 index 000000000..4619704a8 --- /dev/null +++ b/frontend/src/editor/FinalInformationForm/InfoChannelForm.tsx @@ -0,0 +1,34 @@ +import useEditorStorage from '@/editor/useEditorStorage' +import { InputGroup, Label, Section, SectionCard } from '@blueprintjs/core' +import { memo, useEffect, useState } from 'react' + +const InfoChannelForm = () => { + const [config, setConfig] = useEditorStorage() + const [infoChannelName, setInfoChannelName] = useState( + config?.infoChannelName || '' + ) + + useEffect(() => { + setConfig({ + ...config, + infoChannelName, + }) + }, [infoChannelName]) + + return ( + <Section title='Information' style={{ marginBottom: '1rem' }}> + <SectionCard> + <Label> + Custom information channel name + <InputGroup + placeholder='Input text' + value={infoChannelName} + onChange={e => setInfoChannelName(e.target.value)} + /> + </Label> + </SectionCard> + </Section> + ) +} + +export default memo(InfoChannelForm) diff --git a/frontend/src/editor/FinalInformationForm/ToolChannelForm.tsx b/frontend/src/editor/FinalInformationForm/ToolChannelForm.tsx new file mode 100644 index 000000000..05dca2141 --- /dev/null +++ b/frontend/src/editor/FinalInformationForm/ToolChannelForm.tsx @@ -0,0 +1,34 @@ +import useEditorStorage from '@/editor/useEditorStorage' +import { InputGroup, Label, Section, SectionCard } from '@blueprintjs/core' +import { memo, useEffect, useState } from 'react' + +const ToolChannelForm = () => { + const [config, setConfig] = useEditorStorage() + const [toolChannelName, setToolChannelName] = useState( + config?.toolChannelName || '' + ) + + useEffect(() => { + setConfig({ + ...config, + toolChannelName, + }) + }, [toolChannelName]) + + return ( + <Section title='Tools' style={{ marginBottom: '1rem' }}> + <SectionCard> + <Label> + Custom tool channel name + <InputGroup + placeholder='Input text' + value={toolChannelName} + onChange={e => setToolChannelName(e.target.value)} + /> + </Label> + </SectionCard> + </Section> + ) +} + +export default memo(ToolChannelForm) diff --git a/frontend/src/editor/FinalInformationForm/index.tsx b/frontend/src/editor/FinalInformationForm/index.tsx new file mode 100644 index 000000000..dd6e45164 --- /dev/null +++ b/frontend/src/editor/FinalInformationForm/index.tsx @@ -0,0 +1,49 @@ +import { Section, SectionCard } from '@blueprintjs/core' +import { useLiveQuery } from 'dexie-react-hooks' +import { memo, useEffect, useState, type FC } from 'react' +import { db } from '../indexeddb/db' +import EmailChannelForm from './EmailChannelForm' +import ExerciseDurationForm from './ExerciseDurationForm' +import InfoChannelForm from './InfoChannelForm' +import ToolChannelForm from './ToolChannelForm' + +interface FinalInformationFormProps { + onInputChange: (isZero: boolean) => void +} + +const FinalInformationForm: FC<FinalInformationFormProps> = ({ + onInputChange, +}) => { + const emailAddressesCount = useLiveQuery( + () => db.emailAddresses.count(), + [], + 0 + ) + const toolsCount = useLiveQuery(() => db.tools.count(), [], 0) + const [isDurationZero, setIsDurationZero] = useState(true) + + useEffect(() => { + onInputChange(isDurationZero) + }, [isDurationZero]) + + return ( + <div> + <Section title='Exercise details'> + <SectionCard> + <ExerciseDurationForm + onInputChange={duration => setIsDurationZero(duration === 0)} + /> + </SectionCard> + </Section> + <Section title='Channel details'> + <SectionCard> + <InfoChannelForm /> + {toolsCount > 0 && <ToolChannelForm />} + {emailAddressesCount > 0 && <EmailChannelForm />} + </SectionCard> + </Section> + </div> + ) +} + +export default memo(FinalInformationForm) diff --git a/frontend/src/editor/IntroductionForm/index.tsx b/frontend/src/editor/IntroductionForm/index.tsx index f81966e66..604512dd9 100644 --- a/frontend/src/editor/IntroductionForm/index.tsx +++ b/frontend/src/editor/IntroductionForm/index.tsx @@ -1,5 +1,5 @@ +import Checklist from '@/editor/Checklist' import useEditorStorage from '@/editor/useEditorStorage' -import { CheckboxCard, Classes } from '@blueprintjs/core' import { memo, useEffect, useState, type FC } from 'react' import { INTRO_CONDITIONS } from '../utils' @@ -10,37 +10,25 @@ interface IntroductionFormProps { const IntroductionForm: FC<IntroductionFormProps> = ({ onInputChange }) => { const [config, setConfig] = useEditorStorage() const [conditionChecked, setConditionChecked] = useState( - config?.checked || INTRO_CONDITIONS.map(() => false) + config?.introChecked || INTRO_CONDITIONS.map(() => false) ) useEffect(() => { setConfig({ ...config, - checked: conditionChecked, + introChecked: conditionChecked, }) onInputChange(conditionChecked) }, [conditionChecked]) return ( - <div> - {INTRO_CONDITIONS.map((condition, i) => ( - <CheckboxCard - key={i} - checked={conditionChecked[i]} - showAsSelectedWhenChecked={false} - onChange={() => - setConditionChecked(prev => [ - ...prev.slice(0, i), - !prev[i], - ...prev.slice(i + 1), - ]) - } - > - {condition.name} -{' '} - <span className={Classes.TEXT_MUTED}>{condition.description}</span> - </CheckboxCard> - ))} - </div> + <Checklist + conditions={INTRO_CONDITIONS} + initChecked={conditionChecked} + onInputChange={(conditionChecked: boolean[]) => + setConditionChecked(conditionChecked) + } + /> ) } diff --git a/frontend/src/editor/Navbar/index.tsx b/frontend/src/editor/Navbar/index.tsx index a73bb3aa4..cc3bd3c14 100644 --- a/frontend/src/editor/Navbar/index.tsx +++ b/frontend/src/editor/Navbar/index.tsx @@ -16,6 +16,11 @@ const Navbar = () => ( path='/editor/create/activity-specification' name='Activities' /> + <NavbarButton + path='/editor/create/final-information' + name='Final Information' + /> + <NavbarButton path='/editor/create/conclusion' name='Conclusion' /> </div> ) diff --git a/frontend/src/editor/useEditorStorage.tsx b/frontend/src/editor/useEditorStorage.tsx index f82fe8108..33107c49b 100644 --- a/frontend/src/editor/useEditorStorage.tsx +++ b/frontend/src/editor/useEditorStorage.tsx @@ -1,10 +1,18 @@ import { useLocalStorageState } from 'ahooks' export interface EditorConfig { - checked?: boolean[] + introChecked?: boolean[] + conclusionChecked?: boolean[] name?: string description?: string trainee?: string + exerciseDuration?: number + showExerciseTime?: boolean + emailBetweenTeams?: boolean + customEmailSuffix?: string + infoChannelName?: string + toolChannelName?: string + emailChannelName?: string } const useEditorStorage = () => diff --git a/frontend/src/editor/utils.tsx b/frontend/src/editor/utils.tsx index d04c9fcee..9f8efc0a1 100644 --- a/frontend/src/editor/utils.tsx +++ b/frontend/src/editor/utils.tsx @@ -7,6 +7,12 @@ export const INTRO_CONDITIONS = [ { name: 'Injects', description: 'Description' }, ] +export const CONCLUSION_CONDITIONS = [ + { name: 'Check 1', description: 'Description' }, + { name: 'Check 2', description: 'Description' }, + { name: 'Check 3', description: 'Description' }, +] + export const LEARNING_ACTIVITY_TYPES = Object.values(LearningActivityType) export const INJECT_TYPES = Object.values(InjectType) diff --git a/frontend/src/pages/editor/create/activity-specification/[activityId]/index.tsx b/frontend/src/pages/editor/create/activity-specification/[activityId]/index.tsx index 13d01a84d..c8bc7b0a0 100644 --- a/frontend/src/pages/editor/create/activity-specification/[activityId]/index.tsx +++ b/frontend/src/pages/editor/create/activity-specification/[activityId]/index.tsx @@ -12,7 +12,6 @@ const ActivityDefinitionPage = () => { title='Define activity' description='Description.' prevPath='/editor/create/activity-specification' - nextPath='/editor' nextVisible={false} > <LearningActivitySpecification learningActivityId={Number(activityId)} /> diff --git a/frontend/src/pages/editor/create/conclusion.tsx b/frontend/src/pages/editor/create/conclusion.tsx new file mode 100644 index 000000000..adf9d4c4e --- /dev/null +++ b/frontend/src/pages/editor/create/conclusion.tsx @@ -0,0 +1,16 @@ +import ConclusionForm from '@/editor/ConclusionForm' +import EditorPage from '@/editor/EditorPage' +import { memo } from 'react' + +const ConclusionPage = () => ( + <EditorPage + title='Before you finish' + description='make sure that you:' + prevPath='/editor/create/final-information' + nextVisible={false} + > + <ConclusionForm /> + </EditorPage> +) + +export default memo(ConclusionPage) diff --git a/frontend/src/pages/editor/create/final-information.tsx b/frontend/src/pages/editor/create/final-information.tsx new file mode 100644 index 000000000..5140d3c6f --- /dev/null +++ b/frontend/src/pages/editor/create/final-information.tsx @@ -0,0 +1,25 @@ +import EditorPage from '@/editor/EditorPage' +import FinalInformationForm from '@/editor/FinalInformationForm' +import { memo, useState } from 'react' + +const FinalInformationPage = () => { + const [nextDisabled, setNextDisabled] = useState(false) + + return ( + <EditorPage + title='Final information' + description='Description.' + prevPath='/editor/create/activity-specification' + nextPath='/editor/create/conclusion' + nextDisabled={nextDisabled} + > + <FinalInformationForm + onInputChange={(isDurationZero: boolean) => + setNextDisabled(isDurationZero) + } + /> + </EditorPage> + ) +} + +export default memo(FinalInformationPage) diff --git a/frontend/src/router.ts b/frontend/src/router.ts index 1e69306f9..38049c90a 100644 --- a/frontend/src/router.ts +++ b/frontend/src/router.ts @@ -15,7 +15,9 @@ export type Path = | `/editor` | `/editor/create/activity-specification` | `/editor/create/activity-specification/:activityId` + | `/editor/create/conclusion` | `/editor/create/exercise-information` + | `/editor/create/final-information` | `/editor/create/injects` | `/editor/create/introduction` | `/editor/create/learning-objectives` -- GitLab