diff --git a/src/App.jsx b/src/App.jsx index bff1d190fc80135307f21cea72df02e17eab75c3..3b7de1a50b93da18556e26b784f816459c73bcf4 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 647d5a67dcc59ed34a228a9f0611d1d518f9f6dc..367b1493bacc80dfcbb66056aebf78a435e54012 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 69e52f1334dfb4c2f1c7b302d3b4c33ece7a5989..795d696d104f4e081be75f38e6387ba9418c8f97 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 8efbbeca96411f6b520072813b5e1eaf8d936be1..52cf87a3eed5453383ec5b25ea1f7f93ada600dc 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 585312114df181b78cd900f7e887cbccfce6d74e..edbae531b246e0ccb829dc17f3ed90a13a851470 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 195ef9a64627cfba465ebd86ca87da6f70286fc1..3f964577056a48705f6d0cde617b311ebfc30f30 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 048e9460934b1a8eefd6005105c446e97229d75b..2e3de91c9c5dda03da08556937aa8c69c5bc3d95 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 415224dec7db6f0a1a40c298ed7c9b530fbe79d0..a8794ff0bb87e276708d9a0f04fc9887dc21eafa 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 e21427d136836062f7f0cfdd00f834012b2839a2..2ff66b90f842649e6f7590666a29a0f535b5a00a 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 b49673f88b0656afcff79f1bae3c75d77d7efc4c..1519be00007b4db4244f971dca0d443b0e7b0a03 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; + } } }); };