From 26fbc88c1038971e3c4fb48f40eb09ff15816099 Mon Sep 17 00:00:00 2001
From: xaralis <filip.varecha@fragaria.cz>
Date: Thu, 17 Dec 2020 15:11:31 +0100
Subject: [PATCH] feat: edit announcement (wip)

---
 package-lock.json                             | 13 +---
 package.json                                  |  2 +-
 src/actions/announcements.js                  | 58 ++++++++++++++-
 src/actions/program.js                        |  4 +-
 src/api.js                                    | 16 +++++
 .../annoucements/AnnouncementEditModal.jsx    | 70 +++++++++++++++++++
 src/components/cards/Card.jsx                 |  9 +++
 src/components/cards/CardActions.jsx          | 13 ++++
 src/components/cards/CardBody.jsx             |  9 +++
 src/components/cards/CardBodyText.jsx         |  7 ++
 src/components/cards/CardHeadline.jsx         |  7 ++
 src/components/cards/index.js                 |  5 ++
 src/components/modals/ModalConfirm.jsx        | 29 +++++---
 src/containers/AnnoucementsContainer.jsx      | 69 ++++++++++++++----
 14 files changed, 271 insertions(+), 40 deletions(-)
 create mode 100644 src/api.js
 create mode 100644 src/components/annoucements/AnnouncementEditModal.jsx
 create mode 100644 src/components/cards/Card.jsx
 create mode 100644 src/components/cards/CardActions.jsx
 create mode 100644 src/components/cards/CardBody.jsx
 create mode 100644 src/components/cards/CardBodyText.jsx
 create mode 100644 src/components/cards/CardHeadline.jsx
 create mode 100644 src/components/cards/index.js

diff --git a/package-lock.json b/package-lock.json
index 75b4a90..0609126 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10949,19 +10949,12 @@
       }
     },
     "pullstate": {
-      "version": "1.20.4",
-      "resolved": "https://registry.npmjs.org/pullstate/-/pullstate-1.20.4.tgz",
-      "integrity": "sha512-SksJ70iYNrC+YsGjMx54pXT2/iYYUu3zg9hezjHNjPPwyViglo4lh+N0I4DwB6xw92s+9NagD3AXMGMfiIt76g==",
+      "version": "1.20.5",
+      "resolved": "https://registry.npmjs.org/pullstate/-/pullstate-1.20.5.tgz",
+      "integrity": "sha512-9+QAXjf5WugIPEFHgMTwKM42uDx8ezB1BDobh7gpg9OCta5rp1XdFxa6tLljB/4NUxnI5YqoiE2s15ZOh+sl4A==",
       "requires": {
         "fast-deep-equal": "^3.1.3",
         "immer": "^7.0.1"
-      },
-      "dependencies": {
-        "immer": {
-          "version": "7.0.15",
-          "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.15.tgz",
-          "integrity": "sha512-yM7jo9+hvYgvdCQdqvhCNRRio0SCXc8xDPzA25SvKWa7b1WVPjLwQs1VYU5JPXjcJPTqAa5NP5dqpORGYBQ2AA=="
-        }
       }
     },
     "pump": {
diff --git a/package.json b/package.json
index f872dfd..a0a7779 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
     "immer": "^7.0.15",
     "keycloak-js": "^10.0.2",
     "lodash": "^4.17.20",
-    "pullstate": "^1.20.4",
+    "pullstate": "^1.20.5",
     "react": "^16.13.1",
     "react-device-detect": "^1.13.1",
     "react-dom": "^16.13.1",
diff --git a/src/actions/announcements.js b/src/actions/announcements.js
index f4eee78..2681527 100644
--- a/src/actions/announcements.js
+++ b/src/actions/announcements.js
@@ -1,3 +1,5 @@
+import findIndex from "lodash/findIndex";
+import remove from "lodash/remove";
 import { createAsyncAction, successResult } from "pullstate";
 
 import { AnnouncementStore } from "stores";
@@ -20,9 +22,59 @@ export const addAnnouncement = createAsyncAction(
   },
   {
     postActionHook: ({ result }) => {
-      AnnouncementStore.update((state) => {
-        state.items.push(result.payload);
-      });
+      if (!result.error) {
+        AnnouncementStore.update((state) => {
+          state.items.push(result.payload);
+        });
+      }
+    },
+  }
+);
+
+/**
+ * Delete existing announcement.
+ */
+export const deleteAnnouncement = createAsyncAction(
+  /**
+   *
+   * @param {CF2021.Announcement} item
+   */
+  async (item) => {
+    return successResult(item);
+  },
+  {
+    postActionHook: ({ result }) => {
+      if (!result.error) {
+        AnnouncementStore.update((state) => {
+          remove(state.items, { id: result.payload.id });
+        });
+      }
+    },
+  }
+);
+
+/**
+ * Update content of an announcement.
+ */
+export const updateAnnouncementContent = createAsyncAction(
+  /**
+   *
+   * @param {CF2021.Announcement} item
+   * @param {string} newContent
+   */
+  async (item, newContent) => {
+    return successResult({ item, newContent });
+  },
+  {
+    postActionHook: ({ result }) => {
+      if (!result.error) {
+        AnnouncementStore.update((state) => {
+          const itemIdx = findIndex(state.items, {
+            id: result.payload.item.id,
+          });
+          state.items[itemIdx].content = result.payload.newContent;
+        });
+      }
     },
   }
 );
