diff --git a/frontend/src/editor/Checklist/index.tsx b/frontend/src/editor/Checklist/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9763ae5294b004f697a55b9b5e2ede85cc128fc8 --- /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 0000000000000000000000000000000000000000..69f22eee77336b399f1204390f8c8d2efdf14203 --- /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 b7454efbacfe480e3e505220cae2b7341b3cc808..c56fe028d7be9f58d1b8adce7b4c39e6e684f416 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 782e721f1a91a7a065f60f0413b63e87d3686fe7..37b6c61e13d979fd4be1d653095b7c16d6217653 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 0000000000000000000000000000000000000000..21f12d40e1aaefd03dcbdb5317b0ef02a39afd8f --- /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 0000000000000000000000000000000000000000..d62f9fe7b3797060415682990e0e445571397add --- /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 0000000000000000000000000000000000000000..4619704a8d8f0a53268c4d536b309a7487150f1e --- /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 0000000000000000000000000000000000000000..05dca2141d341eefa91cb5038b191cff0d61e77e --- /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 0000000000000000000000000000000000000000..dd6e451643885e48c349b12293d30d263ff8de82 --- /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 f81966e668646a26fd7871b684cf04ddeff15fb5..604512dd93fee0835edf3ffc4707706bfcd74702 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 a73bb3aa4b128f63c04453a8ae29fe51807d7768..cc3bd3c14070de34ed1aefb44afc5c3eaee2af8c 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 f82fe81087a1f8f1b70ab7d23772564c553d07d4..33107c49b59e4d9b3fb92c82979183932718721b 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 d04c9fceea99c2cacd0758429512450f93fbfe55..9f8efc0a113f998a8c167376eaeb34f2de3356ef 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 13d01a84dcd711ac5e283e84b7284c1feb5c31f6..c8bc7b0a09a9d1f6db2252401db31d8a1e211370 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 0000000000000000000000000000000000000000..adf9d4c4edf1d04124fd11e5adadc7e6066fc30f --- /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 0000000000000000000000000000000000000000..5140d3c6f49c8c40b772a0457cfecbbd95a8dcb3 --- /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 1e69306f9fab98352bedd69e3c477884a31f6bf7..38049c90acd4d910c69fc4b2ae6d73d91eeb1c1c 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`