From 2fa6c73edcfba93e1f62fee4a7da5091d8f37c68 Mon Sep 17 00:00:00 2001
From: xaralis <filip.varecha@fragaria.cz>
Date: Sun, 20 Dec 2020 19:30:55 +0100
Subject: [PATCH] feat: announcements with link, better thumb style, fix modals
 on mobile, break/finished/not yet started screens

---
 src/actions/announcements.js                 | 27 +++---
 src/actions/posts.js                         |  2 +-
 src/components/Thumbs.jsx                    | 10 +-
 src/components/annoucements/Announcement.jsx | 13 ++-
 src/components/modals/Modal.jsx              |  9 +-
 src/components/posts/Post.jsx                | 11 ++-
 src/containers/AddAnnouncementForm.jsx       | 92 +++++++++++++++---
 src/pages/Home.jsx                           | 99 ++++++++++++++++++--
 src/pages/Program.jsx                        |  4 +-
 src/stores.js                                |  2 +-
 10 files changed, 221 insertions(+), 48 deletions(-)

diff --git a/src/actions/announcements.js b/src/actions/announcements.js
index 9d43f83..738ef52 100644
--- a/src/actions/announcements.js
+++ b/src/actions/announcements.js
@@ -37,19 +37,22 @@ export const loadAnnouncements = createAsyncAction(
 /**
  * Add new announcement.
  */
-export const addAnnouncement = createAsyncAction(async ({ content }) => {
-  try {
-    const body = JSON.stringify({
-      content,
-      type: announcementTypeMappingRev["announcement"],
-    });
-    const resp = await fetch("/announcements", { method: "POST", body });
-    const data = await resp.json();
-    return successResult(data.data);
-  } catch (err) {
-    return errorResult([], err.toString());
+export const addAnnouncement = createAsyncAction(
+  async ({ content, link, type }) => {
+    try {
+      const body = JSON.stringify({
+        content,
+        link,
+        type: announcementTypeMappingRev[type],
+      });
+      const resp = await fetch("/announcements", { method: "POST", body });
+      const data = await resp.json();
+      return successResult(data.data);
+    } catch (err) {
+      return errorResult([], err.toString());
+    }
   }
-});
+);
 
 /**
  * Delete existing announcement.
diff --git a/src/actions/posts.js b/src/actions/posts.js
index 24b1adf..5d27929 100644
--- a/src/actions/posts.js
+++ b/src/actions/posts.js
@@ -35,7 +35,7 @@ export const loadPosts = createAsyncAction(
             items: filteredPosts.map(property("id")),
             itemCount: filteredPosts.length,
             page: 1,
-            perPage: 5,
+            perPage: 20,
           };
         });
       }
diff --git a/src/components/Thumbs.jsx b/src/components/Thumbs.jsx
index 8f39694..52cac18 100644
--- a/src/components/Thumbs.jsx
+++ b/src/components/Thumbs.jsx
@@ -1,14 +1,16 @@
 import React from "react";
 import classNames from "classnames";
 
-const Thumbs = ({ likes, dislikes, onLike, onDislike, readOnly }) => {
+const Thumbs = ({ likes, dislikes, myVote, onLike, onDislike, readOnly }) => {
   return (
     <div>
       <div className="space-x-2 text-sm flex items-center">
         <button
-          className={classNames("text-blue-300 flex items-center space-x-1", {
+          className={classNames("flex items-center space-x-1", {
             "cursor-pointer": !readOnly,
             "cursor-default": readOnly,
+            "text-blue-300": myVote === "like",
+            "text-grey-200 hover:text-blue-300": myVote !== "like",
           })}
           disabled={readOnly}
           onClick={onLike}
@@ -17,9 +19,11 @@ const Thumbs = ({ likes, dislikes, onLike, onDislike, readOnly }) => {
           <i className="ico--thumbs-up"></i>
         </button>
         <button
-          className={classNames("text-red-600 flex items-center space-x-1", {
+          className={classNames("flex items-center space-x-1", {
             "cursor-pointer": !readOnly,
             "cursor-default": readOnly,
+            "text-red-600": myVote === "dislike",
+            "text-grey-200 hover:text-red-600": myVote !== "dislike",
           })}
           disabled={readOnly}
           onClick={onDislike}
diff --git a/src/components/annoucements/Announcement.jsx b/src/components/annoucements/Announcement.jsx
index 2d6c72a..95973cc 100644
--- a/src/components/annoucements/Announcement.jsx
+++ b/src/components/annoucements/Announcement.jsx
@@ -67,7 +67,7 @@ const Announcement = ({
   }[type];
 
   const linkLabel =
-    type === "voting" ? "Hlasovat v heliosu" : "Zobrazit související příspěvek";
+    type === "voting" ? "Hlasovat" : "Zobrazit související příspěvek";
 
   const showEdit = [
     "suggested-procedure-proposal",
@@ -83,7 +83,16 @@ const Announcement = ({
           <Chip color={chipColor} condensed>
             {chipLabel}
           </Chip>
-          {link && <a href={link}>{linkLabel + "»"}</a>}
+          {link && (
+            <a
+              href={link}
+              className={classNames("text-xs font-bold text-" + chipColor)}
+              target="_blank"
+              rel="noopener noreferrer"
+            >
+              {linkLabel + " »"}
+            </a>
+          )}
         </div>
         {canRunActions && (
           <DropdownMenu right triggerIconClass="ico--dots-three-horizontal">
diff --git a/src/components/modals/Modal.jsx b/src/components/modals/Modal.jsx
index 9b02224..52dcff0 100644
--- a/src/components/modals/Modal.jsx
+++ b/src/components/modals/Modal.jsx
@@ -9,8 +9,13 @@ const CustomModal = ({ children, containerClassName, ...props }) => (
     className="modal__content"
     {...props}
   >
-    <div className={classNames("modal__container w-full", containerClassName)}>
-      <div className="modal__container-body">{children}</div>
+    <div
+      className={classNames(
+        "modal__container w-full flex items-center justify-center",
+        containerClassName
+      )}
+    >
+      <div className="modal__container-body w-full">{children}</div>
     </div>
   </Modal>
 );
diff --git a/src/components/posts/Post.jsx b/src/components/posts/Post.jsx
index 5836b18..9c79a5c 100644
--- a/src/components/posts/Post.jsx
+++ b/src/components/posts/Post.jsx
@@ -144,19 +144,19 @@ const Post = ({
             <div className="flex flex-col xl:flex-row xl:items-center">
               <div className="flex flex-col xl:flex-row xl:items-center">
                 <span className="font-bold">{author.name}</span>
-                <div className="mt-1 lg:mt-0 lg:ml-2">
+                <div className="mt-1 xl:mt-0 xl:ml-2 leading-tight">
                   <span className="text-grey-200 text-sm">{author.group}</span>
                   <span className="text-grey-200 ml-1 text-sm">
                     @ {format(datetime, "H:mm")}
                     {modified && (
-                      <span className="text-grey-200 text-xs ml-2 underline">
-                        (Upraveno přesdedajícím)
+                      <span className="text-grey-200 text-sm block md:inline md:ml-2 underline">
+                        (upraveno)
                       </span>
                     )}
                   </span>
                 </div>
               </div>
-              <div className="flex flex-row flex-wrap lg:flex-no-wrap lg:items-center mt-1 xl:mt-0 xl:ml-2 space-x-2">
+              <div className="hidden lg:flex flex-row flex-wrap lg:flex-no-wrap lg:items-center mt-1 xl:mt-0 xl:ml-2 space-x-2">
                 {labels}
               </div>
             </div>
@@ -218,6 +218,9 @@ const Post = ({
             </div>
           </div>
         </div>
+        <div className="flex lg:hidden flex-row flex-wrap my-2 space-x-2">
+          {labels}
+        </div>
         <p className="text-sm lg:text-base text-black leading-normal">
           {content}
         </p>
diff --git a/src/containers/AddAnnouncementForm.jsx b/src/containers/AddAnnouncementForm.jsx
index d72bd5c..ece3e18 100644
--- a/src/containers/AddAnnouncementForm.jsx
+++ b/src/containers/AddAnnouncementForm.jsx
@@ -1,42 +1,108 @@
 import React, { useState } from "react";
+import classNames from "classnames";
 
 import { addAnnouncement } from "actions/announcements";
 import Button from "components/Button";
 
+const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
+
 const AddAnnouncementForm = ({ className }) => {
   const [text, setText] = useState("");
+  const [link, setLink] = useState("");
+  const [linkValid, setLinkValid] = useState(false);
+  const [type, setType] = useState("announcement");
 
   const onTextInput = (evt) => {
     setText(evt.target.value);
   };
 
+  const onLinkInput = (evt) => {
+    setLink(evt.target.value);
+
+    if (!!evt.target.value) {
+      setLinkValid(evt.target.value.match(urlRegex));
+    }
+  };
+
   const onAdd = (evt) => {
     if (!!text) {
-      addAnnouncement.run({ content: text });
+      addAnnouncement.run({ content: text, link, type });
       setText("");
+      setLink("");
     }
   };
 
   return (
     <div className={className}>
-      <div className="form-field">
-        <div className="form-field__wrapper form-field__wrapper--shadowed">
-          <textarea
-            className="text-input form-field__control "
-            value={text}
-            rows="3"
-            cols="40"
-            placeholder="Vyplňte text oznámení"
-            onChange={onTextInput}
-          ></textarea>
+      <div className="grid grid-cols-1 gap-4">
+        <div
+          className="form-field"
+          onChange={(evt) => setType(evt.target.value)}
+        >
+          <div className="form-field__wrapper text-sm">
+            <div className="radio form-field__control">
+              <label>
+                <input
+                  type="radio"
+                  name="type"
+                  value="announcement"
+                  defaultChecked
+                />
+                <span>Oznámení</span>
+              </label>
+            </div>
+
+            <div className="radio form-field__control">
+              <label>
+                <input type="radio" name="type" value="voting" />
+                <span>Rozhodující hlasování</span>
+              </label>
+            </div>
+          </div>
+        </div>
+
+        <div className="form-field">
+          <div className="form-field__wrapper form-field__wrapper--shadowed">
+            <textarea
+              className="text-input text-sm form-field__control "
+              value={text}
+              rows="3"
+              cols="40"
+              placeholder="Vyplňte text oznámení"
+              onChange={onTextInput}
+            ></textarea>
+          </div>
+        </div>
+
+        <div
+          className={classNames("form-field", {
+            hidden: type !== "voting",
+            "form-field--error": !!link && !linkValid,
+          })}
+        >
+          <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={onLinkInput}
+            />
+            <div className="text-input-addon text-input-addon--l order-first">
+              <i className="ico--link1"></i>
+            </div>
+          </div>
+          {!!link && !linkValid && (
+            <div className="form-field__error">Zadejte platnou URL.</div>
+          )}
         </div>
       </div>
 
       <Button
         onClick={onAdd}
-        className="text-sm mt-2"
+        className="text-sm mt-4"
         hoverActive
-        disabled={!text}
+        disabled={!text || (type === "voting" && !linkValid)}
       >
         Přidat oznámení
       </Button>
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
index 16a2f62..c3740ac 100644
--- a/src/pages/Home.jsx
+++ b/src/pages/Home.jsx
@@ -1,4 +1,5 @@
 import React, { useState } from "react";
+import { format } from "date-fns";
 
 import {
   closeDiscussion,
@@ -18,8 +19,8 @@ import PostsContainer from "containers/PostsContainer";
 import { useActionConfirm } from "hooks";
 import { AuthStore, ProgramStore } from "stores";
 
-const noprogramEntryDiscussion = (
-  <article className="container container--wide pt-8 py-8 lg:py-32">
+const NotYetStarted = ({ startAt }) => (
+  <article className="container container--wide py-8 md:py-16 lg:py-32">
     <div className="hidden md:inline-block flag bg-violet-400 text-white head-alt-base mb-4 py-4 px-5">
       Jejda ...
     </div>
@@ -27,8 +28,14 @@ const noprogramEntryDiscussion = (
       Jednání ještě nebylo zahájeno :(
     </h1>
     <p className="text-xl leading-snug mb-8">
-      Jednání celostátního fóra v tuto chvíli neprobíhá. Můžete si ale zobrazit
-      program.
+      <span>Jednání celostátního fóra ještě nezačalo. </span>
+      {startAt && (
+        <span>
+          Mělo by být zahájeno <strong>{format(startAt, "d. M. Y")}</strong> v{" "}
+          <strong>{format(startAt, "H:mm")}</strong>.{" "}
+        </span>
+      )}
+      <span>Můžete si ale zobrazit program.</span>
     </p>
     <Button routerTo="/program" className="md:text-lg lg:text-xl" hoverActive>
       Zobrazit program
@@ -36,10 +43,58 @@ const noprogramEntryDiscussion = (
   </article>
 );
 
+const AlreadyFinished = () => (
+  <article className="container container--wide py-8 md:py-16 lg:py-32">
+    <div className="flex">
+      <div>
+        <i className="ico--anchor text-2xl md:text-6xl lg:text-9xl mr-4 lg:mr-8"></i>
+      </div>
+      <div>
+        <h1 className="head-alt-base md:head-alt-md lg:head-alt-xl mb-2">
+          Jednání už skočilo!
+        </h1>
+        <p className="text-xl leading-snug">
+          Oficiální program již skončil. Těšíme se na viděnou zase příště.
+        </p>
+      </div>
+    </div>
+  </article>
+);
+
+const BreakInProgress = () => (
+  <article className="container container--wide py-8 md:py-16 lg:py-32">
+    <div className="flex">
+      <div>
+        <i className="ico--clock text-2xl md:text-6xl lg:text-9xl mr-4 lg:mr-8"></i>
+      </div>
+      <div>
+        <h1 className="head-alt-base md:head-alt-md lg:head-alt-xl mb-2">
+          Probíhá přestávka ...
+        </h1>
+        <p className="text-xl leading-snug mb-8">
+          Jednání celostátního fóra je momentálně přerušeno. Můžete si ale
+          zobrazit program.
+        </p>
+        <Button
+          routerTo="/program"
+          className="md:text-lg lg:text-xl"
+          hoverActive
+        >
+          Zobrazit program
+        </Button>
+      </div>
+    </div>
+  </article>
+);
+
 const Home = () => {
-  const { currentId, items } = ProgramStore.useState();
+  const {
+    currentId,
+    items: programEntries,
+    scheduleIds,
+  } = ProgramStore.useState();
   const { isAuthenticated, user } = AuthStore.useState();
-  const programEntry = currentId ? items[currentId] : null;
+  const programEntry = currentId ? programEntries[currentId] : null;
   const [showProgramEditModal, setShowProgramEditModal] = useState(false);
   const [
     showCloseDiscussion,
@@ -68,8 +123,24 @@ const Home = () => {
     setShowProgramEditModal(false);
   };
 
+  const firstProgramEntry = scheduleIds.length
+    ? programEntries[scheduleIds[0]]
+    : null;
+
+  const lastProgramEntry = scheduleIds.length
+    ? programEntries[scheduleIds[0]]
+    : null;
+
+  if (!programEntry && new Date() < firstProgramEntry.expectedStartAt) {
+    return <NotYetStarted startAt={firstProgramEntry.expectedStartAt} />;
+  }
+
+  if (!programEntry && new Date() > lastProgramEntry.expectedStartAt) {
+    return <AlreadyFinished />;
+  }
+
   if (!programEntry) {
-    return noprogramEntryDiscussion;
+    return <BreakInProgress />;
   }
 
   return (
@@ -144,7 +215,19 @@ 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">
-              Příspěvky v rozpravě
+              <span>Příspěvky v rozpravě</span>
+              {!programEntry.discussionOpened && (
+                <i
+                  className="ico--lock text-black ml-1 opacity-50 hover:opacity-100 transition duration-500 text-xl"
+                  title="Rozprava je uzavřena"
+                />
+              )}
+              {programEntry.discussionOpened && (
+                <i
+                  className="ico--lock-open text-black ml-1 opacity-50 hover:opacity-100 transition duration-500 text-xl"
+                  title="Probíhá rozprava"
+                />
+              )}
             </h2>
             <PostFilters />
           </div>
diff --git a/src/pages/Program.jsx b/src/pages/Program.jsx
index 19a2093..7816858 100644
--- a/src/pages/Program.jsx
+++ b/src/pages/Program.jsx
@@ -49,8 +49,8 @@ const Schedule = () => {
                 <p className="head-heavy-xs md:head-heavy-base">
                   {format(entry.expectedStartAt, "H:mm")}
                 </p>
-                <p className="ml-auto md:ml-0 head-heavy-xs md:head-heavy-xs md:text-grey-200">
-                  {format(entry.expectedStartAt, "d.M.Y")}
+                <p className="ml-auto md:ml-0 head-heavy-xs md:head-heavy-xs md:text-grey-200 whitespace-no-wrap">
+                  {format(entry.expectedStartAt, "d. M. Y")}
                 </p>
               </div>
               <div className="flex-grow w-full">
diff --git a/src/stores.js b/src/stores.js
index aff0f1b..6616a4f 100644
--- a/src/stores.js
+++ b/src/stores.js
@@ -33,7 +33,7 @@ const postStoreInitial = {
     items: [],
     itemCount: 0,
     page: 1,
-    perPage: 5,
+    perPage: 20,
   },
   filters: {
     flags: "all",
-- 
GitLab