diff --git a/src/actions/program.js b/src/actions/program.js
index 2c45842..7a70b07 100644
--- a/src/actions/program.js
+++ b/src/actions/program.js
@@ -1,13 +1,13 @@
+import { fetch } from "api";
 import pick from "lodash/pick";
 import { createAsyncAction, errorResult, successResult } from "pullstate";
-import fetch from "unfetch";
 
 import { ProgramStore } from "stores";
 
 export const loadProgram = createAsyncAction(
   async () => {
     try {
-      const resp = await fetch(`${process.env.REACT_APP_API_BASE_URL}/program`);
+      const resp = await fetch("/program");
       const mappings = await resp.json();
       return successResult(mappings);
     } catch (err) {
diff --git a/src/api.js b/src/api.js
new file mode 100644
index 0000000..b5674b2
--- /dev/null
+++ b/src/api.js
@@ -0,0 +1,16 @@
+import baseFetch from "unfetch";
+
+import { AuthStore } from "./stores";
+
+export const fetch = (url, opts) => {
+  const { isAuthenticated, user } = AuthStore.getRawState();
+
+  opts = opts || {};
+  opts.headers = opts.headers || {};
+
+  if (isAuthenticated) {
+    // opts.headers.Authorization = "Bearer " + user.accessToken;
+  }
+
+  return baseFetch(process.env.REACT_APP_API_BASE_URL + url, opts);
+};
diff --git a/src/components/annoucements/AnnouncementEditModal.jsx b/src/components/annoucements/AnnouncementEditModal.jsx
new file mode 100644
index 0000000..29624d8
--- /dev/null
+++ b/src/components/annoucements/AnnouncementEditModal.jsx
@@ -0,0 +1,70 @@
+import React, { useState } from "react";
+
+import Button from "components/Button";
+import { Card, CardActions, CardBody, CardHeadline } from "components/cards";
+import Modal from "components/modals/Modal";
+
+const AnnouncementEditModal = ({
+  announcement,
+  onCancel,
+  onConfirm,
+  ...props
+}) => {
+  const [text, setText] = useState(announcement.content);
+
+  const onTextInput = (evt) => {
+    setText(evt.target.value);
+  };
+
+  const confirm = (evt) => {
+    if (!!text) {
+      onConfirm(text);
+    }
+  };
+
+  return (
+    <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}>
+      <Card>
+        <CardBody>
+          <div className="flex items-center justify-between mb-4">
+            <CardHeadline>Upravit oznámení</CardHeadline>
+            <button onClick={onCancel}>
+              <i className="ico--close"></i>
+            </button>
+          </div>
+          <div className="form-field">
+            <div className="form-field__wrapper form-field__wrapper--shadowed">
+              <textarea
+                className="text-input form-field__control "
+                value={text}
+                rows="8"
+                placeholder="Vyplňte text oznámení"
+                onChange={onTextInput}
+              ></textarea>
+            </div>
+          </div>
+        </CardBody>
+        <CardActions right className="space-x-1">
+          <Button
+            hoverActive
+            color="blue-300"
+            className="text-sm"
+            onClick={confirm}
+          >
+            Uložit
+          </Button>
+          <Button
+            hoverActive
+            color="red-600"
+            className="text-sm"
+            onClick={onCancel}
+          >
+            Zrušit
+          </Button>
+        </CardActions>
+      </Card>
+    </Modal>
+  );
+};
+
+export default AnnouncementEditModal;
diff --git a/src/components/cards/Card.jsx b/src/components/cards/Card.jsx
new file mode 100644
index 0000000..58ce9fd
--- /dev/null
+++ b/src/components/cards/Card.jsx
@@ -0,0 +1,9 @@
+import React from "react";
+import classNames from "classnames";
+
+const Card = ({ children, elevation = 21, className }) => {
+  const cls = classNames("card", `elevation-${elevation}`, className);
+  return <div className={cls}>{children}</div>;
+};
+
+export default Card;
diff --git a/src/components/cards/CardActions.jsx b/src/components/cards/CardActions.jsx
new file mode 100644
index 0000000..0dc8e51
--- /dev/null
+++ b/src/components/cards/CardActions.jsx
@@ -0,0 +1,13 @@
+import React from "react";
+import classNames from "classnames";
+
+const CardActions = ({ children, right, className }) => {
+  const cls = classNames(
+    "card-actions",
+    { "card-actions--right": !!right },
+    className
+  );
+  return <div className={cls}>{children}</div>;
+};
+
+export default CardActions;
diff --git a/src/components/cards/CardBody.jsx b/src/components/cards/CardBody.jsx
new file mode 100644
index 0000000..3bf0815
--- /dev/null
+++ b/src/components/cards/CardBody.jsx
@@ -0,0 +1,9 @@
+import React from "react";
+import classNames from "classnames";
+
+const CardBody = ({ children, className }) => {
+  const cls = classNames("card__body", className);
+  return <div className={cls}>{children}</div>;
+};
+
+export default CardBody;
diff --git a/src/components/cards/CardBodyText.jsx b/src/components/cards/CardBodyText.jsx
new file mode 100644
index 0000000..7fb8944
--- /dev/null
+++ b/src/components/cards/CardBodyText.jsx
@@ -0,0 +1,7 @@
+import React from "react";
+
+const CardBodyText = ({ children }) => {
+  return <div className="card-body-text">{children}</div>;
+};
+
+export default CardBodyText;
diff --git a/src/components/cards/CardHeadline.jsx b/src/components/cards/CardHeadline.jsx
new file mode 100644
index 0000000..fda727c
--- /dev/null
+++ b/src/components/cards/CardHeadline.jsx
@@ -0,0 +1,7 @@
+import React from "react";
+
+const CardHeadline = ({ children }) => {
+  return <h1 className="card-headline">{children}</h1>;
+};
+
+export default CardHeadline;
diff --git a/src/components/cards/index.js b/src/components/cards/index.js
new file mode 100644
index 0000000..51bf741
--- /dev/null
+++ b/src/components/cards/index.js
@@ -0,0 +1,5 @@
+export { default as Card } from "./Card";
+export { default as CardActions } from "./CardActions";
+export { default as CardBody } from "./CardBody";
+export { default as CardBodyText } from "./CardBodyText";
+export { default as CardHeadline } from "./CardHeadline";
diff --git a/src/components/modals/ModalConfirm.jsx b/src/components/modals/ModalConfirm.jsx
index 0863fb9..b427aff 100644
--- a/src/components/modals/ModalConfirm.jsx
+++ b/src/components/modals/ModalConfirm.jsx
@@ -1,6 +1,13 @@
 import React from "react";
 
 import Button from "components/Button";
+import {
+  Card,
+  CardActions,
+  CardBody,
+  CardBodyText,
+  CardHeadline,
+} from "components/cards";
 
 import Modal from "./Modal";
 
@@ -15,19 +22,19 @@ const ModalConfirm = ({
 }) => {
   return (
     <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}>
-      <div className="card elevation-21">
-        <div className="card__body">
+      <Card>
+        <CardBody>
           <div className="flex items-center justify-between mb-4">
-            <h1 className="card-headline">{title}</h1>
+            <CardHeadline>{title}</CardHeadline>
             <button onClick={onCancel}>
               <i className="ico--close"></i>
             </button>
           </div>
-          <p className="card-body-text">{children}</p>
-        </div>
-        <div className="card-actions card-actions--right space-x-1">
+          <CardBodyText>{children}</CardBodyText>
+        </CardBody>
+        <CardActions right className="space-x-1">
           <Button
-            hoveractive
+            hoverActive
             color="blue-300"
             className="text-sm"
             onClick={onConfirm}
@@ -35,15 +42,15 @@ const ModalConfirm = ({
             {yesActionLabel}
           </Button>
           <Button
-            hoveractive
+            hoverActive
             color="red-600"
             className="text-sm"
-            onClick={onConfirm}
+            onClick={onCancel}
           >
             {cancelActionLabel}
           </Button>
-        </div>
-      </div>
+        </CardActions>
+      </Card>
     </Modal>
   );
 };
diff --git a/src/containers/AnnoucementsContainer.jsx b/src/containers/AnnoucementsContainer.jsx
index dc7204d..e3b5322 100644
--- a/src/containers/AnnoucementsContainer.jsx
+++ b/src/containers/AnnoucementsContainer.jsx
@@ -1,25 +1,68 @@
-import React from "react";
+import React, { useCallback, useState } from "react";
 
+import {
+  deleteAnnouncement,
+  updateAnnouncementContent,
+} from "actions/announcements";
+import AnnouncementEditModal from "components/annoucements/AnnouncementEditModal";
 import AnnouncementList from "components/annoucements/AnnouncementList";
+import ModalConfirm from "components/modals/ModalConfirm";
 import { AnnouncementStore } from "stores";
 
 const AnnoucementsContainer = () => {
+  const [itemToDelete, setItemToDelete] = useState(null);
+  const [itemToEdit, setItemToEdit] = useState(null);
   const items = AnnouncementStore.useState((state) => state.items);
 
-  const onEdit = (announcement) => {
-    console.log("edit", announcement);
-  };
-  const onDelete = (announcement) => {
-    console.log("delete", announcement);
-  };
+  const confirmEdit = useCallback(
+    async (newContent) => {
+      if (itemToEdit && newContent) {
+        await updateAnnouncementContent.run(itemToEdit, newContent);
+        setItemToEdit(null);
+      }
+    },
+    [itemToEdit, setItemToEdit]
+  );
+
+  const cancelEdit = useCallback(() => {
+    setItemToEdit(null);
+  }, [setItemToEdit]);
+
+  const confirmDelete = useCallback(async () => {
+    await deleteAnnouncement.run(itemToDelete);
+    setItemToDelete(null);
+  }, [setItemToDelete, itemToDelete]);
+
+  const cancelDelete = useCallback(() => {
+    setItemToDelete(null);
+  }, [setItemToDelete]);
 
   return (
-    <AnnouncementList
-      items={items}
-      displayActions={true}
-      onDelete={onDelete}
-      onEdit={onEdit}
-    />
+    <>
+      <AnnouncementList
+        items={items}
+        displayActions={true}
+        onDelete={setItemToDelete}
+        onEdit={setItemToEdit}
+      />
+      <ModalConfirm
+        isOpen={!!itemToDelete}
+        onConfirm={confirmDelete}
+        onCancel={cancelDelete}
+        title="Opravdu chcete toto oznámení smazat?"
+        yesActionLabel="Smazat"
+      >
+        Opravdu chcete ukončit rozpravu?
+      </ModalConfirm>
+      {itemToEdit && (
+        <AnnouncementEditModal
+          isOpen={true}
+          announcement={itemToEdit}
+          onConfirm={confirmEdit}
+          onCancel={cancelEdit}
+        />
+      )}
+    </>
   );
 };
 
-- 
GitLab