From 9c085ccf90aa64e28365619e01bffff7089958e7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Katar=C3=ADna=20Platkov=C3=A1?= <xplatkov@fi.muni.cz>
Date: Mon, 24 Jun 2024 15:13:30 +0200
Subject: [PATCH] Editor db init

---
 frontend/package.json                         |  2 +
 .../LearningObjectives/LearningObjective.tsx  | 43 ++++++++++++++++
 .../src/editor/LearningObjectives/index.tsx   | 23 +++++++++
 .../editor/LearningObjectivesForm/index.tsx   | 39 +++++++++++++++
 frontend/src/editor/Navbar/NavbarButton.tsx   | 21 ++++++++
 frontend/src/editor/Navbar/index.tsx          | 14 ++++++
 frontend/src/editor/indexeddb/db.tsx          | 15 ++++++
 frontend/src/editor/indexeddb/operations.tsx  | 13 +++++
 frontend/src/editor/indexeddb/types.tsx       |  3 ++
 frontend/src/logic/Login/index.tsx            |  7 +--
 frontend/src/pages/(navbar)/graphiql.tsx      |  6 ++-
 frontend/src/pages/editor/_layout.tsx         | 10 ++++
 frontend/src/pages/editor/create/_layout.tsx  | 15 ++++++
 frontend/src/pages/editor/create/injects.tsx  |  0
 .../src/pages/editor/create/introduction.tsx  |  0
 .../editor/create/learning-objectives.tsx     | 14 ++++++
 .../src/pages/editor/create/participants.tsx  |  0
 frontend/src/pages/editor/index.tsx           | 47 ++++++++++++++++++
 frontend/src/router.ts                        |  5 ++
 frontend/src/views/EditorView/index.tsx       | 49 +++++++++++++++++++
 yarn.lock                                     | 20 ++++++++
 21 files changed, 339 insertions(+), 7 deletions(-)
 create mode 100644 frontend/src/editor/LearningObjectives/LearningObjective.tsx
 create mode 100644 frontend/src/editor/LearningObjectives/index.tsx
 create mode 100644 frontend/src/editor/LearningObjectivesForm/index.tsx
 create mode 100644 frontend/src/editor/Navbar/NavbarButton.tsx
 create mode 100644 frontend/src/editor/Navbar/index.tsx
 create mode 100644 frontend/src/editor/indexeddb/db.tsx
 create mode 100644 frontend/src/editor/indexeddb/operations.tsx
 create mode 100644 frontend/src/editor/indexeddb/types.tsx
 create mode 100644 frontend/src/pages/editor/_layout.tsx
 create mode 100644 frontend/src/pages/editor/create/_layout.tsx
 create mode 100644 frontend/src/pages/editor/create/injects.tsx
 create mode 100644 frontend/src/pages/editor/create/introduction.tsx
 create mode 100644 frontend/src/pages/editor/create/learning-objectives.tsx
 create mode 100644 frontend/src/pages/editor/create/participants.tsx
 create mode 100644 frontend/src/pages/editor/index.tsx
 create mode 100644 frontend/src/views/EditorView/index.tsx

diff --git a/frontend/package.json b/frontend/package.json
index d0205be67..b92bfeb71 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -50,6 +50,8 @@
     "d3-selection": "^3.0.0",
     "d3-time": "^3.1.0",
     "d3-time-format": "^4.1.0",
+    "dexie": "^4.0.7",
+    "dexie-react-hooks": "^1.1.7",
     "lodash": "4.17.21",
     "normalize.css": "8.0.1",
     "react": "18.2.0",
