diff --git a/src/actions/announcements.js b/src/actions/announcements.js index 9d43f8333d5aebf845313520e70ac1095f8c2616..738ef521f3b6f448fa05c13dbffbd46ce1b0d3fd 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 24b1adfa20b4ff79fa009e462cb4124f81209304..5d279297869ea2defa12e9279db8388df1ec3b98 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 8f39694e3d187ffabb4cd235994eb7e893668f19..52cac18a8ce4fab19a37f93e5ba2da1e71b01d23 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 2d6c72aa8dd70bdb662be43e8b44627c8309195e..95973cc4ff7fb264ec4c30e33181986fac5b8328 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 9b02224a4f4b91db7ca3fd199885c69f433801b2..52dcff03f0b8745b5a154cef0a4ebb270ee6466e 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 5836b184a3b2cbf72f54c1b0321180fc29b81e82..9c79a5cd7cfdee96e2ba93df8aa505aeed412153 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 d72bd5c5d7dc80142e839805183b62f0a46573dd..ece3e18545edaf27cabac8e9f1d1afc525e2d299 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 16a2f628b9a3ae02a5c12b7d38d08fe128897b0c..c3740ac3cdc37bd46c21f50741539426063a7203 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 19a2093008e6e0ffa57e7a0526cb281ff6f44a48..7816858f5ad826d68e8af52bb64d38b88d826f7b 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 aff0f1bddee9ac1ce96dfc89b9699f5879f38b70..6616a4f5ec9a6f9c9555e21cac12bf78ce4680b9 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",