From 0185c9c9d8dc93e020ca1d4415ae5947f90dddfd Mon Sep 17 00:00:00 2001
From: xaralis <filip.varecha@fragaria.cz>
Date: Thu, 24 Dec 2020 12:58:33 +0100
Subject: [PATCH] feat: edit announcement link, polishing

---
 src/App.jsx                                   |   4 +-
 src/actions/announcements.js                  |  14 +-
 .../annoucements/AnnouncementEditModal.jsx    | 158 ++++++++++++------
 src/components/modals/ModalConfirm.jsx        |   2 +-
 src/components/posts/PostEditModal.jsx        | 102 +++++------
 src/containers/AddAnnouncementForm.jsx        |  49 ++++--
 src/containers/AnnoucementsContainer.jsx      |  16 +-
 src/pages/Home.jsx                            |  12 +-
 src/utils.js                                  |   1 +
 src/ws/handlers/announcements.js              |   3 +
 10 files changed, 220 insertions(+), 141 deletions(-)

diff --git a/src/App.jsx b/src/App.jsx
index bff1d19..3b7de1a 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -65,7 +65,7 @@ const onKeycloakEvent = (event) => {
 const LoadingComponent = (
   <div className="h-screen w-screen flex justify-center items-center">
     <div className="text-center">
-      <div className="flex flex-col md:flex-row items-center space-x-4 text-center mb-2 md:mb-4">
+      <div className="flex flex-col md:flex-row items-center space-x-4 text-center mb-2">
         <img
           src={`${process.env.REACT_APP_STYLEGUIDE_URL}/images/logo-round-black.svg`}
           className="w-16 mb-2"
@@ -73,7 +73,7 @@ const LoadingComponent = (
         />
         <h1 className="head-alt-md md:head-alt-lg">Celostátní fórum 2021</h1>
       </div>
-      <p className="text-center">Načítám aplikaci ...</p>
+      <p className="text-center head-xs md:head-base">Načítám aplikaci ...</p>
     </div>
   </div>
 );
diff --git a/src/actions/announcements.js b/src/actions/announcements.js
index 647d5a6..367b149 100644
--- a/src/actions/announcements.js
+++ b/src/actions/announcements.js
@@ -82,25 +82,23 @@ export const deleteAnnouncement = createAsyncAction(
 );
 
 /**
- * Update content of an announcement.
+ * Update an announcement.
  */
-export const updateAnnouncementContent = createAsyncAction(
+export const updateAnnouncement = createAsyncAction(
   /**
    *
    * @param {CF2021.Announcement} item
-   * @param {string} newContent
+   * @param {Object} payload
    */
-  async ({ item, newContent }) => {
+  async ({ item, payload }) => {
     try {
-      const body = JSON.stringify({
-        content: newContent,
-      });
+      const body = JSON.stringify(payload);
       await fetch(`/announcements/${item.id}`, {
         method: "PUT",
         body,
         expectedStatus: 204,
       });
-      return successResult({ item, newContent });
+      return successResult({ item, payload });
     } catch (err) {
       return errorResult([], err.toString());
     }
diff --git a/src/components/annoucements/AnnouncementEditModal.jsx b/src/components/annoucements/AnnouncementEditModal.jsx
index 69e52f1..795d696 100644
--- a/src/components/annoucements/AnnouncementEditModal.jsx
+++ b/src/components/annoucements/AnnouncementEditModal.jsx
@@ -1,10 +1,12 @@
 import React, { useState } from "react";
+import classNames from "classnames";
 
 import Button from "components/Button";
 import { Card, CardActions, CardBody, CardHeadline } from "components/cards";
 import ErrorMessage from "components/ErrorMessage";
 import MarkdownEditor from "components/mde/MarkdownEditor";
 import Modal from "components/modals/Modal";
+import { urlRegex } from "utils";
 
 const AnnouncementEditModal = ({
   announcement,
@@ -15,6 +17,8 @@ const AnnouncementEditModal = ({
   ...props
 }) => {
   const [text, setText] = useState(announcement.content);
+  const [link, setLink] = useState(announcement.link);
+  const [linkValid, setLinkValid] = useState(null);
   const [noTextError, setNoTextError] = useState(false);
 
   const onTextInput = (newText) => {
@@ -25,64 +29,116 @@ const AnnouncementEditModal = ({
     }
   };
 
+  const onLinkInput = (newLink) => {
+    setLink(newLink);
+
+    if (!!newLink) {
+      setLinkValid(urlRegex.test(newLink));
+    }
+  };
+
   const confirm = (evt) => {
-    if (!!text) {
-      onConfirm(text);
-    } else {
+    evt.preventDefault();
+
+    let preventAction = false;
+    const payload = {
+      content: text,
+    };
+
+    if (!text) {
       setNoTextError(true);
+      preventAction = true;
     }
+
+    if (announcement.type === "voting" && !link) {
+      setLinkValid(false);
+      preventAction = true;
+    } else {
+      payload.link = link;
+    }
+
+    if (preventAction) {
+      return;
+    }
+
+    onConfirm(payload);
   };
 
   return (
     <Modal containerClassName="max-w-lg" 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>
-          <MarkdownEditor
-            value={text}
-            onChange={onTextInput}
-            error={
-              noTextError
-                ? "Před úpravou oznámení nezapomeňte vyplnit jeho obsah."
-                : null
-            }
-            placeholder="Vyplňte text oznámení"
-            toolbarCommands={[
-              ["bold", "italic", "strikethrough"],
-              ["link", "unordered-list", "ordered-list"],
-            ]}
-          />
-          {error && (
-            <ErrorMessage className="mt-2">
-              Při editaci došlo k problému: {error}
-            </ErrorMessage>
-          )}
-        </CardBody>
-        <CardActions right className="space-x-1">
-          <Button
-            hoverActive
-            color="blue-300"
-            className="text-sm"
-            loading={confirming}
-            onClick={confirm}
-          >
-            Uložit
-          </Button>
-          <Button
-            hoverActive
-            color="red-600"
-            className="text-sm"
-            onClick={onCancel}
-          >
-            Zrušit
-          </Button>
-        </CardActions>
-      </Card>
+      <form onSubmit={confirm}>
+        <Card>
+          <CardBody>
+            <div className="flex items-center justify-between mb-4">
+              <CardHeadline>Upravit oznámení</CardHeadline>
+              <button onClick={onCancel} type="button">
+                <i className="ico--close"></i>
+              </button>
+            </div>
+            <MarkdownEditor
+              value={text}
+              onChange={onTextInput}
+              error={
+                noTextError
+                  ? "Před úpravou oznámení nezapomeňte vyplnit jeho obsah."
+                  : null
+              }
+              placeholder="Vyplňte text oznámení"
+              toolbarCommands={[
+                ["bold", "italic", "strikethrough"],
+                ["link", "unordered-list", "ordered-list"],
+              ]}
+            />
+            <div
+              className={classNames("form-field mt-4", {
+                hidden: announcement.type !== "voting",
+                "form-field--error": linkValid === false,
+              })}
+            >
+              <div className="form-field__wrapper form-field__wrapper--shadowed">
+                <input
+                  type="text"
+                  className="text-input text-sm text-input--has-addon-l form-field__control"
+                  value={link}
+                  placeholder="URL hlasování"
+                  onChange={(evt) => onLinkInput(evt.target.value)}
+                />
+                <div className="text-input-addon text-input-addon--l order-first">
+                  <i className="ico--link1"></i>
+                </div>
+              </div>
+              {linkValid === false && (
+                <div className="form-field__error">Zadejte platnou URL.</div>
+              )}
+            </div>
+            {error && (
+              <ErrorMessage className="mt-2">
+                Při editaci došlo k problému: {error}
+              </ErrorMessage>
+            )}
+          </CardBody>
+          <CardActions right className="space-x-1">
+            <Button
+              hoverActive
+              color="blue-300"
+              className="text-sm"
+              loading={confirming}
+              type="submit"
+            >
+              Uložit
+            </Button>
+            <Button
+              hoverActive
+              color="red-600"
+              className="text-sm"
+              onClick={onCancel}
+              type="button"
+            >
+              Zrušit
+            </Button>
+          </CardActions>
+        </Card>
+      </form>
     </Modal>
   );
 };
diff --git a/src/components/modals/ModalConfirm.jsx b/src/components/modals/ModalConfirm.jsx
index 8efbbec..52cf87a 100644
--- a/src/components/modals/ModalConfirm.jsx
+++ b/src/components/modals/ModalConfirm.jsx
@@ -29,7 +29,7 @@ const ModalConfirm = ({
         <CardBody>
           <div className="flex items-center justify-between mb-4">
             <CardHeadline>{title}</CardHeadline>
-            <button onClick={onCancel}>
+            <button onClick={onCancel} type="button">
               <i className="ico--close"></i>
             </button>
           </div>
diff --git a/src/components/posts/PostEditModal.jsx b/src/components/posts/PostEditModal.jsx
index 5853121..edbae53 100644
--- a/src/components/posts/PostEditModal.jsx
+++ b/src/components/posts/PostEditModal.jsx
@@ -26,6 +26,8 @@ const PostEditModal = ({
   };
 
   const confirm = (evt) => {
+    evt.preventDefault();
+
     if (!!text) {
       onConfirm(text);
     } else {
@@ -35,55 +37,57 @@ const PostEditModal = ({
 
   return (
     <Modal containerClassName="max-w-xl" onRequestClose={onCancel} {...props}>
-      <Card>
-        <CardBody>
-          <div className="flex items-center justify-between mb-4">
-            <CardHeadline>Upravit text příspěvku</CardHeadline>
-            <button onClick={onCancel}>
-              <i className="ico--close"></i>
-            </button>
-          </div>
-          <MarkdownEditor
-            value={text}
-            onChange={onTextInput}
-            error={
-              noTextError
-                ? "Před upravením příspěvku nezapomeňte vyplnit jeho obsah."
-                : null
-            }
-            placeholder="Vyplňte text příspěvku"
-            toolbarCommands={[
-              ["header", "bold", "italic", "strikethrough"],
-              ["link", "quote", "image"],
-              ["unordered-list", "ordered-list"],
-            ]}
-          />
-          {error && (
-            <ErrorMessage className="mt-2">
-              Při editaci došlo k problému: {error}
-            </ErrorMessage>
-          )}
-        </CardBody>
-        <CardActions right className="space-x-1">
-          <Button
-            hoverActive
-            color="blue-300"
-            className="text-sm"
-            loading={confirming}
-            onClick={confirm}
-          >
-            Uložit
-          </Button>
-          <Button
-            hoverActive
-            color="red-600"
-            className="text-sm"
-            onClick={onCancel}
-          >
-            Zrušit
-          </Button>
-        </CardActions>
-      </Card>
+      <form onSubmit={confirm}>
+        <Card>
+          <CardBody>
+            <div className="flex items-center justify-between mb-4">
+              <CardHeadline>Upravit text příspěvku</CardHeadline>
+              <button onClick={onCancel} type="button">
+                <i className="ico--close"></i>
+              </button>
+            </div>
+            <MarkdownEditor
+              value={text}
+              onChange={onTextInput}
+              error={
+                noTextError
+                  ? "Před upravením příspěvku nezapomeňte vyplnit jeho obsah."
+                  : null
+              }
+              placeholder="Vyplňte text příspěvku"
+              toolbarCommands={[
+                ["header", "bold", "italic", "strikethrough"],
+                ["link", "quote", "image"],
+                ["unordered-list", "ordered-list"],
+              ]}
+            />
+            {error && (
+              <ErrorMessage className="mt-2">
+                Při editaci došlo k problému: {error}
+              </ErrorMessage>
+            )}
+          </CardBody>
+          <CardActions right className="space-x-1">
+            <Button
+              hoverActive
+              color="blue-300"
+              className="text-sm"
+              loading={confirming}
+              onClick={confirm}
+            >
+              Uložit
+            </Button>
+            <Button
+              hoverActive
+              color="red-600"
+              className="text-sm"
+              onClick={onCancel}
+            >
+              Zrušit
+            </Button>
+          </CardActions>
+        </Card>
+      </form>
     </Modal>
   );
 };
diff --git a/src/containers/AddAnnouncementForm.jsx b/src/containers/AddAnnouncementForm.jsx
index 195ef9a..3f96457 100644
--- a/src/containers/AddAnnouncementForm.jsx
+++ b/src/containers/AddAnnouncementForm.jsx
@@ -6,8 +6,7 @@ import Button from "components/Button";
 import ErrorMessage from "components/ErrorMessage";
 import MarkdownEditor from "components/mde/MarkdownEditor";
 import { useActionState } from "hooks";
-
-const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
+import { urlRegex } from "utils";
 
 const AddAnnouncementForm = ({ className }) => {
   const [text, setText] = useState("");
@@ -30,30 +29,44 @@ const AddAnnouncementForm = ({ className }) => {
     }
   };
 
-  const onLinkInput = (evt) => {
-    setLink(evt.target.value);
+  const onLinkInput = (newLink) => {
+    setLink(newLink);
 
-    if (!!evt.target.value) {
-      setLinkValid(!!evt.target.value.match(urlRegex));
+    if (!!newLink) {
+      setLinkValid(urlRegex.test(newLink));
     }
   };
 
   const onAdd = async (evt) => {
-    if (!link) {
+    let preventAction = false;
+    const payload = {
+      content: text,
+      type,
+    };
+
+    if (!text) {
+      setNoTextError(true);
+      preventAction = true;
+    }
+
+    if (type === "voting" && !link) {
       setLinkValid(false);
+      preventAction = true;
+    } else {
+      payload.link = link;
     }
 
-    if (!!text) {
-      if (type === "voting" && link && linkValid) {
-        const result = await addAnnouncement.run({ content: text, link, type });
+    if (preventAction) {
+      return;
+    }
 
-        if (!result.error) {
-          setText("");
-          setLink("");
-        }
-      }
-    } else {
-      setNoTextError(true);
+    const result = await addAnnouncement.run({ content: text, link, type });
+
+    if (!result.error) {
+      setText("");
+      setLink("");
+      setNoTextError(false);
+      setLinkValid(null);
     }
   };
 
@@ -119,7 +132,7 @@ const AddAnnouncementForm = ({ className }) => {
               className="text-input text-sm text-input--has-addon-l form-field__control"
               value={link}
               placeholder="URL hlasování"
-              onChange={onLinkInput}
+              onChange={(evt) => onLinkInput(evt.target.value)}
             />
             <div className="text-input-addon text-input-addon--l order-first">
               <i className="ico--link1"></i>
diff --git a/src/containers/AnnoucementsContainer.jsx b/src/containers/AnnoucementsContainer.jsx
index 048e946..2e3de91 100644
--- a/src/containers/AnnoucementsContainer.jsx
+++ b/src/containers/AnnoucementsContainer.jsx
@@ -4,7 +4,7 @@ import {
   deleteAnnouncement,
   loadAnnouncements,
   markSeen,
-  updateAnnouncementContent,
+  updateAnnouncement,
 } from "actions/announcements";
 import AnnouncementEditModal from "components/annoucements/AnnouncementEditModal";
 import AnnouncementList from "components/annoucements/AnnouncementList";
@@ -14,7 +14,7 @@ import ModalConfirm from "components/modals/ModalConfirm";
 import { useActionState, useItemActionConfirm } from "hooks";
 import { AnnouncementStore, AuthStore } from "stores";
 
-const AnnoucementsContainer = () => {
+const AnnoucementsContainer = ({ className }) => {
   const [itemToEdit, setItemToEdit] = useState(null);
   const [confirmingEdit, setConfirmingEdit] = useState(false);
   const [editError, setEditError] = useState(null);
@@ -38,13 +38,13 @@ const AnnoucementsContainer = () => {
   );
 
   const confirmEdit = useCallback(
-    async (newContent) => {
-      if (itemToEdit && newContent) {
+    async (payload) => {
+      if (itemToEdit && payload) {
         setConfirmingEdit(true);
 
-        const result = await updateAnnouncementContent.run({
+        const result = await updateAnnouncement.run({
           item: itemToEdit,
-          newContent,
+          payload,
         });
 
         if (!result.error) {
@@ -65,7 +65,7 @@ const AnnoucementsContainer = () => {
   }, [setItemToEdit]);
 
   return (
-    <>
+    <div className={className}>
       {loadResult && loadResult.error && (
         <CardBody>
           <ErrorMessage>
@@ -101,7 +101,7 @@ const AnnoucementsContainer = () => {
           error={editError}
         />
       )}
-    </>
+    </div>
   );
 };
 
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
index 415224d..a8794ff 100644
--- a/src/pages/Home.jsx
+++ b/src/pages/Home.jsx
@@ -156,11 +156,15 @@ const Home = () => {
       <article className="container container--wide pt-8 lg:py-24 cf2021">
         <section className="cf2021__video">
           <div className="flex justify-between mb-4 lg:mb-8">
-            <h1 className="head-alt-md lg:head-alt-lg">
+            <h1 className="head-alt-base lg:head-alt-lg">
               Bod č. {programEntry.number}: {programEntry.title}
             </h1>
             {displayActions && (
-              <DropdownMenu right triggerSize="lg" className="pl-4 pt-5">
+              <DropdownMenu
+                right
+                triggerSize="lg"
+                className="pl-4 pt-1 lg:pt-5"
+              >
                 <DropdownMenuItem
                   onClick={() => setShowProgramEditModal(true)}
                   icon="ico--edit-pencil"
@@ -224,7 +228,7 @@ const Home = () => {
         <section className="cf2021__notifications">
           <div className="lg:card lg:elevation-10">
             <div className="lg:card__body pb-2 lg:py-6">
-              <h2 className="head-heavy-sm">Oznámení</h2>
+              <h2 className="head-heavy-xs md:head-heavy-sm">Oznámení</h2>
             </div>
 
             <AnnouncementsContainer className="container-padding--zero lg:container-padding--auto" />
@@ -236,7 +240,7 @@ const Home = () => {
 
         <section className="cf2021__posts">
           <div className="flex flex-col xl:flex-row xl:justify-between xl:items-center mb-4">
-            <h2 className="head-heavy-sm whitespace-no-wrap">
+            <h2 className="head-heavy-xs md:head-heavy-sm whitespace-no-wrap">
               <span>Příspěvky v rozpravě</span>
               {!programEntry.discussionOpened && (
                 <i
diff --git a/src/utils.js b/src/utils.js
index e21427d..2ff66b9 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -7,6 +7,7 @@ import WaitQueue from "wait-queue";
 
 import { markdownConverter } from "markdown";
 
+export const urlRegex = /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i;
 export const seenPostsLSKey = "cf2021_seen_posts";
 export const seenAnnouncementsLSKey = "cf2021_seen_announcements";
 
diff --git a/src/ws/handlers/announcements.js b/src/ws/handlers/announcements.js
index b49673f..1519be0 100644
--- a/src/ws/handlers/announcements.js
+++ b/src/ws/handlers/announcements.js
@@ -9,6 +9,9 @@ export const handleAnnouncementChanged = (payload) => {
       if (has(payload, "content")) {
         state.items[payload.id].content = payload.content;
       }
+      if (has(payload, "link")) {
+        state.items[payload.id].link = payload.link;
+      }
     }
   });
 };
-- 
GitLab