diff --git a/frontend/src/editor/LearningObjectives/LearningObjective.tsx b/frontend/src/editor/LearningObjectives/LearningObjective.tsx
new file mode 100644
index 000000000..0e20d66d3
--- /dev/null
+++ b/frontend/src/editor/LearningObjectives/LearningObjective.tsx
@@ -0,0 +1,43 @@
+import { Button } from '@blueprintjs/core'
+import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext'
+import type { FC } from 'react'
+import { memo, useCallback } from 'react'
+import { deleteLearningObjective } from '../indexeddb/operations'
+import type { LearningObjectiveInfo } from '../indexeddb/types'
+
+interface LearningObjectiveProps {
+  objective: LearningObjectiveInfo
+}
+
+const LearningObjectiveItem: FC<LearningObjectiveProps> = ({ objective }) => {
+  const { notify } = useNotifyContext()
+
+  const handleDeleteButton = useCallback(
+    async (objective: LearningObjectiveInfo) => {
+      try {
+        await deleteLearningObjective(objective.id)
+      } catch (err) {
+        notify(
+          `Failed to delete learning objective '${objective.name}': ${err}`,
+          {
+            intent: 'danger',
+          }
+        )
+      }
+    },
+    [notify]
+  )
+
+  return (
+    <div>
+      {objective.name}{' '}
+      <Button
+        type='button'
+        icon='trash'
+        onClick={() => handleDeleteButton(objective)}
+      />
+    </div>
+  )
+}
+
+export default memo(LearningObjectiveItem)
diff --git a/frontend/src/editor/LearningObjectives/index.tsx b/frontend/src/editor/LearningObjectives/index.tsx
new file mode 100644
index 000000000..6043dddd2
--- /dev/null
+++ b/frontend/src/editor/LearningObjectives/index.tsx
@@ -0,0 +1,23 @@
+import LearningObjectiveItem from '@/editor/LearningObjectives/LearningObjective'
+import { db } from '@/editor/indexeddb/db'
+import type { LearningObjectiveInfo } from '@/editor/indexeddb/types'
+import { useLiveQuery } from 'dexie-react-hooks'
+import { memo } from 'react'
+
+const LearningObjectives = () => {
+  const learningObjectives = useLiveQuery(
+    () => db.learningObjectives.toArray(),
+    [],
+    []
+  )
+
+  return (
+    <>
+      {learningObjectives?.map((objective: LearningObjectiveInfo) => (
+        <LearningObjectiveItem key={objective.id} objective={objective} />
+      ))}
+    </>
+  )
+}
+
+export default memo(LearningObjectives)
diff --git a/frontend/src/editor/LearningObjectivesForm/index.tsx b/frontend/src/editor/LearningObjectivesForm/index.tsx
new file mode 100644
index 000000000..f7cf5d3e7
--- /dev/null
+++ b/frontend/src/editor/LearningObjectivesForm/index.tsx
@@ -0,0 +1,39 @@
+import { addLearningObjective } from '@/editor/indexeddb/operations'
+import { Button, InputGroup } from '@blueprintjs/core'
+import { useNotifyContext } from '@inject/shared/notification/contexts/NotifyContext'
+import { memo, useCallback, useRef } from 'react'
+
+const LearningObjectivesForm = () => {
+  const nameRef = useRef<HTMLInputElement>(null)
+  const { notify } = useNotifyContext()
+
+  const handleAddButton = useCallback(
+    async (name: string) => {
+      try {
+        await addLearningObjective({ name })
+        if (nameRef.current) {
+          nameRef.current.value = ''
+        }
+      } catch (err) {
+        notify(`Failed to add learning objective '${name}': ${err}`, {
+          intent: 'danger',
+        })
+      }
+    },
+    [notify]
+  )
+
+  return (
+    <>
+      <InputGroup placeholder='Name' inputRef={nameRef} />
+      <Button
+        type='button'
+        onClick={() => handleAddButton(nameRef.current?.value || '')}
+      >
+        Add
+      </Button>
+    </>
+  )
+}
+
+export default memo(LearningObjectivesForm)
diff --git a/frontend/src/editor/Navbar/NavbarButton.tsx b/frontend/src/editor/Navbar/NavbarButton.tsx
new file mode 100644
index 000000000..fdd41bbc9
--- /dev/null
+++ b/frontend/src/editor/Navbar/NavbarButton.tsx
@@ -0,0 +1,21 @@
+import type { Path } from '@/router'
+import { useNavigate } from '@/router'
+import { Button } from '@blueprintjs/core'
+import type { FC } from 'react'
+
+interface NavbarButtonProps {
+  path: Path
+  name: string
+}
+
+const NavbarButton: FC<NavbarButtonProps> = ({ path, name }) => {
+  const nav = useNavigate()
+
+  return (
+    <Button type='button' onClick={() => nav(path)} alignText='left' minimal>
+      {name}
+    </Button>
+  )
+}
+
+export default NavbarButton
diff --git a/frontend/src/editor/Navbar/index.tsx b/frontend/src/editor/Navbar/index.tsx
new file mode 100644
index 000000000..8a9d8b32d
--- /dev/null
+++ b/frontend/src/editor/Navbar/index.tsx
@@ -0,0 +1,14 @@
+import NavbarButton from './NavbarButton'
+
+const Navbar = () => (
+  <div style={{ display: 'flex', flexDirection: 'column' }}>
+    <NavbarButton path='/editor/create/participants' name='Participants' />
+    <NavbarButton
+      path='/editor/create/learning-objectives'
+      name='Learning objectives'
+    />
+    <NavbarButton path='/editor/create/injects' name='Injects' />
+  </div>
+)
+
+export default Navbar
diff --git a/frontend/src/editor/indexeddb/db.tsx b/frontend/src/editor/indexeddb/db.tsx
new file mode 100644
index 000000000..6a0c3973f
--- /dev/null
+++ b/frontend/src/editor/indexeddb/db.tsx
@@ -0,0 +1,15 @@
+import Dexie, { type EntityTable } from 'dexie'
+import type { LearningObjectiveInfo } from './types'
+
+const dbName = 'EditorDatabase'
+const dbVersion = 1
+
+const db = new Dexie(dbName) as Dexie & {
+  learningObjectives: EntityTable<LearningObjectiveInfo, 'id'>
+}
+
+db.version(dbVersion).stores({
+  learningObjectives: '++id, &name',
+})
+
+export { db }
diff --git a/frontend/src/editor/indexeddb/operations.tsx b/frontend/src/editor/indexeddb/operations.tsx
new file mode 100644
index 000000000..8ee3e1e19
--- /dev/null
+++ b/frontend/src/editor/indexeddb/operations.tsx
@@ -0,0 +1,13 @@
+import { db } from './db'
+import type { LearningObjectiveInfo } from './types'
+
+// learning objectives operations
+export const addLearningObjective = async (
+  objective: Omit<LearningObjectiveInfo, 'id'>
+) =>
+  await db.transaction('rw', db.learningObjectives, async () => {
+    await db.learningObjectives.add(objective)
+  })
+
+export const deleteLearningObjective = async (id: string) =>
+  await db.learningObjectives.delete(id)
diff --git a/frontend/src/editor/indexeddb/types.tsx b/frontend/src/editor/indexeddb/types.tsx
new file mode 100644
index 000000000..abc5b0479
--- /dev/null
+++ b/frontend/src/editor/indexeddb/types.tsx
@@ -0,0 +1,3 @@
+import type { LearningObjective } from '@inject/graphql/fragments/LearningObjective.generated'
+
+export type LearningObjectiveInfo = Pick<LearningObjective, 'id' | 'name'>
diff --git a/frontend/src/logic/Login/index.tsx b/frontend/src/logic/Login/index.tsx
index fde9f3f84..05a6ebf15 100644
--- a/frontend/src/logic/Login/index.tsx
+++ b/frontend/src/logic/Login/index.tsx
@@ -3,11 +3,8 @@ import { Button, InputGroup, NonIdealState, Spinner } from '@blueprintjs/core'
 import { css } from '@emotion/css'
 import useApolloClient from '@inject/graphql/client/useApolloClient'
 import { useLogin } from '@inject/graphql/mutations/Login.generated'
