From 71911063b0107e5b22311a970387c0254fe8e7b1 Mon Sep 17 00:00:00 2001
From: mrfry <mr.fry@tutanota.com>
Date: Thu, 4 Mar 2021 21:31:32 +0100
Subject: [PATCH] Added comments to news items

---
 src/components/comments.js            | 169 ++++++++++++++++++++++++++
 src/components/comments.module.css    |  80 ++++++++++++
 src/components/newsEntry.js           |  33 ++++-
 src/components/reactButton.js         |   3 +
 src/components/reactButton.module.css |  10 +-
 src/components/tooltip.js             |   8 +-
 src/components/tooltip.module.css     |   1 -
 src/constants.json                    |   2 +-
 src/pages/index.js                    |  73 ++++++++++-
 9 files changed, 357 insertions(+), 22 deletions(-)
 create mode 100644 src/components/comments.js
 create mode 100644 src/components/comments.module.css

diff --git a/src/components/comments.js b/src/components/comments.js
new file mode 100644
index 0000000..909e573
--- /dev/null
+++ b/src/components/comments.js
@@ -0,0 +1,169 @@
+import React, { useState } from 'react'
+
+import ReactButton from './reactButton.js'
+
+import styles from './comments.module.css'
+
+function CommentInput({ onSubmit }) {
+  const [val, setVal] = useState('')
+  return (
+    <div className={styles.commentAreaContainer}>
+      <textarea
+        autoFocus
+        className={styles.commentArea}
+        value={val}
+        onChange={(e) => {
+          setVal(e.target.value)
+        }}
+      />
+      <div>
+        <span
+          onClick={() => {
+            onSubmit(val)
+          }}
+          className={styles.button}
+        >
+          Submit
+        </span>
+      </div>
+    </div>
+  )
+}
+
+function Comment({ comment, index, onComment, onDelete, onReact, uid }) {
+  const [displayed, setDisplayed] = useState(true)
+  const [commenting, setCommenting] = useState(false)
+  const { text, subComments, date, user, reacts } = comment
+  const own = uid === user
+
+  return (
+    <div className={styles.comment}>
+      <div className={`${styles.commentData} ${own && styles.ownComment}`}>
+        <div className={styles.commentHeader}>
+          <div className={styles.userContainer}>
+            <div
+              className={styles.showHide}
+              onClick={() => {
+                setDisplayed(!displayed)
+              }}
+            >
+              {displayed ? '[-]' : '[+]'}
+            </div>
+            <div>User #{user}</div>
+          </div>
+          <div>{date}</div>
+        </div>
+        <div className={`${!displayed && styles.hidden}`}>
+          <div className={styles.commentText}> {text}</div>
+          <div className={styles.actionsContainer}>
+            <div
+              className={styles.button}
+              onClick={() => {
+                setCommenting(true)
+              }}
+            >
+              Reply...
+            </div>
+            {own && (
+              <div
+                className={styles.button}
+                onClick={() => {
+                  onDelete([index])
+                }}
+              >
+                Delete
+              </div>
+            )}
+            <ReactButton
+              onClick={(reaction, isDelete) => {
+                onReact([index], reaction, isDelete)
+              }}
+              uid={uid}
+              existingReacts={reacts}
+            />
+          </div>
+          {commenting && (
+            <CommentInput
+              onSubmit={(e) => {
+                onComment([index], e)
+                setCommenting(false)
+              }}
+            />
+          )}
+        </div>
+      </div>
+      <div className={`${!displayed && styles.hidden}`}>
+        {subComments &&
+          subComments.map((sc, i) => {
+            return (
+              <Comment
+                comment={sc}
+                onReact={(path, reaction, isDelete) => {
+                  onReact([...path, index], reaction, isDelete)
+                }}
+                onDelete={(path) => {
+                  onDelete([...path, index])
+                }}
+                onComment={(path, text) => {
+                  onComment([...path, index], text)
+                }}
+                index={i}
+                key={i}
+                uid={uid}
+              />
+            )
+          })}
+      </div>
+    </div>
+  )
+}
+
+export default function Comments({
+  comments,
+  onComment,
+  onDelete,
+  onReact,
+  uid,
+}) {
+  const [addingNewComment, setAddingNewComment] = useState(false)
+  return (
+    <div>
+      {comments && comments.length > 0 ? (
+        comments.map((comment, i) => {
+          return (
+            <Comment
+              onReact={onReact}
+              onComment={onComment}
+              onDelete={onDelete}
+              comment={comment}
+              index={i}
+              key={i}
+              uid={uid}
+            />
+          )
+        })
+      ) : (
+        <div>
+          <div>No comments yet</div>
+        </div>
+      )}
+      {addingNewComment ? (
+        <CommentInput
+          onSubmit={(e) => {
+            setAddingNewComment(false)
+            onComment([], e)
+          }}
+        />
+      ) : (
+        <span
+          onClick={() => {
+            setAddingNewComment(true)
+          }}
+          className={styles.button}
+        >
+          Add new
+        </span>
+      )}
+    </div>
+  )
+}
diff --git a/src/components/comments.module.css b/src/components/comments.module.css
new file mode 100644
index 0000000..ef4c72d
--- /dev/null
+++ b/src/components/comments.module.css
@@ -0,0 +1,80 @@
+.comment {
+  margin-left: 25px;
+  padding: 8px 0px;
+}
+
+.commentData {
+  padding: 5px 2px;
+  border-left: 1px solid var(--text-color);
+  background-color: #222;
+}
+
+.commentHeader {
+  display: flex;
+  justify-content: space-between;
+}
+
+.commentHeader > div {
+  font-weight: bold;
+  margin: 2px;
+}
+
+.userContainer {
+  display: flex;
+  flex-direction: row;
+}
+
+.userContainer > div {
+  padding: 0px 3px;
+}
+
+.commentText {
+  margin: 2px;
+  padding: 10px 5px;
+}
+
+.ownComment {
+  border-left: 1px solid yellow;
+}
+
+.showHide {
+  cursor: pointer;
+}
+
+.hidden {
+  display: none;
+}
+
+.actionsContainer {
+  display: flex;
+  align-items: center;
+}
+
+.button {
+  margin: 2px 2px;
+  padding: 0px 10px;
+  border: 1px solid #444;
+  border-radius: 6px;
+  cursor: pointer;
+}
+
+.button:hover {
+  background-color: #444;
+}
+
+.commentArea {
+  color: var(--text-color);
+  background-color: var(--background-color);
+  font-size: 16px;
+  box-sizing: border-box;
+  height: 120px;
+  width: 100%;
+}
+
+.commentAreaContainer {
+  margin: 0px 8px 3px 8px;
+}
+
+.commentAreaContainer > div {
+  margin: 5px 0px;
+}
diff --git a/src/components/newsEntry.js b/src/components/newsEntry.js
index 58228d2..f899f52 100644
--- a/src/components/newsEntry.js
+++ b/src/components/newsEntry.js
@@ -1,29 +1,52 @@
 import React from 'react'
 
 import ReactButton from './reactButton.js'
