From 4a1807e2480274455f439913f45f7df05fe87b82 Mon Sep 17 00:00:00 2001
From: Albin Henriksson <albhe428@student.liu.se>
Date: Thu, 22 Apr 2021 11:03:33 +0000
Subject: [PATCH] Resolve "Move tiny mce to settings panel"

---
 .../components/RndComponent.tsx               | 89 +++++++++++++++++++
 .../components/SlideEditor.tsx                | 20 +----
 .../components/SlideSettings.tsx              | 31 ++++---
 .../components/TextComponentDisplay.test.tsx  | 18 ----
 .../components/TextComponentDisplay.tsx       | 80 -----------------
 .../components/TextComponentEdit.tsx          | 78 ++++++++++++++++
 .../presentationEditor/components/styled.tsx  | 22 ++++-
 client/src/pages/views/PresenterViewPage.tsx  |  2 +-
 8 files changed, 210 insertions(+), 130 deletions(-)
 create mode 100644 client/src/pages/presentationEditor/components/RndComponent.tsx
 delete mode 100644 client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx
 delete mode 100644 client/src/pages/presentationEditor/components/TextComponentDisplay.tsx
 create mode 100644 client/src/pages/presentationEditor/components/TextComponentEdit.tsx

diff --git a/client/src/pages/presentationEditor/components/RndComponent.tsx b/client/src/pages/presentationEditor/components/RndComponent.tsx
new file mode 100644
index 00000000..b1b586ff
--- /dev/null
+++ b/client/src/pages/presentationEditor/components/RndComponent.tsx
@@ -0,0 +1,89 @@
+import axios from 'axios'
+import React, { useState } from 'react'
+import { Rnd } from 'react-rnd'
+import { ComponentTypes } from '../../../enum/ComponentTypes'
+import { useAppSelector } from '../../../hooks'
+import { Component, ImageComponent, TextComponent } from '../../../interfaces/ApiModels'
+import { Position, Size } from '../../../interfaces/Components'
+import CheckboxComponent from './CheckboxComponent'
+import ImageComponentDisplay from './ImageComponentDisplay'
+import { TextComponentContainer } from './styled'
+
+type ImageComponentProps = {
+  component: Component
+}
+
+const RndComponent = ({ component }: ImageComponentProps) => {
+  const [hover, setHover] = useState(false)
+  const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y })
+  const [currentSize, setCurrentSize] = useState<Size>({ w: component.w, h: component.h })
+  const competitionId = useAppSelector((state) => state.editor.competition.id)
+  const slideId = useAppSelector((state) => state.editor.activeSlideId)
+  const handleUpdatePos = (pos: Position) => {
+    // TODO: change path to /slides/${slideId}
+    axios.put(`/competitions/${competitionId}/slides/0/components/${component.id}`, {
+      x: pos.x,
+      y: pos.y,
+    })
+  }
+  const handleUpdateSize = (size: Size) => {
+    // TODO: change path to /slides/${slideId}
+    axios.put(`/competitions/${competitionId}/slides/0/components/${component.id}`, {
+      w: size.w,
+      h: size.h,
+    })
+  }
+
+  const renderInnerComponent = () => {
+    switch (component.type_id) {
+      case ComponentTypes.Checkbox:
+        return <CheckboxComponent key={component.id} component={component} />
+      case ComponentTypes.Text:
+        return (
+          <TextComponentContainer
+            hover={hover}
+            dangerouslySetInnerHTML={{ __html: (component as TextComponent).data.text }}
+          />
+        )
+      case ComponentTypes.Image:
+        return <ImageComponentDisplay key={component.id} component={component as ImageComponent} />
+      default:
+        break
+    }
+  }
+
+  return (
+    <Rnd
+      minWidth={50}
+      minHeight={50}
+      bounds="parent"
+      onDragStop={(e, d) => {
+        setCurrentPos({ x: d.x, y: d.y })
+        handleUpdatePos(d)
+      }}
+      onMouseEnter={() => setHover(true)}
+      onMouseLeave={() => setHover(false)}
+      size={{ width: currentSize.w, height: currentSize.h }}
+      position={{ x: currentPos.x, y: currentPos.y }}
+      onResizeStop={(e, direction, ref, delta, position) => {
+        setCurrentSize({
+          w: ref.offsetWidth,
+          h: ref.offsetHeight,
+        })
+        setCurrentPos(position)
+        handleUpdateSize({ w: ref.offsetWidth, h: ref.offsetHeight })
+        handleUpdatePos(position)
+      }}
+      onResize={(e, direction, ref, delta, position) =>
+        setCurrentSize({
+          w: ref.offsetWidth,
+          h: ref.offsetHeight,
+        })
+      }
+    >
+      {renderInnerComponent()}
+    </Rnd>
+  )
+}
+
+export default RndComponent
diff --git a/client/src/pages/presentationEditor/components/SlideEditor.tsx b/client/src/pages/presentationEditor/components/SlideEditor.tsx
index a71b9262..457f903f 100644
--- a/client/src/pages/presentationEditor/components/SlideEditor.tsx
+++ b/client/src/pages/presentationEditor/components/SlideEditor.tsx
@@ -1,11 +1,7 @@
 import React from 'react'