-import type {
-  Identity} from '@inject/graphql/queries/Identity.generated';
-import {
-  IdentityDocument,
-} from '@inject/graphql/queries/Identity.generated'
+import type { Identity } from '@inject/graphql/queries/Identity.generated'
+import { IdentityDocument } from '@inject/graphql/queries/Identity.generated'
 import type { MutableRefObject } from 'react'
 import { useMemo, useRef, useState } from 'react'
 import { useNavigate } from 'react-router-dom'
diff --git a/frontend/src/pages/(navbar)/graphiql.tsx b/frontend/src/pages/(navbar)/graphiql.tsx
index cbb007ebc..db2a71337 100644
--- a/frontend/src/pages/(navbar)/graphiql.tsx
+++ b/frontend/src/pages/(navbar)/graphiql.tsx
@@ -2,8 +2,10 @@ import { Suspense, lazy } from 'react'
 
 const GraphiQLPage = lazy(() => import('@/logic/GraphiQL'))
 
-export const GraphiQL = () => <Suspense>
+export const GraphiQL = () => (
+  <Suspense>
     <GraphiQLPage />
-</Suspense>
+  </Suspense>
+)
 
 export default GraphiQL
diff --git a/frontend/src/pages/editor/_layout.tsx b/frontend/src/pages/editor/_layout.tsx
new file mode 100644
index 000000000..45e2df52a
--- /dev/null
+++ b/frontend/src/pages/editor/_layout.tsx
@@ -0,0 +1,10 @@
+import { useSetPageTitle } from '@/utils'
+import { Outlet } from 'react-router-dom'
+
+const Layout = () => {
+  useSetPageTitle('Editor')
+
+  return <Outlet />
+}
+
+export default Layout
diff --git a/frontend/src/pages/editor/create/_layout.tsx b/frontend/src/pages/editor/create/_layout.tsx
new file mode 100644
index 000000000..53d2a9a54
--- /dev/null
+++ b/frontend/src/pages/editor/create/_layout.tsx
@@ -0,0 +1,15 @@
+import { useSetPageTitle } from '@/utils'
+import EditorView from '@/views/EditorView'
+import { Outlet } from 'react-router-dom'
+
+const Layout = () => {
+  useSetPageTitle('Editor - create definition')
+
+  return (
+    <EditorView>
+      <Outlet />
+    </EditorView>
+  )
+}
+
+export default Layout
diff --git a/frontend/src/pages/editor/create/injects.tsx b/frontend/src/pages/editor/create/injects.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/pages/editor/create/introduction.tsx b/frontend/src/pages/editor/create/introduction.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/pages/editor/create/learning-objectives.tsx b/frontend/src/pages/editor/create/learning-objectives.tsx
new file mode 100644
index 000000000..47bde31b6
--- /dev/null
+++ b/frontend/src/pages/editor/create/learning-objectives.tsx
@@ -0,0 +1,14 @@
+import LearningObjectives from '@/editor/LearningObjectives'
+import LearningObjectivesForm from '@/editor/LearningObjectivesForm'
+import { memo } from 'react'
+
+const LearningObjectivesPage = () => (
+  <>
+    <h1>Expected outcomes</h1>
+    <p>Description.</p>
+    <LearningObjectivesForm />
+    <LearningObjectives />
+  </>
+)
+
+export default memo(LearningObjectivesPage)
diff --git a/frontend/src/pages/editor/create/participants.tsx b/frontend/src/pages/editor/create/participants.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/pages/editor/index.tsx b/frontend/src/pages/editor/index.tsx
new file mode 100644
index 000000000..1c681051c
--- /dev/null
+++ b/frontend/src/pages/editor/index.tsx
@@ -0,0 +1,47 @@
+import InjectLogo from '@/assets/inject-logo--vertical-black.svg?react'
+import { useNavigate } from '@/router'
+import { useSetPageTitle } from '@/utils'
+import { Button } from '@blueprintjs/core'
+import { css } from '@emotion/css'
+import Container from '@inject/shared/components/Container'
+
+const introduction = css`
+  display: flex;
+  flex-direction: column;
+  text-align: center;
+  gap: 1rem;
+  margin: 0 auto;
+  max-width: 200px;
+  width: 100%;
+`
+
+const EditorIndexPage = () => {
+  useSetPageTitle('Editor')
+  const nav = useNavigate()
+
+  return (
+    <Container makeFullHeight>
+      <InjectLogo
+        style={{
+          width: '100%',
+          height: '200px',
+          margin: 'auto',
+        }}
+      />
+      <div className={introduction}>
+        <h1>Editor</h1>
+        <p>Placeholder.</p>
+        <Button
+          type='button'
+          intent='primary'
+          icon='plus'
+          onClick={() => nav('/editor/create/introduction')}
+        >
+          Create
+        </Button>
+      </div>
+    </Container>
+  )
+}
+
+export default EditorIndexPage
diff --git a/frontend/src/router.ts b/frontend/src/router.ts
index 9b1325213..471eca80f 100644
--- a/frontend/src/router.ts
+++ b/frontend/src/router.ts
@@ -12,6 +12,11 @@ export type Path =
   | `/analyst/:exerciseId/emails/:tab/:threadId`
   | `/analyst/:exerciseId/milestones`
   | `/analyst/:exerciseId/tools`
