diff --git a/frontend/src/editor/InjectForm/index.tsx b/frontend/src/editor/InjectForm/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fec430db07c85e492508eb6aa161b1de02f2b15c --- /dev/null +++ b/frontend/src/editor/InjectForm/index.tsx @@ -0,0 +1,149 @@ +import type { ButtonProps } from '@blueprintjs/core' +import { + Button, + Dialog, + DialogBody, + DialogFooter, + HTMLSelect, + InputGroup, + Label, + TextArea, +} from '@blueprintjs/core' +import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' +import type { FC } from 'react' +import { memo, useCallback, useEffect, useState } from 'react' +import { addInjectInfo, updateInjectInfo } from '../indexeddb/operations' +import type { InjectInfo } from '../indexeddb/types' +import { InjectType } from '../indexeddb/types' +import { INJECT_TYPES } from '../utils' + +interface InjectFormProps { + inject?: InjectInfo + buttonProps: ButtonProps +} + +const InjectForm: FC<InjectFormProps> = ({ inject, buttonProps }) => { + const [isOpen, setIsOpen] = useState(false) + const [name, setName] = useState<string>('') + const [description, setDescription] = useState<string>('') + const [type, setType] = useState<InjectType>(InjectType.INFORMATION) + + const { notify } = useNotifyContext() + + const clearInput = useCallback(() => { + setName('') + setDescription('') + }, []) + + const handleAddButton = useCallback( + async (inject: Omit<InjectInfo, 'id'>) => { + try { + await addInjectInfo(inject) + clearInput() + setIsOpen(false) + } catch (err) { + notify(`Failed to add inject '${inject.name}': ${err}`, { + intent: 'danger', + }) + } + }, + [notify] + ) + + const handleUpdateButton = useCallback( + async (inject: InjectInfo) => { + try { + await updateInjectInfo(inject) + setIsOpen(false) + } catch (err) { + notify(`Failed to update inject '${inject.name}': ${err}`, { + intent: 'danger', + }) + } + }, + [notify] + ) + + useEffect(() => { + setName(inject?.name || '') + setDescription(inject?.description || '') + setType(inject?.type || InjectType.INFORMATION) + }, [inject]) + + return ( + <> + <Button {...buttonProps} onClick={() => setIsOpen(true)} /> + <Dialog + isOpen={isOpen} + onClose={() => setIsOpen(false)} + icon={inject ? 'edit' : 'plus'} + title={inject ? 'Edit inject' : 'New inject'} + > + <DialogBody> + <Label style={{ width: '100%' }}> + Title + <InputGroup + placeholder='Input text' + value={name} + onChange={e => setName(e.target.value)} + /> + </Label> + <Label style={{ width: '100%' }}> + What is this inject about? (optional) + <TextArea + value={description} + style={{ + width: '100%', + height: '5rem', + resize: 'none', + overflowY: 'auto', + }} + placeholder='Input text' + onChange={e => setDescription(e.target.value)} + /> + </Label> + <Label style={{ width: '100%' }}> + Channel + <HTMLSelect + options={INJECT_TYPES} + value={type} + onChange={event => + setType(event.currentTarget.value as InjectType) + } + /> + </Label> + </DialogBody> + <DialogFooter + actions={ + inject ? ( + <Button + disabled={!name} + onClick={() => + handleUpdateButton({ + id: inject.id, + name, + description, + type, + }) + } + intent='primary' + icon='edit' + text='Save changes' + /> + ) : ( + <Button + disabled={!name} + onClick={() => handleAddButton({ name, description, type })} + intent='primary' + icon='plus' + text='Add' + /> + ) + } + /> + </Dialog> + </> + ) +} + +export default memo(InjectForm) diff --git a/frontend/src/editor/Injects/Inject.tsx b/frontend/src/editor/Injects/Inject.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9b3e50a586a361e81bab861897dc11cd564fe173 --- /dev/null +++ b/frontend/src/editor/Injects/Inject.tsx @@ -0,0 +1,58 @@ +import { Button, ButtonGroup, Card, Icon } from '@blueprintjs/core' +import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext' +import type { FC } from 'react' +import { memo, useCallback } from 'react' +import InjectForm from '../InjectForm' +import { deleteInjectInfo } from '../indexeddb/operations' +import type { InjectInfo } from '../indexeddb/types' +import { InjectType } from '../indexeddb/types' + +interface InjectItemProps { + inject: InjectInfo +} + +const InjectItem: FC<InjectItemProps> = ({ inject }) => { + const { notify } = useNotifyContext() + + const handleDeleteButton = useCallback( + async (inject: InjectInfo) => { + try { + await deleteInjectInfo(inject.id) + } catch (err) { + notify(`Failed to delete inject '${inject.name}': ${err}`, { + intent: 'danger', + }) + } + }, + [notify] + ) + + return ( + <Card style={{ display: 'flex', justifyContent: 'space-between' }}> + <span style={{ height: '100%', flexGrow: 1 }}> + <Icon + icon={inject.type === InjectType.EMAIL ? 'envelope' : 'clipboard'} + style={{ marginRight: '1rem' }} + /> + {inject.name} + </span> + <ButtonGroup> + <InjectForm + inject={inject} + buttonProps={{ + minimal: true, + icon: 'edit', + style: { marginRight: '1rem' }, + }} + /> + <Button + minimal + icon='cross' + onClick={() => handleDeleteButton(inject)} + /> + </ButtonGroup> + </Card> + ) +} + +export default memo(InjectItem) diff --git a/frontend/src/editor/Injects/index.tsx b/frontend/src/editor/Injects/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dac5dd0fa84f4f0217619d78b41f2c0cc010b43f --- /dev/null +++ b/frontend/src/editor/Injects/index.tsx @@ -0,0 +1,20 @@ +import Inject from '@/editor/Injects/Inject' +import { CardList } from '@blueprintjs/core' +import { useLiveQuery } from 'dexie-react-hooks' +import { memo } from 'react' +import { db } from '../indexeddb/db' +import type { InjectInfo } from '../indexeddb/types' + +const Injects = () => { + const injects = useLiveQuery(() => db.injectInfos.toArray(), [], []) + + return ( + <CardList> + {injects?.map((inject: InjectInfo) => ( + <Inject key={inject.id} inject={inject} /> + ))} + </CardList> + ) +} + +export default memo(Injects) diff --git a/frontend/src/editor/indexeddb/db.tsx b/frontend/src/editor/indexeddb/db.tsx index 6a0c3973f6e38618ef210fcc3d465075db0fc414..be0cb235bb08e746c048c0848ddfb8e8a9003cb5 100644 --- a/frontend/src/editor/indexeddb/db.tsx +++ b/frontend/src/editor/indexeddb/db.tsx @@ -1,15 +1,17 @@ import Dexie, { type EntityTable } from 'dexie' -import type { LearningObjectiveInfo } from './types' +import type { InjectInfo, LearningObjectiveInfo } from './types' const dbName = 'EditorDatabase' const dbVersion = 1 const db = new Dexie(dbName) as Dexie & { learningObjectives: EntityTable<LearningObjectiveInfo, 'id'> + injectInfos: EntityTable<InjectInfo, 'id'> } db.version(dbVersion).stores({ learningObjectives: '++id, &name', + injectInfos: '++id, &name, description, type', }) export { db } diff --git a/frontend/src/editor/indexeddb/operations.tsx b/frontend/src/editor/indexeddb/operations.tsx index 8ee3e1e1991261223b6b8f7875bdd8510ac4fffc..4ac05600c9b7150da8f2adbac74885bfea3747d5 100644 --- a/frontend/src/editor/indexeddb/operations.tsx +++ b/frontend/src/editor/indexeddb/operations.tsx @@ -1,5 +1,5 @@ import { db } from './db' -import type { LearningObjectiveInfo } from './types' +import type { InjectInfo, LearningObjectiveInfo } from './types' // learning objectives operations export const addLearningObjective = async ( @@ -11,3 +11,15 @@ export const addLearningObjective = async ( export const deleteLearningObjective = async (id: string) => await db.learningObjectives.delete(id) + +// inject info operations +export const addInjectInfo = async (injectInfo: Omit<InjectInfo, 'id'>) => + await db.transaction('rw', db.injectInfos, async () => { + await db.injectInfos.add(injectInfo) + }) + +export const updateInjectInfo = async (injectInfo: InjectInfo) => + await db.injectInfos.put(injectInfo) + +export const deleteInjectInfo = async (id: number) => + await db.injectInfos.delete(id) diff --git a/frontend/src/editor/indexeddb/types.tsx b/frontend/src/editor/indexeddb/types.tsx index abc5b0479793e3a87ea9c5ba4cdf5f5cba8a33b8..e5a58b70f2138c79901601197ffd1f712b0145f9 100644 --- a/frontend/src/editor/indexeddb/types.tsx +++ b/frontend/src/editor/indexeddb/types.tsx @@ -1,3 +1,15 @@ import type { LearningObjective } from '@inject/graphql/fragments/LearningObjective.generated' export type LearningObjectiveInfo = Pick<LearningObjective, 'id' | 'name'> + +export enum InjectType { + INFORMATION = 'Information', + EMAIL = 'Email', +} + +export type InjectInfo = { + id: number + name: string + description: string + type: InjectType +} diff --git a/frontend/src/editor/utils.tsx b/frontend/src/editor/utils.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8c9629c81f422db00472059b6764647218b1441c --- /dev/null +++ b/frontend/src/editor/utils.tsx @@ -0,0 +1,3 @@ +import { InjectType } from './indexeddb/types' + +export const INJECT_TYPES = Object.values(InjectType) diff --git a/frontend/src/pages/editor/create/injects.tsx b/frontend/src/pages/editor/create/injects.tsx index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f122c18854d2354cb206fd4f0a107669a4ea4324 100644 --- a/frontend/src/pages/editor/create/injects.tsx +++ b/frontend/src/pages/editor/create/injects.tsx @@ -0,0 +1,60 @@ +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> + ) +} + +export default memo(InjectsPage)