-import { ComponentTypes } from '../../../enum/ComponentTypes'
 import { useAppSelector } from '../../../hooks'
-import { ImageComponent, TextComponent } from '../../../interfaces/ApiModels'
-import CheckboxComponent from './CheckboxComponent'
-import ImageComponentDisplay from './ImageComponentDisplay'
+import RndComponent from './RndComponent'
 import { SlideEditorContainer, SlideEditorContainerRatio, SlideEditorPaper } from './styled'
-import TextComponentDisplay from './TextComponentDisplay'
 
 const SlideEditor: React.FC = () => {
   const components = useAppSelector(
@@ -16,19 +12,7 @@ const SlideEditor: React.FC = () => {
     <SlideEditorContainer>
       <SlideEditorContainerRatio>
         <SlideEditorPaper>
-          {components &&
-            components.map((component) => {
-              switch (component.type_id) {
-                case ComponentTypes.Checkbox:
-                  return <CheckboxComponent key={component.id} component={component} />
-                case ComponentTypes.Text:
-                  return <TextComponentDisplay key={component.id} component={component as TextComponent} />
-                case ComponentTypes.Image:
-                  return <ImageComponentDisplay key={component.id} component={component as ImageComponent} />
-                default:
-                  break
-              }
-            })}
+          {components && components.map((component) => <RndComponent key={component.id} component={component} />)}
         </SlideEditorPaper>
       </SlideEditorContainerRatio>
     </SlideEditorContainer>
diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx
index e59586bb..e6be5c15 100644
--- a/client/src/pages/presentationEditor/components/SlideSettings.tsx
+++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx
@@ -6,6 +6,7 @@ import {
   DialogContent,
   DialogContentText,
   DialogTitle,
+  Divider,
   FormControl,
   InputLabel,
   List,
@@ -26,7 +27,8 @@ import { useParams } from 'react-router-dom'
 import { getEditorCompetition } from '../../../actions/editor'
 import { useAppDispatch, useAppSelector } from '../../../hooks'
 import { QuestionAlternative, TextComponent } from '../../../interfaces/ApiModels'
-import { HiddenInput } from './styled'
+import { HiddenInput, TextCard } from './styled'
+import TextComponentEdit from './TextComponentEdit'
 
 const useStyles = makeStyles((theme: Theme) =>
   createStyles({
@@ -234,9 +236,15 @@ const SlideSettings: React.FC = () => {
   }
 
   const handleAddText = async () => {
-    console.log('Add text component')
-    // TODO: post the new text]
-    // setTexts([...texts, { id: 'newText', name: 'New Text' }])
+    if (activeSlide) {
+      await axios.post(`/competitions/${id}/slides/${activeSlide?.order}/components`, {
+        type_id: 1,
+        data: { text: 'Ny text' },
+        w: 315,
+        h: 50,
+      })
+      dispatch(getEditorCompetition(id))
+    }
   }
 
   const GreenCheckbox = withStyles({
@@ -338,9 +346,8 @@ const SlideSettings: React.FC = () => {
           helperText="Lämna blank för att inte använda timerfunktionen"
           label="Timer"
           type="number"
-          defaultValue={activeSlide?.timer || 0}
           onChange={updateTimer}
-          value={timer}
+          value={timer || ''}
         />
       </ListItem>
 
@@ -383,13 +390,13 @@ const SlideSettings: React.FC = () => {
         </ListItem>
         {texts &&
           texts.map((text) => (
-            <div key={text.id}>
-              <ListItem divider>
-                <TextField className={classes.textInput} label={text.data.text} variant="outlined" />
-                <CloseIcon className={classes.clickableIcon} />
-              </ListItem>
-            </div>
+            <TextCard elevation={4} key={text.id}>
+              <TextComponentEdit component={text} />
+
+              <Divider />
+            </TextCard>
           ))}
+
         <ListItem className={classes.center} button onClick={handleAddText}>
           <Typography className={classes.addButtons} variant="button">
             Lägg till text
diff --git a/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx b/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx
deleted file mode 100644
index c4489878..00000000
--- a/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Editor } from '@tinymce/tinymce-react'
-import { mount } from 'enzyme'
-import React from 'react'
-import { Provider } from 'react-redux'
-import store from '../../../store'
-import TextComponentDisplay from './TextComponentDisplay'
-
-it('renders text component display', () => {
-  const testText = 'TEST'
-  const container = mount(
-    <Provider store={store}>
-      <TextComponentDisplay
-        component={{ id: 0, x: 0, y: 0, w: 0, h: 0, data: { text: testText, font: '123123' }, type_id: 2 }}
-      />
-    </Provider>
-  )
-  expect(container.find(Editor).prop('initialValue')).toBe(testText)
-})
diff --git a/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx b/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx
deleted file mode 100644
index 4fb34650..00000000
--- a/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { Editor } from '@tinymce/tinymce-react'
-import axios from 'axios'
-import React, { useState } from 'react'
-import { Rnd } from 'react-rnd'
-import { useAppSelector } from '../../../hooks'
-import { TextComponent } from '../../../interfaces/ApiModels'
-import { Position, Size } from '../../../interfaces/Components'
-
-type ImageComponentProps = {
-  component: TextComponent
-}
-
-const TextComponentDisplay = ({ component }: ImageComponentProps) => {
-  const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y })
-  const [currentSize, setCurrentSize] = useState<Size>({ w: component.w, h: component.h })
-  const competitionId = useAppSelector((state) => state.editor.competition.id)
-  const slideId = useAppSelector((state) => state.editor.activeSlideId)
-  if (component.id === 1) console.log(component)
-  const handleEditorChange = (e: any) => {
-    console.log('Content was updated:', e.target.getContent())
-    axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, {
-      data: { ...component.data, text: e.target.getContent() },
-    })
-  }
-  const handleUpdatePos = (pos: Position) => {
-    axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, {
-      x: pos.x,
-      y: pos.y,
-    })
-  }
-  const handleUpdateSize = () => {
-    axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, {
-      w: currentSize.w,
-      h: currentSize.h,
-    })
-  }
-  return (
-    <Rnd
-      minWidth={50}
-      minHeight={50}
-      bounds="parent"
-      onDragStop={(e, d) => {
-        setCurrentPos({ x: d.x, y: d.y })
-        handleUpdatePos(d)
-      }}
-      size={{ width: currentSize.w, height: currentSize.h }}
-      position={{ x: currentPos.x, y: currentPos.y }}
-      onResize={(e, direction, ref, delta, position) => {
-        setCurrentSize({
-          w: ref.offsetWidth,
-          h: ref.offsetHeight,
-        })
-        setCurrentPos(position)
-      }}
-      onResizeStop={handleUpdateSize}
-    >
-      <div style={{ height: '100%', width: '100%' }}>
-        <Editor
-          initialValue={component.data.text}
-          init={{
-            height: '100%',
-            menubar: false,
-            plugins: [
-              'advlist autolink lists link image charmap print preview anchor',
-              'searchreplace visualblocks code fullscreen',
-              'insertdatetime media table paste code help wordcount',
-            ],
-            toolbar:
-              'undo redo | formatselect | fontselect | bold italic backcolor | \
-             alignleft aligncenter alignright alignjustify | \
-             bullist numlist outdent indent | removeformat | help',
-          }}
-          onChange={handleEditorChange}
-        />
-      </div>
-    </Rnd>
-  )
-}
-
-export default TextComponentDisplay
diff --git a/client/src/pages/presentationEditor/components/TextComponentEdit.tsx b/client/src/pages/presentationEditor/components/TextComponentEdit.tsx
new file mode 100644
index 00000000..92202993
--- /dev/null
+++ b/client/src/pages/presentationEditor/components/TextComponentEdit.tsx
@@ -0,0 +1,78 @@
+import { Editor } from '@tinymce/tinymce-react'
+import axios from 'axios'
+import React, { useEffect, useState } from 'react'
+import { useParams } from 'react-router-dom'
+import { getEditorCompetition } from '../../../actions/editor'
+import { useAppDispatch, useAppSelector } from '../../../hooks'
+import { TextComponent } from '../../../interfaces/ApiModels'
+import { DeleteTextButton } from './styled'
+
+type ImageComponentProps = {
+  component: TextComponent
+}
+
+interface CompetitionParams {
+  id: string
+}
+
+const TextComponentEdit = ({ component }: ImageComponentProps) => {
+  const { id }: CompetitionParams = useParams()
+  const competitionId = useAppSelector((state) => state.editor.competition.id)
+  const [content, setContent] = useState('')
+  const [timerHandle, setTimerHandle] = React.useState<number | undefined>(undefined)
+  const dispatch = useAppDispatch()
+
+  useEffect(() => {
+    setContent(component.data.text)
+  }, [])
+
+  const handleSaveText = async (a: string) => {
+    setContent(a)
+    if (timerHandle) {
+      clearTimeout(timerHandle)
+      setTimerHandle(undefined)
+    }
+    //Only updates 250ms after last input was made to not spam
+    setTimerHandle(
+      window.setTimeout(async () => {
+        console.log('Content was updated on server. id: ', component.id)
+        await axios.put(`/competitions/${competitionId}/slides/0/components/${component.id}`, {
+          data: { ...component.data, text: a },
+        })
+        dispatch(getEditorCompetition(id))
+      }, 250)
+    )
+  }
+
+  const handleDeleteText = async (componentId: number) => {
+    await axios.delete(`/competitions/${id}/slides/0/components/${componentId}`)
+    dispatch(getEditorCompetition(id))
+  }
+
+  return (
+    <div style={{ minHeight: '300px', height: '100%', width: '100%' }}>
+      <Editor
+        value={content || ''}
+        init={{
+          height: '300px',
+          menubar: false,
+          plugins: [
+            'advlist autolink lists link image charmap print preview anchor',
+            'searchreplace visualblocks code fullscreen',
+            'insertdatetime media table paste code help wordcount',
+          ],
+          toolbar:
+            'undo redo save | fontselect | formatselect | bold italic backcolor | \
+             alignleft aligncenter alignright alignjustify | \
+             bullist numlist outdent indent | removeformat | help',
+        }}
+        onEditorChange={(a, e) => handleSaveText(a)}
+      />
+      <DeleteTextButton variant="contained" color="secondary" onClick={() => handleDeleteText(component.id)}>
+        Ta bort
+      </DeleteTextButton>
+    </div>
+  )
+}
+
+export default TextComponentEdit
diff --git a/client/src/pages/presentationEditor/components/styled.tsx b/client/src/pages/presentationEditor/components/styled.tsx
index 32df0ac9..8bee7f0d 100644
--- a/client/src/pages/presentationEditor/components/styled.tsx
+++ b/client/src/pages/presentationEditor/components/styled.tsx
@@ -1,4 +1,4 @@
-import { Tab } from '@material-ui/core'
+import { Button, Card, Tab } from '@material-ui/core'
 import styled from 'styled-components'
 
 export const SettingsTab = styled(Tab)`
@@ -44,3 +44,23 @@ export const ToolbarPadding = styled.div`
   height: 0;
   padding-top: 55px;
 `
+
+export const TextCard = styled(Card)`
+  margin-bottom: 15px;
+  margin-top: 10px;
+`
+
+export const DeleteTextButton = styled(Button)`
+  width: 100%;
+  margin-bottom: 7px;
+`
+
+interface TextComponentContainerProps {
+  hover: boolean
+}
+
+export const TextComponentContainer = styled.div<TextComponentContainerProps>`
+  height: 100%;
+  width: 100%;
+  border: solid ${(props) => (props.hover ? 1 : 0)}px;
+`
diff --git a/client/src/pages/views/PresenterViewPage.tsx b/client/src/pages/views/PresenterViewPage.tsx
index 3bb20a43..40aa252d 100644
--- a/client/src/pages/views/PresenterViewPage.tsx
+++ b/client/src/pages/views/PresenterViewPage.tsx
@@ -67,7 +67,7 @@ const PresenterViewPage: React.FC = () => {
     socket_connect()
     socketSetSlide // Behövs denna?
     setTimeout(startCompetition, 500) // Ghetto, wait for everything to load
-    console.log(id)
+    // console.log(id)
   }, [])
 
   const handleOpenPopover = (event: React.MouseEvent<HTMLButtonElement>) => {
-- 
GitLab