+  | `/editor`
+  | `/editor/create/injects`
+  | `/editor/create/introduction`
+  | `/editor/create/learning-objectives`
+  | `/editor/create/participants`
   | `/exercise-panel`
   | `/graphiql`
   | `/instructor`
diff --git a/frontend/src/views/EditorView/index.tsx b/frontend/src/views/EditorView/index.tsx
new file mode 100644
index 000000000..28773edd1
--- /dev/null
+++ b/frontend/src/views/EditorView/index.tsx
@@ -0,0 +1,49 @@
+import ExitButton from '@/components/ExitButton'
+import type { Section } from '@/components/Sidebar'
+import Sidebar from '@/components/Sidebar'
+import useHideButton from '@/components/Sidebar/useHideButton'
+import Navbar from '@/editor/Navbar'
+import NotificationDropdown from '@inject/shared/notification/NotificationDropdown'
+import type { FC, PropsWithChildren } from 'react'
+import { memo, useMemo } from 'react'
+
+const EditorView: FC<PropsWithChildren> = ({ children }) => {
+  const { hide: hideLeftBar, node: hideButton } = useHideButton()
+
+  const sections: Section[] = useMemo(
+    () => [
+      {
+        name: 'Options',
+        node: (
+          <>
+            {hideButton}
+            <NotificationDropdown hideLabel={hideLeftBar} fill />
+            <ExitButton hideLabel={hideLeftBar} />
+          </>
+        ),
+      },
+      {
+        name: 'Steps',
+        node: !hideLeftBar && <Navbar />,
+      },
+    ],
+    [hideButton, hideLeftBar]
+  )
+
+  return (
+    <>
+      <div style={{ height: '100%', display: 'flex' }}>
+        <Sidebar
+          position='left'
+          sections={sections}
+          hideNames={hideLeftBar}
+          showLogo
+        />
+
+        <div style={{ overflow: 'auto', flex: 1 }}>{children}</div>
+      </div>
+    </>
+  )
+}
+
+export default memo(EditorView)
diff --git a/yarn.lock b/yarn.lock
index bbce953e5..75037aeb7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1177,6 +1177,8 @@ __metadata:
     d3-selection: "npm:^3.0.0"
     d3-time: "npm:^3.1.0"
     d3-time-format: "npm:^4.1.0"
