diff --git a/package-lock.json b/package-lock.json index 235d29b41ad9435dfd95a134a74cb18b5044993a..c92643b4690815096e81d7d8eef22a0db3302513 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11350,9 +11350,9 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "react-modal": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.11.2.tgz", - "integrity": "sha512-o8gvvCOFaG1T7W6JUvsYjRjMVToLZgLIsi5kdhFIQCtHxDkA47LznX62j+l6YQkpXDbvQegsDyxe/+JJsFQN7w==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.12.1.tgz", + "integrity": "sha512-WGuXn7Fq31PbFJwtWmOk+jFtGC7E9tJVbFX0lts8ZoS5EPi9+WWylUJWLKKVm3H4GlQ7ZxY7R6tLlbSIBQ5oZA==", "requires": { "exenv": "^1.2.0", "prop-types": "^15.5.10", diff --git a/package.json b/package.json index 114be49def5ade904c527d80748a059d17164ab7..5fc7b32161b77b63f2fb421876a9821ab7346c8c 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "react": "^16.13.1", "react-device-detect": "^1.13.1", "react-dom": "^16.13.1", - "react-modal": "^3.11.2", + "react-modal": "^3.12.1", "react-router-dom": "^5.2.0", "react-scripts": "3.4.3" }, diff --git a/src/components/Button.jsx b/src/components/Button.jsx index d1af51ff52781b177ccc1ec9ba6cff6dd059811a..612654057a5162f37a661e5c27683de81d678b3f 100644 --- a/src/components/Button.jsx +++ b/src/components/Button.jsx @@ -5,6 +5,7 @@ const Button = ({ className, iconWrapperClassName, icon, + color = "black", iconChildren = null, hoverActive = true, fullwidth = false, @@ -13,6 +14,7 @@ const Button = ({ }) => { const btnClass = classNames( "btn", + `btn--${color}`, { "btn--icon": !!icon, "btn--hoveractive": hoverActive, diff --git a/src/components/annoucements/Announcement.jsx b/src/components/annoucements/Announcement.jsx index 392f7da5a512c1810d3c3eb80e13f2790d0c976d..0ccea113875eb69f80915084de54967515669b3d 100644 --- a/src/components/annoucements/Announcement.jsx +++ b/src/components/annoucements/Announcement.jsx @@ -3,6 +3,7 @@ import classNames from "classnames"; import { format } from "date-fns"; import Chip from "components/Chip"; +import { DropdownMenu, DropdownMenuItem } from "components/dropdown-menu"; const Announcement = ({ className, @@ -12,6 +13,9 @@ const Announcement = ({ link, relatedPostId, seen, + displayActions = false, + onDelete, + onEdit, }) => { const wrapperClassName = classNames( "bg-opacity-50 border-l-2 px-4 py-2 lg:px-8 lg:py-3", @@ -49,6 +53,12 @@ const Announcement = ({ const linkLabel = type === "voting" ? "Hlasovat v heliosu" : "Zobrazit související příspěvek"; + const showEdit = [ + "suggested-procedure-proposal", + "voting", + "announcement", + ].includes(type); + return ( <div className={wrapperClassName}> <div className="flex items-center justify-between mb-2"> @@ -59,6 +69,22 @@ const Announcement = ({ </Chip> {link && <a href={link}>{linkLabel + "»"}</a>} </div> + {displayActions && ( + <DropdownMenu right triggerIconClass="ico--dots-three-horizontal"> + {showEdit && ( + <DropdownMenuItem + onClick={onEdit} + icon="ico--edit-pencil" + title="Upravit" + /> + )} + <DropdownMenuItem + onClick={onDelete} + icon="ico--trashcan" + title="Smazat" + /> + </DropdownMenu> + )} </div> <span className="leading-tight text-sm lg:text-base">{content}</span> </div> diff --git a/src/components/annoucements/AnnouncementList.jsx b/src/components/annoucements/AnnouncementList.jsx index 7698f6c749bcfa273becc72be9f9c12307ec3833..b681cfb830c9678b69f5bc4b5af129d999bbf983 100644 --- a/src/components/annoucements/AnnouncementList.jsx +++ b/src/components/annoucements/AnnouncementList.jsx @@ -3,7 +3,21 @@ import classNames from "classnames"; import Announcement from "./Announcement"; -const AnnouncementList = ({ items, className }) => { +const AnnouncementList = ({ + items, + className, + displayActions, + onDelete, + onEdit, +}) => { + const buildHandler = (responderFn) => (post) => (evt) => { + evt.preventDefault(); + responderFn(post); + }; + + const onAnnouncementEdit = buildHandler(onEdit); + const onAnnouncementDelete = buildHandler(onDelete); + return ( <div className={classNames("space-y-px", className)}> {items.map((item) => ( @@ -14,6 +28,9 @@ const AnnouncementList = ({ items, className }) => { content={item.content} link={item.link} seen={item.seen} + displayActions={displayActions} + onEdit={onAnnouncementEdit(item)} + onDelete={onAnnouncementDelete(item)} /> ))} </div> diff --git a/src/components/dropdown-menu/DropdownMenu.jsx b/src/components/dropdown-menu/DropdownMenu.jsx new file mode 100644 index 0000000000000000000000000000000000000000..682459a004d9423da16834ef0effa9d6a2b01b71 --- /dev/null +++ b/src/components/dropdown-menu/DropdownMenu.jsx @@ -0,0 +1,33 @@ +import React from "react"; +import classNames from "classnames"; + +const DropdownMenu = ({ + children, + className, + right, + triggerSize = "sm", + triggerIconClass = "ico--dots-three-vertical", +}) => { + const wrapperCls = classNames( + "dropdown", + { + "dropdown--right": !!right, + }, + className + ); + + const triggerCls = classNames( + "cursor-pointer ml-auto text-grey-200 hover:text-black", + `text-${triggerSize}`, + triggerIconClass + ); + + return ( + <div className={wrapperCls}> + <i className={triggerCls}></i> + <ul className="dropdown__content whitespace-no-wrap">{children}</ul> + </div> + ); +}; + +export default React.memo(DropdownMenu); diff --git a/src/components/dropdown-menu/DropdownMenuItem.jsx b/src/components/dropdown-menu/DropdownMenuItem.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f2fbf385113afd34fbd587ebe20891b79bf76f84 --- /dev/null +++ b/src/components/dropdown-menu/DropdownMenuItem.jsx @@ -0,0 +1,30 @@ +import React from "react"; +import classNames from "classnames"; + +const DropdownMenuItem = ({ + icon, + className, + onClick, + title, + titleClass, + iconSize = "2xs", + titleSize = "xs", +}) => { + const iconCls = classNames("text-2xs mr-2", `text-${iconSize}`, icon); + const titleCls = classNames(`text-${titleSize}`, titleClass); + const cls = classNames( + "dropdown__content-item bg-white hover:bg-grey-125 cursor-pointer", + className + ); + + return ( + <li className={cls} onClick={onClick}> + <span className="block px-3 py-3"> + <i className={iconCls}></i> + <span className={titleCls}>{title}</span> + </span> + </li> + ); +}; + +export default React.memo(DropdownMenuItem); diff --git a/src/components/dropdown-menu/index.js b/src/components/dropdown-menu/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1bff064197d7febb4de4c42b7abef70ab852f308 --- /dev/null +++ b/src/components/dropdown-menu/index.js @@ -0,0 +1,2 @@ +export { default as DropdownMenu } from "./DropdownMenu"; +export { default as DropdownMenuItem } from "./DropdownMenuItem"; diff --git a/src/components/modals/Modal.jsx b/src/components/modals/Modal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9b02224a4f4b91db7ca3fd199885c69f433801b2 --- /dev/null +++ b/src/components/modals/Modal.jsx @@ -0,0 +1,18 @@ +import React from "react"; +import Modal from "react-modal"; +import classNames from "classnames"; + +const CustomModal = ({ children, containerClassName, ...props }) => ( + <Modal + contentLabel={props.headline} + overlayClassName="modal__overlay" + className="modal__content" + {...props} + > + <div className={classNames("modal__container w-full", containerClassName)}> + <div className="modal__container-body">{children}</div> + </div> + </Modal> +); + +export default CustomModal; diff --git a/src/components/modals/ModalConfirm.jsx b/src/components/modals/ModalConfirm.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0863fb9908f2813ed4ea6c75cfe9b24ca1fa8767 --- /dev/null +++ b/src/components/modals/ModalConfirm.jsx @@ -0,0 +1,51 @@ +import React from "react"; + +import Button from "components/Button"; + +import Modal from "./Modal"; + +const ModalConfirm = ({ + title, + children, + yesActionLabel = "OK", + cancelActionLabel = "Zrušit", + onCancel, + onConfirm, + ...props +}) => { + return ( + <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}> + <div className="card elevation-21"> + <div className="card__body"> + <div className="flex items-center justify-between mb-4"> + <h1 className="card-headline">{title}</h1> + <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"> + <Button + hoveractive + color="blue-300" + className="text-sm" + onClick={onConfirm} + > + {yesActionLabel} + </Button> + <Button + hoveractive + color="red-600" + className="text-sm" + onClick={onConfirm} + > + {cancelActionLabel} + </Button> + </div> + </div> + </Modal> + ); +}; + +export default ModalConfirm; diff --git a/src/components/posts/Post.jsx b/src/components/posts/Post.jsx index 675642415e283cfb7d8e6722bbb0a007acbd6b27..3a76326f9465211bd77498b06a4226a241ac9f4a 100644 --- a/src/components/posts/Post.jsx +++ b/src/components/posts/Post.jsx @@ -3,6 +3,7 @@ import classNames from "classnames"; import { format } from "date-fns"; import Chip from "components/Chip"; +import { DropdownMenu, DropdownMenuItem } from "components/dropdown-menu"; import Thumbs from "components/Thumbs"; const Post = ({ @@ -16,9 +17,15 @@ const Post = ({ archived, state, historyLog, + dimIfArchived = true, + displayActions = false, onLike, onDislike, - dimIfArchived = true, + onHide, + onBanUser, + onAnnounceProcedureProposal, + onAcceptProcedureProposal, + onRejectProcedureProposal, }) => { const wrapperClassName = classNames( "flex items-start p-4 lg:p-2 lg:py-3 lg:-mx-2", @@ -102,6 +109,15 @@ const Post = ({ logRecord.attribute === "content" && logRecord.originator === "chairman" ).length > 0; + const showAnnounceAction = + type === "procedure-proposal" && state === "pending"; + const showAcceptAction = + type === "procedure-proposal" && state === "announced"; + const showRejectAction = + type === "procedure-proposal" && state === "announced"; + const showBanAction = true; + const showHideAction = !archived; + return ( <div className={wrapperClassName}> <img @@ -139,6 +155,45 @@ const Post = ({ onDislike={onDislike} canThumb={ranking.myVote === "none"} /> + {displayActions && ( + <DropdownMenu right> + {showAnnounceAction && ( + <DropdownMenuItem + onClick={onAnnounceProcedureProposal} + icon="ico--bullhorn" + title="Vyhlásit procedurální návrh" + /> + )} + {showAcceptAction && ( + <DropdownMenuItem + onClick={onAcceptProcedureProposal} + icon="ico--thumbs-up" + title="Schválit procedurální návrh" + /> + )} + {showRejectAction && ( + <DropdownMenuItem + onClick={onRejectProcedureProposal} + icon="ico--thumbs-down" + title="Zamítnout procedurální návrh" + /> + )} + {showBanAction && ( + <DropdownMenuItem + onClick={onBanUser} + icon="ico--lock" + title="Zablokovat uživatele" + /> + )} + {showHideAction && ( + <DropdownMenuItem + onClick={onHide} + icon="ico--eye-off" + title="Skrýt příspěvek" + /> + )} + </DropdownMenu> + )} </div> </div> </div> diff --git a/src/components/posts/PostList.jsx b/src/components/posts/PostList.jsx index b76ebc4212649e3101a3c80f68f8499b21701a0f..9ff1ad8fe1bcbde72d78a38d171a1f3ce41b994f 100644 --- a/src/components/posts/PostList.jsx +++ b/src/components/posts/PostList.jsx @@ -3,18 +3,33 @@ import classNames from "classnames"; import Post from "./Post"; -const PostList = ({ className, items, onLike, onDislike, dimArchived }) => { - const onPostLike = (post) => { - return (evt) => { - onLike(post); - }; - }; - const onPostDislike = (post) => { - return (evt) => { - onDislike(post); - }; +const PostList = ({ + className, + items, + onLike, + onDislike, + onHide, + onBanUser, + onAnnounceProcedureProposal, + onAcceptProcedureProposal, + onRejectProcedureProposal, + dimArchived, +}) => { + const buildHandler = (responderFn) => (post) => (evt) => { + evt.preventDefault(); + responderFn(post); }; + const onPostLike = buildHandler(onLike); + const onPostDislike = buildHandler(onDislike); + const onPostHide = buildHandler(onHide); + const onPostBanUser = buildHandler(onBanUser); + const onPostAnnounceProcedureProposal = buildHandler( + onAnnounceProcedureProposal + ); + const onPostAcceptProcedureProposal = buildHandler(onAcceptProcedureProposal); + const onPostRejectProcedureProposal = buildHandler(onRejectProcedureProposal); + return ( <div className={classNames("space-y-px", className)}> {items @@ -31,9 +46,15 @@ const PostList = ({ className, items, onLike, onDislike, dimArchived }) => { historyLog={item.historyLog} seen={item.seen} archived={item.archived} + displayActions={true} + dimIfArchived={dimArchived} onLike={onPostLike(item)} onDislike={onPostDislike(item)} - dimIfArchived={dimArchived} + onHide={onPostHide(item)} + onBanUser={onPostBanUser(item)} + onAnnounceProcedureProposal={onPostAnnounceProcedureProposal(item)} + onAcceptProcedureProposal={onPostAcceptProcedureProposal(item)} + onRejectProcedureProposal={onPostRejectProcedureProposal(item)} /> ))} </div> diff --git a/src/containers/AddAnnouncementForm.jsx b/src/containers/AddAnnouncementForm.jsx index 9febd43c003ee16fbef231e60126daf5b6e89a40..d72bd5c5d7dc80142e839805183b62f0a46573dd 100644 --- a/src/containers/AddAnnouncementForm.jsx +++ b/src/containers/AddAnnouncementForm.jsx @@ -34,7 +34,7 @@ const AddAnnouncementForm = ({ className }) => { <Button onClick={onAdd} - className="btn--black text-sm mt-2" + className="text-sm mt-2" hoverActive disabled={!text} > diff --git a/src/containers/AddPostForm.jsx b/src/containers/AddPostForm.jsx index b4f1bb3eb01b6a8a589463dabe6d5b8e9c03b0a9..ba79cc9ea548cde1c0fd0db94d7ba4ac9fbc3578 100644 --- a/src/containers/AddPostForm.jsx +++ b/src/containers/AddPostForm.jsx @@ -53,7 +53,6 @@ const AddPostForm = ({ className }) => { <div className="space-x-4"> <Button - className="btn--black" onClick={onAddPost} disabled={!text} hoverActive diff --git a/src/containers/AnnoucementsContainer.jsx b/src/containers/AnnoucementsContainer.jsx index a2937a3360051dcfa04cca0d6eb0a60836411159..dc7204de90f9d0613a5abd562d8bfed57e72b128 100644 --- a/src/containers/AnnoucementsContainer.jsx +++ b/src/containers/AnnoucementsContainer.jsx @@ -6,7 +6,21 @@ import { AnnouncementStore } from "stores"; const AnnoucementsContainer = () => { const items = AnnouncementStore.useState((state) => state.items); - return <AnnouncementList items={items} />; + const onEdit = (announcement) => { + console.log("edit", announcement); + }; + const onDelete = (announcement) => { + console.log("delete", announcement); + }; + + return ( + <AnnouncementList + items={items} + displayActions={true} + onDelete={onDelete} + onEdit={onEdit} + /> + ); }; export default AnnoucementsContainer; diff --git a/src/containers/PostsContainer.jsx b/src/containers/PostsContainer.jsx index dc02809892b5215fc57690314e4f968cc453ee2f..ca4187efee81448e8d569de63f8bd01cc6c00222 100644 --- a/src/containers/PostsContainer.jsx +++ b/src/containers/PostsContainer.jsx @@ -19,6 +19,22 @@ const PostsContainer = ({ className }) => { const sliceStart = (window.page - 1) * window.perPage; const sliceEnd = window.page * window.perPage; + const onHide = (post) => { + console.log("hide", post); + }; + const onBanUser = (post) => { + console.log("banUser", post); + }; + const onAnnounceProcedureProposal = (post) => { + console.log("announce", post); + }; + const onAcceptProcedureProposal = (post) => { + console.log("accept", post); + }; + const onRejectProcedureProposal = (post) => { + console.log("reject", post); + }; + return ( <PostList items={window.items @@ -28,6 +44,12 @@ const PostsContainer = ({ className }) => { onDislike={dislike.run} className={className} dimArchived={!showingArchivedOnly} + displayActions={true} + onHide={onHide} + onBanUser={onBanUser} + onAnnounceProcedureProposal={onAnnounceProcedureProposal} + onAcceptProcedureProposal={onAcceptProcedureProposal} + onRejectProcedureProposal={onRejectProcedureProposal} /> ); }; diff --git a/src/index.js b/src/index.js index dd6b4cfd15e745508c96cbaf559398b3d0451731..f22bcc4daa6ad66d9b767af2c5e5a2b515029416 100644 --- a/src/index.js +++ b/src/index.js @@ -1,15 +1,19 @@ import React from "react"; import ReactDOM from "react-dom"; +import ReactModal from "react-modal"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; +const root = document.getElementById("root"); + ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, - document.getElementById("root") + root ); +ReactModal.setAppElement(root); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 8b5f95840a2650af6bcf8a1cddbdf52e0d39f16c..97fadafde097688c36a0b25e83bae8e14dd05825 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -1,5 +1,7 @@ -import React from "react"; +import React, { useState } from "react"; +import { DropdownMenu, DropdownMenuItem } from "components/dropdown-menu"; +import ModalConfirm from "components/modals/ModalConfirm"; import AddAnnouncementForm from "containers/AddAnnouncementForm"; import AddPostForm from "containers/AddPostForm"; import AnnouncementsContainer from "containers/AnnoucementsContainer"; @@ -7,6 +9,21 @@ import PostFilters from "containers/PostFilters"; import PostsContainer from "containers/PostsContainer"; const Home = () => { + const [showEndDiscussionConfirm, setShowEndDiscussionConfirm] = useState( + false + ); + + const onRenameProgramPoint = () => { + console.log("renameProgramPoint"); + }; + const onEndDiscussion = () => { + console.log("endDiscussion"); + setShowEndDiscussionConfirm(true); + }; + const onEndProgramPoint = () => { + console.log("endProgramPoint"); + }; + return ( <> <article className="container container--wide pt-8 lg:py-24 cf2021"> @@ -16,6 +33,30 @@ const Home = () => { Bod č. 1: Programové priority Pirátské strany pro sněmovní volby 2021 </h1> + <DropdownMenu right triggerSize="lg"> + <DropdownMenuItem + onClick={onRenameProgramPoint} + icon="ico--edit-pencil" + title="Přejmenovat bod programu" + titleSize="base" + iconSize="base" + /> + <DropdownMenuItem + onClick={onEndDiscussion} + icon="ico--bubbles" + title="Ukončit rozpravu" + titleSize="base" + iconSize="base" + /> + <DropdownMenuItem + onClick={onEndProgramPoint} + icon="ico--switch" + title="Ukončit bod programu" + titleSize="base" + iconSize="base" + /> + )} + </DropdownMenu> </div> <iframe @@ -52,6 +93,15 @@ const Home = () => { <AddPostForm className="my-8 space-y-4" /> </section> </article> + <ModalConfirm + isOpen={showEndDiscussionConfirm} + onConfirm={() => setShowEndDiscussionConfirm(false)} + onCancel={() => setShowEndDiscussionConfirm(false)} + title="Ukončit rozpravu?" + yesActionLabel="Ukončit" + > + Opravdu chcete ukončit rozpravu? + </ModalConfirm> </> ); };