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