+    dexie: "npm:^4.0.7"
+    dexie-react-hooks: "npm:^1.1.7"
     graphiql: "npm:^3.2.0"
     graphql: "npm:^16.8.1"
     lightningcss: "npm:^1.24.1"
@@ -5210,6 +5212,24 @@ __metadata:
   languageName: node
   linkType: hard
 
+"dexie-react-hooks@npm:^1.1.7":
+  version: 1.1.7
+  resolution: "dexie-react-hooks@npm:1.1.7"
+  peerDependencies:
+    "@types/react": ">=16"
+    dexie: ^3.2 || ^4.0.1-alpha
+    react: ">=16"
+  checksum: 10c0/3ff38e715a52bff9132b483963246ea5372a282d9257c8c2bf8891c522fa0d761d1e362bb352eb63386f49ea9a55c52e1bcc5cce2019deb1eceba40d8a88631d
+  languageName: node
+  linkType: hard
+
+"dexie@npm:^4.0.7":
+  version: 4.0.7
+  resolution: "dexie@npm:4.0.7"
+  checksum: 10c0/37029bdfaaf5ca863680a4d2e2166b86ee78ca970cfd53ae26df0e34e028a98383934437806176ebe7862afa53aabe71c82b5c13b59387e9756736e085aa59fc
+  languageName: node
+  linkType: hard
+
 "diff-sequences@npm:^29.6.3":
   version: 29.6.3
   resolution: "diff-sequences@npm:29.6.3"
-- 
GitLab