diff --git a/frontend/src/editor/EditorPage/index.tsx b/frontend/src/editor/EditorPage/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b7454efbacfe480e3e505220cae2b7341b3cc808 --- /dev/null +++ b/frontend/src/editor/EditorPage/index.tsx @@ -0,0 +1,77 @@ +import type { Path } from '@/router' +import { useNavigate } from '@/router' +import { Button } from '@blueprintjs/core' +import { css } from '@emotion/css' +import type { FC, ReactNode } from 'react' +import { memo } from 'react' + +const editorPage = css` + display: grid; + grid-template-rows: auto auto 1fr auto; + height: 100vh; + padding-right: 0.5rem; +` + +interface EditorPageProps { + title: string + description: string + children: ReactNode + prevPath: Path + nextPath: Path + nextDisabled?: boolean + nextVisible?: boolean +} + +const EditorPage: FC<EditorPageProps> = ({ + title, + description, + children, + prevPath, + nextPath, + nextDisabled, + nextVisible, +}) => { + const nav = useNavigate() + + return ( + <div className={editorPage}> + <h1>{title}</h1> + <p style={{ marginBottom: '1rem' }}>{description}</p> + <div style={{ overflowY: 'auto' }}>{children}</div> + <div + style={{ + display: 'flex', + justifyContent: 'center', + padding: '0.5rem 0', + }} + > + <Button + type='button' + onClick={() => nav(prevPath)} + text='Back' + icon='arrow-left' + style={{ + marginRight: '0.5rem', + }} + />{' '} + {nextVisible && ( + <Button + type='button' + onClick={() => nav(nextPath)} + text='Next' + intent='primary' + rightIcon='arrow-right' + disabled={nextDisabled} + /> + )} + </div> + </div> + ) +} + +EditorPage.defaultProps = { + nextDisabled: false, + nextVisible: true, +} + +export default memo(EditorPage) diff --git a/frontend/src/editor/ExerciseInformationForm/index.tsx b/frontend/src/editor/ExerciseInformationForm/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..782e721f1a91a7a065f60f0413b63e87d3686fe7 --- /dev/null +++ b/frontend/src/editor/ExerciseInformationForm/index.tsx @@ -0,0 +1,63 @@ +import useEditorStorage from '@/editor/useEditorStorage' +import { InputGroup, Label, TextArea } from '@blueprintjs/core' +import { memo, useEffect, useState, type FC } from 'react' + +interface ExerciseInformationFormProps { + onInputChange: (name: string, description: string, trainee: string) => void +} + +const ExerciseInformationPage: FC<ExerciseInformationFormProps> = ({ + onInputChange, +}) => { + const [config, setConfig] = useEditorStorage() + const [name, setName] = useState(config?.name || '') + const [description, setDescription] = useState(config?.description || '') + const [trainee, setTrainee] = useState(config?.trainee || '') + + useEffect(() => { + setConfig({ + ...config, + name, + description, + trainee, + }) + onInputChange(name, description, trainee) + }, [name, description, trainee]) + + return ( + <div> + <Label> + Name + <InputGroup + placeholder='Input text' + value={name} + onChange={e => setName(e.target.value)} + /> + </Label> + <Label> + Description + <TextArea + value={description} + style={{ + width: '100%', + height: '5rem', + resize: 'none', + overflowY: 'auto', + }} + placeholder='Input text' + onChange={e => setDescription(e.target.value)} + /> + </Label> + <Label> + Who is your trainee? + <InputGroup + placeholder='Input text' + value={trainee} + onChange={e => setTrainee(e.target.value)} + /> + </Label> + </div> + ) +} + +export default memo(ExerciseInformationPage) diff --git a/frontend/src/editor/IntroductionForm/index.tsx b/frontend/src/editor/IntroductionForm/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f81966e668646a26fd7871b684cf04ddeff15fb5 --- /dev/null +++ b/frontend/src/editor/IntroductionForm/index.tsx @@ -0,0 +1,47 @@ +import useEditorStorage from '@/editor/useEditorStorage' +import { CheckboxCard, Classes } from '@blueprintjs/core' +import { memo, useEffect, useState, type FC } from 'react' +import { INTRO_CONDITIONS } from '../utils' + +interface IntroductionFormProps { + onInputChange: (conditions: boolean[]) => void +} + +const IntroductionForm: FC<IntroductionFormProps> = ({ onInputChange }) => { + const [config, setConfig] = useEditorStorage() + const [conditionChecked, setConditionChecked] = useState( + config?.checked || INTRO_CONDITIONS.map(() => false) + ) + + useEffect(() => { + setConfig({ + ...config, + checked: 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> + ) +} + +export default memo(IntroductionForm) diff --git a/frontend/src/editor/Navbar/index.tsx b/frontend/src/editor/Navbar/index.tsx index bc92caaabcee8bafc7bc477342db3655fe8b9a28..a73bb3aa4b128f63c04453a8ae29fe51807d7768 100644 --- a/frontend/src/editor/Navbar/index.tsx +++ b/frontend/src/editor/Navbar/index.tsx @@ -2,7 +2,11 @@ import NavbarButton from './NavbarButton' const Navbar = () => ( <div style={{ display: 'flex', flexDirection: 'column' }}> - <NavbarButton path='/editor/create/participants' name='Participants' /> + <NavbarButton path='/editor/create/introduction' name='Introduction' /> + <NavbarButton + path='/editor/create/exercise-information' + name='Exercise information' + /> <NavbarButton path='/editor/create/learning-objectives' name='Learning objectives' diff --git a/frontend/src/editor/useEditorStorage.tsx b/frontend/src/editor/useEditorStorage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f82fe81087a1f8f1b70ab7d23772564c553d07d4 --- /dev/null +++ b/frontend/src/editor/useEditorStorage.tsx @@ -0,0 +1,17 @@ +import { useLocalStorageState } from 'ahooks' + +export interface EditorConfig { + checked?: boolean[] + name?: string + description?: string + trainee?: string +} + +const useEditorStorage = () => + useLocalStorageState<EditorConfig>('editor-config', { + defaultValue: {}, + serializer: value => JSON.stringify(value), + deserializer: localValue => JSON.parse(localValue) as EditorConfig, + }) + +export default useEditorStorage diff --git a/frontend/src/editor/utils.tsx b/frontend/src/editor/utils.tsx index 1d15e704076f802c04da9bf6e1fbbe4f1eb93b81..d04c9fceea99c2cacd0758429512450f93fbfe55 100644 --- a/frontend/src/editor/utils.tsx +++ b/frontend/src/editor/utils.tsx @@ -1,6 +1,12 @@ import type { InjectInfo, LearningActivityInfo } from './indexeddb/types' import { InjectType, LearningActivityType } from './indexeddb/types' +export const INTRO_CONDITIONS = [ + { name: 'Purpose', description: 'What do you want to achieve' }, + { name: 'Learning objectives', description: 'Description' }, + { name: 'Injects', 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 acc88f18d2e94c1037dee1624eec08d6e6c43406..13d01a84dcd711ac5e283e84b7284c1feb5c31f6 100644 --- a/frontend/src/pages/editor/create/activity-specification/[activityId]/index.tsx +++ b/frontend/src/pages/editor/create/activity-specification/[activityId]/index.tsx @@ -1,48 +1,22 @@ +import EditorPage from '@/editor/EditorPage' import LearningActivitySpecification from '@/editor/LearningActivitySpecification' -import { useNavigate, useParams } from '@/router' -import { Button } from '@blueprintjs/core' -import { css } from '@emotion/css' +import { useParams } from '@/router' import { memo } from 'react' -const specificationPage = css` - display: grid; - grid-template-rows: auto auto 1fr auto; - height: 100vh; -` - const ActivityDefinitionPage = () => { const { activityId } = useParams( '/editor/create/activity-specification/:activityId' ) - const nav = useNavigate() - return ( - <div className={specificationPage}> - <h1>Define activity</h1> - <p style={{ marginBottom: '1rem' }}>Description.</p> - <div style={{ overflowY: 'auto' }}> - <LearningActivitySpecification - learningActivityId={Number(activityId)} - /> - </div> - <div - style={{ - display: 'flex', - justifyContent: 'center', - padding: '0.5rem 0', - }} - > - <Button - type='button' - onClick={() => nav('/editor/create/activity-specification')} - text='Back' - icon='arrow-left' - style={{ - marginRight: '0.5rem', - }} - /> - </div> - </div> + <EditorPage + title='Define activity' + description='Description.' + prevPath='/editor/create/activity-specification' + nextPath='/editor' + nextVisible={false} + > + <LearningActivitySpecification learningActivityId={Number(activityId)} /> + </EditorPage> ) } diff --git a/frontend/src/pages/editor/create/activity-specification/index.tsx b/frontend/src/pages/editor/create/activity-specification/index.tsx index a38ec15dcfd5ad64cb2ba06b8c1cb6a602a1babf..dd3e8d6b2e7baf7fa835fb8099623af3e5914d0c 100644 --- a/frontend/src/pages/editor/create/activity-specification/index.tsx +++ b/frontend/src/pages/editor/create/activity-specification/index.tsx @@ -1,44 +1,17 @@ +import EditorPage from '@/editor/EditorPage' import LearningActivitiesOverview from '@/editor/LearningActivitiesOverview' -import { Button } from '@blueprintjs/core' -import { css } from '@emotion/css' import { memo } from 'react' -const specificationPage = css` - display: grid; - grid-template-rows: auto auto 1fr auto; - height: 100vh; -` - const ActivitiesSpecificationPage = () => ( - <div className={specificationPage}> - <h1>Define activities</h1> - <p style={{ marginBottom: '1rem' }}>Description.</p> - <div style={{ overflowY: 'auto' }}> - <LearningActivitiesOverview /> - </div> - <div - style={{ - display: 'flex', - justifyContent: 'center', - padding: '0.5rem 0', - }} - > - <Button - type='button' - text='Back' - icon='arrow-left' - style={{ - marginRight: '0.5rem', - }} - /> - <Button - type='button' - text='Continue' - intent='primary' - rightIcon='arrow-right' - /> - </div> - </div> + <EditorPage + title='Define activities' + description='Description.' + prevPath='/editor/create/injects' + nextPath='/editor' + nextDisabled + > + <LearningActivitiesOverview /> + </EditorPage> ) export default memo(ActivitiesSpecificationPage) diff --git a/frontend/src/pages/editor/create/exercise-information.tsx b/frontend/src/pages/editor/create/exercise-information.tsx new file mode 100644 index 0000000000000000000000000000000000000000..79fed9eb2d851934de441b02c84295bb92b16499 --- /dev/null +++ b/frontend/src/pages/editor/create/exercise-information.tsx @@ -0,0 +1,25 @@ +import EditorPage from '@/editor/EditorPage' +import ExerciseInformationForm from '@/editor/ExerciseInformationForm' +import { memo, useState } from 'react' + +const ExerciseInformationPage = () => { + const [nextDisabled, setNextDisabled] = useState(false) + + return ( + <EditorPage + title='Exercise essentials' + description='Description.' + prevPath='/editor/create/introduction' + nextPath='/editor/create/learning-objectives' + nextDisabled={nextDisabled} + > + <ExerciseInformationForm + onInputChange={(name: string, description: string, trainee: string) => + setNextDisabled(!name || !description || !trainee) + } + /> + </EditorPage> + ) +} + +export default memo(ExerciseInformationPage) diff --git a/frontend/src/pages/editor/create/injects.tsx b/frontend/src/pages/editor/create/injects.tsx index f122c18854d2354cb206fd4f0a107669a4ea4324..4e1a98309d2946bacc63ef2ed68f7862f9f4936b 100644 --- a/frontend/src/pages/editor/create/injects.tsx +++ b/frontend/src/pages/editor/create/injects.tsx @@ -1,60 +1,26 @@ +import EditorPage from '@/editor/EditorPage' import InjectForm from '@/editor/InjectForm' import Injects from '@/editor/Injects' -import { useNavigate } from '@/router' -import { Button } from '@blueprintjs/core' -import { css } from '@emotion/css' import { memo } from 'react' -const injectsPage = css` - display: grid; - grid-template-rows: auto auto 1fr auto; - height: 100vh; -` - -const InjectsPage = () => { - const nav = useNavigate() - - return ( - <div className={injectsPage}> - <h1>Define injects</h1> - <p style={{ marginBottom: '1rem' }}>Description.</p> - <div style={{ overflowY: 'auto' }}> - <Injects /> - <InjectForm - buttonProps={{ - minimal: true, - text: 'Add new inject', - alignText: 'left', - icon: 'plus', - style: { padding: '1rem', width: '100%' }, - }} - /> - </div> - <div - style={{ - display: 'flex', - justifyContent: 'center', - padding: '0.5rem 0', - }} - > - <Button - type='button' - onClick={() => nav('/editor/create/learning-objectives')} - text='Back' - icon='arrow-left' - style={{ - marginRight: '0.5rem', - }} - /> - <Button - type='button' - text='Continue' - intent='primary' - rightIcon='arrow-right' - /> - </div> - </div> - ) -} +const InjectsPage = () => ( + <EditorPage + title='Define injects' + description='Description.' + prevPath='/editor/create/learning-objectives' + nextPath='/editor/create/activity-specification' + > + <Injects /> + <InjectForm + buttonProps={{ + minimal: true, + text: 'Add new inject', + alignText: 'left', + icon: 'plus', + style: { padding: '1rem', width: '100%' }, + }} + /> + </EditorPage> +) export default memo(InjectsPage) diff --git a/frontend/src/pages/editor/create/introduction.tsx b/frontend/src/pages/editor/create/introduction.tsx index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1cafc18166acb4ebeb1fde3448de665c3f79260a 100644 --- a/frontend/src/pages/editor/create/introduction.tsx +++ b/frontend/src/pages/editor/create/introduction.tsx @@ -0,0 +1,25 @@ +import EditorPage from '@/editor/EditorPage' +import IntroductionForm from '@/editor/IntroductionForm' +import { memo, useState } from 'react' + +const IntroductionPage = () => { + const [nextDisabled, setNextDisabled] = useState(true) + + return ( + <EditorPage + title='Before you start' + description='make sure that you:' + prevPath='/editor' + nextPath='/editor/create/exercise-information' + nextDisabled={nextDisabled} + > + <IntroductionForm + onInputChange={(conditions: boolean[]) => + setNextDisabled(!conditions.every(Boolean)) + } + /> + </EditorPage> + ) +} + +export default memo(IntroductionPage) diff --git a/frontend/src/pages/editor/create/learning-objectives.tsx b/frontend/src/pages/editor/create/learning-objectives.tsx index 8b227f4478c6ef79ccf297748f4396d189d59fa5..897a0c1ab4381295972fa1e3d07d1910cf4d5b26 100644 --- a/frontend/src/pages/editor/create/learning-objectives.tsx +++ b/frontend/src/pages/editor/create/learning-objectives.tsx @@ -1,61 +1,26 @@ +import EditorPage from '@/editor/EditorPage' import LearningObjectiveForm from '@/editor/LearningObjectiveForm' import LearningObjectives from '@/editor/LearningObjectives' -import { useNavigate } from '@/router' -import { Button } from '@blueprintjs/core' -import { css } from '@emotion/css' import { memo } from 'react' -const objectivesPage = css` - display: grid; - grid-template-rows: auto auto 1fr auto; - height: 100vh; -` - -const LearningObjectivesPage = () => { - const nav = useNavigate() - - return ( - <div className={objectivesPage}> - <h1>Define objectives and activities</h1> - <p style={{ marginBottom: '1rem' }}>Description.</p> - <div style={{ overflowY: 'auto' }}> - <LearningObjectives /> - <LearningObjectiveForm - buttonProps={{ - minimal: true, - text: 'Add new learning objective', - alignText: 'left', - icon: 'plus', - style: { padding: '1rem', width: '100%' }, - }} - /> - </div> - <div - style={{ - display: 'flex', - justifyContent: 'center', - padding: '0.5rem 0', - }} - > - <Button - type='button' - onClick={() => nav('/editor/create/participants')} - text='Back' - icon='arrow-left' - style={{ - marginRight: '0.5rem', - }} - /> - <Button - type='button' - onClick={() => nav('/editor/create/injects')} - text='Continue' - intent='primary' - rightIcon='arrow-right' - /> - </div> - </div> - ) -} +const LearningObjectivesPage = () => ( + <EditorPage + title='Define objectives and activities' + description='Description.' + prevPath='/editor/create/exercise-information' + nextPath='/editor/create/injects' + > + <LearningObjectives /> + <LearningObjectiveForm + buttonProps={{ + minimal: true, + text: 'Add new learning objective', + alignText: 'left', + icon: 'plus', + style: { padding: '1rem', width: '100%' }, + }} + /> + </EditorPage> +) export default memo(LearningObjectivesPage) diff --git a/frontend/src/pages/editor/create/participants.tsx b/frontend/src/pages/editor/create/participants.tsx deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/frontend/src/router.ts b/frontend/src/router.ts index 52dfcb86cde2a04aa979d1fd45524942e1b86925..d7d9883340a641566ad5860570c862382bd675ad 100644 --- a/frontend/src/router.ts +++ b/frontend/src/router.ts @@ -15,10 +15,10 @@ export type Path = | `/editor` | `/editor/create/activity-specification` | `/editor/create/activity-specification/:activityId` + | `/editor/create/exercise-information` | `/editor/create/injects` | `/editor/create/introduction` | `/editor/create/learning-objectives` - | `/editor/create/participants` | `/exercise-panel` | `/graphiql` | `/instructor`