+import Comments from './comments.js'
 
 import styles from './newsEntry.module.css'
 
-export default function NewsEntry({ newsKey, newsItem, uid, onReact }) {
+export default function NewsEntry({
+  newsKey,
+  newsItem,
+  uid,
+  onReact,
+  onComment,
+  onDelete,
+}) {
+  const { reacts, title, body, comments } = newsItem
+
   return (
     <div>
       <div className={styles.newsContainer}>
         <div className={styles.itemNumber}>{newsKey} :</div>
         <div
           className={styles.newsTitle}
-          dangerouslySetInnerHTML={{ __html: newsItem.title }}
+          dangerouslySetInnerHTML={{ __html: title }}
         />
         <div
           className={styles.newsBody}
-          dangerouslySetInnerHTML={{ __html: newsItem.body }}
+          dangerouslySetInnerHTML={{ __html: body }}
         />
       </div>
       <ReactButton
-        existingReacts={newsItem.reacts}
+        existingReacts={reacts}
         uid={uid}
-        onClick={onReact}
+        onClick={(reaction, isDelete) => {
+          onReact({ type: 'news', reaction, isDelete })
+        }}
       />
       <hr />
+      <Comments
+        uid={uid}
+        onReact={(path, reaction, isDelete) => {
+          onReact({ type: 'comment', path, reaction, isDelete })
+        }}
+        onComment={onComment}
+        onDelete={onDelete}
+        comments={comments}
+      />
+      <hr />
+      <hr />
     </div>
   )
 }
diff --git a/src/components/reactButton.js b/src/components/reactButton.js
index 1937463..d23aa3c 100644
--- a/src/components/reactButton.js
+++ b/src/components/reactButton.js
@@ -5,6 +5,7 @@ import Tooltip from './tooltip.js'
 import styles from './reactButton.module.css'
 import reactions from '../data/reactions.json'
 
+// TODO: fixdis
 const breakEvery = 7
 
 function ExistingReacts({ existingReacts, onClick, uid }) {
@@ -20,6 +21,7 @@ function ExistingReacts({ existingReacts, onClick, uid }) {
           }
           return (
             <div
+              title={currReact.join(', ')}
               className={`${currReact.includes(uid) && styles.reacted}`}
               key={key}
               onClick={() => {
@@ -78,6 +80,7 @@ export default function ReactButton({ onClick, existingReacts, uid }) {
 
   return (
     <div
+      className={styles.reactContainer}
       onMouseEnter={() => {
         setOpened(true)
       }}
diff --git a/src/components/reactButton.module.css b/src/components/reactButton.module.css
index 03217c8..9c53a71 100644
--- a/src/components/reactButton.module.css
+++ b/src/components/reactButton.module.css
@@ -15,12 +15,10 @@
 
 .reactionContainer > div {
   margin: 2px 2px;
-  padding: 0px 8px;
-  background-color: #444;
+  padding: 0px 10px;
+  border: 1px solid #444;
   border-radius: 6px;
   cursor: pointer;
-
-  font-size: 18px;
 }
 
 .reactionContainer > div:hover {
@@ -35,3 +33,7 @@
 .reacted {
   color: yellow;
 }
+
+.reactContainer {
+  display: inline-block;
+}
diff --git a/src/components/tooltip.js b/src/components/tooltip.js
index a629003..f8a8987 100644
--- a/src/components/tooltip.js
+++ b/src/components/tooltip.js
@@ -3,11 +3,9 @@ import styles from './tooltip.module.css'
 
 export default function Tooltip({ children, text, opened }) {
   return (
-    <div>
-      <div className={styles.tooltip}>
-        {text()}
-        {opened && <span className={styles.tooltiptext}>{children}</span>}
-      </div>
+    <div className={styles.tooltip}>
+      {text()}
+      {opened && <span className={styles.tooltiptext}>{children}</span>}
     </div>
   )
 }
diff --git a/src/components/tooltip.module.css b/src/components/tooltip.module.css
index 59d5723..05b9267 100644
--- a/src/components/tooltip.module.css
+++ b/src/components/tooltip.module.css
@@ -1,7 +1,6 @@
 .tooltip {
   position: relative;
   display: inline-block;
-  border-bottom: 1px dotted black;
 }
 
 .tooltip .tooltiptext {
diff --git a/src/constants.json b/src/constants.json
index 8650745..dec44ec 100644
--- a/src/constants.json
+++ b/src/constants.json
@@ -1,6 +1,6 @@
 {
   "siteUrl": "https://qmining.frylabs.net/",
-  "apiUrl": "https://api.frylabs.net/",
+  "apiUrl": "http://localhost:8080/",
   "mobileWindowWidth": 700,
   "maxQuestionsToRender": 250
 }
diff --git a/src/pages/index.js b/src/pages/index.js
index bbc157c..23f193f 100644
--- a/src/pages/index.js
+++ b/src/pages/index.js
@@ -57,9 +57,50 @@ export default function Index({ globalData }) {
           let newsEntryData = news[key]
           return (
             <NewsEntry
-              onReact={(reaction, isDelete) => {
-                console.log(reaction, isDelete)
-                fetch(constants.apiUrl + 'infos', {
+              onReact={({ type, path, reaction, isDelete }) => {
+                if (type === 'news') {
+                  fetch(constants.apiUrl + 'infos', {
+                    method: 'POST',
+                    credentials: 'include',
+                    headers: {
+                      Accept: 'application/json',
+                      'Content-Type': 'application/json',
+                    },
+                    body: JSON.stringify({
+                      react: reaction,
+                      newsKey: key,
+                      isDelete: isDelete,
+                    }),
+                  }).then((res) => {
+                    // TODO: dont refetch news
+                    fetchNews().then((res) => {
+                      setNews(res)
+                    })
+                  })
+                } else if (type === 'comment') {
+                  fetch(constants.apiUrl + 'comment', {
+                    method: 'POST',
+                    credentials: 'include',
+                    headers: {
+                      Accept: 'application/json',
+                      'Content-Type': 'application/json',
+                    },
+                    body: JSON.stringify({
+                      type: 'reaction',
+                      newsKey: key,
+                      path: path,
+                      reaction: reaction,
+                      isDelete: isDelete,
+                    }),
+                  }).then((res) => {
+                    fetchNews().then((res) => {
+                      setNews(res)
+                    })
+                  })
+                }
+              }}
+              onDelete={(path) => {
+                fetch(constants.apiUrl + 'comment', {
                   method: 'POST',
                   credentials: 'include',
                   headers: {
@@ -67,11 +108,31 @@ export default function Index({ globalData }) {
                     'Content-Type': 'application/json',
                   },
                   body: JSON.stringify({
-                    react: reaction,
+                    type: 'delete',
+                    path: path,
                     newsKey: key,
-                    isDelete: isDelete,
                   }),
-                }).then((res) => {
+                }).then(() => {
+                  fetchNews().then((res) => {
+                    setNews(res)
+                  })
+                })
+              }}
+              onComment={(path, text) => {
+                fetch(constants.apiUrl + 'comment', {
+                  method: 'POST',
+                  credentials: 'include',
+                  headers: {
+                    Accept: 'application/json',
+                    'Content-Type': 'application/json',
+                  },
+                  body: JSON.stringify({
+                    type: 'add',
+                    path: path,
+                    text: text,
+                    newsKey: key,
+                  }),
+                }).then(() => {
                   fetchNews().then((res) => {
                     setNews(res)
                   })