diff --git a/src/App.jsx b/src/App.jsx index caecd16687bdbb4df84c5dcdad3aa49d5b1b3c37..225ee33807383ca265ee4d90afe80d939d6afbad 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,7 +8,8 @@ import Footer from "components/Footer"; import Navbar from "components/Navbar"; import Home from "pages/Home"; import Program from "pages/Program"; -import { AuthStore } from "stores"; +import { AuthStore, PostStore } from "stores"; +import { updateWindowPosts } from "utils"; import keycloak from "./keycloak"; @@ -26,18 +27,19 @@ if (process.env.REACT_APP_SENTRY_DSN) { const onKeycloakEvent = (event) => { if (["onAuthRefreshSuccess", "onAuthSuccess"].includes(event)) { Sentry.setUser(keycloak.tokenParsed); - AuthStore.update((state) => { - const kcRoles = keycloak.tokenParsed.roles; - let role = null; - if (kcRoles.includes("chairman")) { - role = "chairman"; - } else if (kcRoles.includes("member")) { - role = "member"; - } else { - role = "regp"; - } + const kcRoles = keycloak.tokenParsed.roles; + let role = null; + + if (kcRoles.includes("chairman")) { + role = "chairman"; + } else if (kcRoles.includes("member")) { + role = "member"; + } else { + role = "regp"; + } + AuthStore.update((state) => { state.isAuthenticated = true; state.user = { name: keycloak.tokenParsed.name, @@ -46,6 +48,12 @@ const onKeycloakEvent = (event) => { accessToken: keycloak.token, }; }); + + PostStore.update((state) => { + // Only display proposals verified by chairman to other users. + state.filters.showPendingProposals = role === "chairman"; + updateWindowPosts(state); + }); } }; diff --git a/src/components/MarkdownEditor.jsx b/src/components/MarkdownEditor.jsx deleted file mode 100644 index 1379fe5a176ad1317c57299f502a7ab0c1bd708a..0000000000000000000000000000000000000000 --- a/src/components/MarkdownEditor.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from "react"; -import ReactMde from "react-mde"; -import Showdown from "showdown"; - -const converter = new Showdown.Converter({ - tables: true, - simplifiedAutoLink: true, - strikethrough: true, - tasklists: true, -}); - -const MarkdownEditor = ({ value, onChange }) => { - return ( - <ReactMde - value={value} - onChange={onChange} - // selectedTab={selectedTab} - // onTabChange={setSelectedTab} - generateMarkdownPreview={(markdown) => - Promise.resolve(converter.makeHtml(markdown)) - } - /> - ); -}; - -export default MarkdownEditor; diff --git a/src/components/annoucements/AnnouncementEditModal.jsx b/src/components/annoucements/AnnouncementEditModal.jsx index c0bed97aeb03f8044cdaa4ea0e78dc39de345527..69e52f1334dfb4c2f1c7b302d3b4c33ece7a5989 100644 --- a/src/components/annoucements/AnnouncementEditModal.jsx +++ b/src/components/annoucements/AnnouncementEditModal.jsx @@ -3,6 +3,7 @@ import React, { useState } from "react"; 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"; const AnnouncementEditModal = ({ @@ -14,19 +15,26 @@ const AnnouncementEditModal = ({ ...props }) => { const [text, setText] = useState(announcement.content); + const [noTextError, setNoTextError] = useState(false); - const onTextInput = (evt) => { - setText(evt.target.value); + const onTextInput = (newText) => { + setText(newText); + + if (newText !== "") { + setNoTextError(false); + } }; const confirm = (evt) => { if (!!text) { onConfirm(text); + } else { + setNoTextError(true); } }; return ( - <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}> + <Modal containerClassName="max-w-lg" onRequestClose={onCancel} {...props}> <Card> <CardBody> <div className="flex items-center justify-between mb-4"> @@ -35,20 +43,20 @@ const AnnouncementEditModal = ({ <i className="ico--close"></i> </button> </div> - <div className="form-field"> - <label className="form-field__label" htmlFor="field"> - Nový text oznámení - </label> - <div className="form-field__wrapper form-field__wrapper--shadowed"> - <textarea - className="text-input form-field__control text-base" - value={text} - rows="8" - placeholder="Vyplňte text oznámení" - onChange={onTextInput} - ></textarea> - </div> - </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} diff --git a/src/components/mde/MarkdownEditor.css b/src/components/mde/MarkdownEditor.css new file mode 100644 index 0000000000000000000000000000000000000000..970a2c9409f1b8793a59a97a7d5891fa5479d4d5 --- /dev/null +++ b/src/components/mde/MarkdownEditor.css @@ -0,0 +1,77 @@ +.mde-header { + background: transparent; +} + +.react-mde .invisible { + display: none; +} + +.mde-header { + border: 0; + align-items: center; +} + +.mde-header .mde-tabs { + display: inline-flex; + background-color: #000; + background-color: rgba(0,0,0,1); + padding: .25rem; +} + +.mde-header .mde-tabs button { + padding: .5rem 1rem; + font-family: Bebas Neue,Helvetica,Arial,sans-serif; + font-weight: 400; + font-size: 1.1rem; + --text-opacity: 1; + color: #fff; + color: rgba(255,255,255,var(--text-opacity)); + text-align: center; + cursor: pointer; + border: 0; + border-radius: 0; + margin: 0 !important; + outline: 0; +} + +.mde-header .mde-tabs button.selected { + /* blue-300 */ + background: #027da8; + color: #fff; + border: 0; + border-radius: 0; + margin: 0; + outline: 0; +} + +.mde-header .mde-tabs button:not(.selected):hover { + /* grey-500 */ + background: #303132; +} + +.mde-header .mde-header-item { + border: 1px transparent solid; + transition: border-color 100ms ease-in-out; +} + +.mde-header ul.mde-header-group li.mde-header-item { + margin: 0; +} + +.mde-header .mde-header-item:hover { + /* grey-200 */ + border: 1px #adadad solid; +} + +.mde-text { + font-family: monospace; +} + + +.mde-header ul.mde-header-group { + padding: 0 0.5rem; +} + +.mde-header ul.mde-header-group + .mde-header-group { + margin-left: .5rem; +} diff --git a/src/components/mde/MarkdownEditor.jsx b/src/components/mde/MarkdownEditor.jsx new file mode 100644 index 0000000000000000000000000000000000000000..88a74fe4d50d38f0dbcea58e67d6e9bd5bc99de9 --- /dev/null +++ b/src/components/mde/MarkdownEditor.jsx @@ -0,0 +1,62 @@ +import React, { useState } from "react"; +import ReactMde from "react-mde"; +import classNames from "classnames"; +import Showdown from "showdown"; + +import "react-mde/lib/styles/css/react-mde-toolbar.css"; +import "./MarkdownEditor.css"; + +const converter = new Showdown.Converter({ + tables: true, + simplifiedAutoLink: true, + strikethrough: true, + tasklists: true, +}); + +const MarkdownEditor = ({ + value, + onChange, + error, + placeholder = "", + ...props +}) => { + const [selectedTab, setSelectedTab] = useState("write"); + + const classes = { + preview: "content-block p-2 border border-grey-200", + textArea: "p-2 text-input text-base", + }; + + const l18n = { + write: "Psaní", + preview: "Náhled", + uploadingImage: "Nahrávám obrázek", + }; + + const childProps = { + textArea: { + placeholder, + }, + }; + + return ( + <div className={classNames("form-field", { "form-field--error": !!error })}> + <ReactMde + value={value} + onChange={onChange} + selectedTab={selectedTab} + onTabChange={setSelectedTab} + generateMarkdownPreview={(markdown) => + Promise.resolve(converter.makeHtml(markdown)) + } + classes={classes} + l18n={l18n} + childProps={childProps} + {...props} + /> + {error && <div className="form-field__error">{error}</div>} + </div> + ); +}; + +export default MarkdownEditor; diff --git a/src/components/posts/PostEditModal.jsx b/src/components/posts/PostEditModal.jsx index 72b5c0c3cdc09da0ff1c415857196c87f75977a1..d2b00e7e3f4bc9d7fdea8a290743d1e542adcae5 100644 --- a/src/components/posts/PostEditModal.jsx +++ b/src/components/posts/PostEditModal.jsx @@ -3,6 +3,7 @@ import React, { useState } from "react"; 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"; const PostEditModal = ({ @@ -14,19 +15,26 @@ const PostEditModal = ({ ...props }) => { const [text, setText] = useState(post.content); + const [noTextError, setNoTextError] = useState(false); - const onTextInput = (evt) => { - setText(evt.target.value); + const onTextInput = (newText) => { + setText(newText); + + if (newText !== "") { + setNoTextError(false); + } }; const confirm = (evt) => { if (!!text) { onConfirm(text); + } else { + setNoTextError(true); } }; return ( - <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}> + <Modal containerClassName="max-w-xl" onRequestClose={onCancel} {...props}> <Card> <CardBody> <div className="flex items-center justify-between mb-4"> @@ -35,20 +43,16 @@ const PostEditModal = ({ <i className="ico--close"></i> </button> </div> - <div className="form-field"> - <label className="form-field__label" htmlFor="field"> - Nový text příspěvku - </label> - <div className="form-field__wrapper form-field__wrapper--shadowed"> - <textarea - className="text-input form-field__control text-base" - value={text} - rows="8" - placeholder="Vyplňte text příspěvku" - onChange={onTextInput} - ></textarea> - </div> - </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" + /> {error && ( <ErrorMessage className="mt-2"> Při editaci došlo k problému: {error} diff --git a/src/containers/AddAnnouncementForm.jsx b/src/containers/AddAnnouncementForm.jsx index de76ab8243c9bafd5d990db62d7965898fb512a2..2a3f5fbaa750a8ca9bc080d8461af17f4577831a 100644 --- a/src/containers/AddAnnouncementForm.jsx +++ b/src/containers/AddAnnouncementForm.jsx @@ -4,6 +4,7 @@ import classNames from "classnames"; import { addAnnouncement } from "actions/announcements"; 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()@:%_+.~#?&//=]*)/; @@ -21,10 +22,10 @@ const AddAnnouncementForm = ({ className }) => { type, }); - const onTextInput = (evt) => { - setText(evt.target.value); + const onTextInput = (newText) => { + setText(newText); - if (evt.target.value !== "") { + if (newText !== "") { setNoTextError(false); } }; @@ -90,27 +91,21 @@ const AddAnnouncementForm = ({ className }) => { </div> </div> - <div - className={classNames("form-field", { - "form-field--error": noTextError, - })} - > - <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> - {noTextError && ( - <div className="form-field__error"> - Před přidáním oznámení nezapomeňte vyplnit jeho obsah. - </div> - )} - </div> + <MarkdownEditor + value={text} + onChange={onTextInput} + error={ + noTextError + ? "Před přidáním oznámení nezapomeňte vyplnit jeho obsah." + : null + } + placeholder="Vyplňte text oznámení" + toolbarCommands={[ + ["bold", "italic", "strikethrough"], + ["link", "unordered-list", "ordered-list"], + ]} + minEditorHeight={100} + /> <div className={classNames("form-field", { diff --git a/src/containers/AddPostForm.jsx b/src/containers/AddPostForm.jsx index 667278372f7f3d9b2292f89f66097261c54c4b77..2e3fedf098985ce6e59aad359fabaf2c57d3335d 100644 --- a/src/containers/AddPostForm.jsx +++ b/src/containers/AddPostForm.jsx @@ -1,9 +1,9 @@ import React, { useState } from "react"; -import classNames from "classnames"; import { addPost, addProposal } from "actions/posts"; import Button from "components/Button"; import ErrorMessage from "components/ErrorMessage"; +import MarkdownEditor from "components/mde/MarkdownEditor"; import { useActionState } from "hooks"; const AddPostForm = ({ className }) => { @@ -17,10 +17,10 @@ const AddPostForm = ({ className }) => { content: text, }); - const onTextInput = (evt) => { - setText(evt.target.value); + const onTextInput = (newText) => { + setText(newText); - if (evt.target.value !== "") { + if (newText !== "") { setNoTextError(false); } }; @@ -83,27 +83,16 @@ const AddPostForm = ({ className }) => { Při přidávání příspěvku došlo k problému: {addingProposalError}. </ErrorMessage> )} - <div - className={classNames("form-field", { - "form-field--error": noTextError, - })} - > - <div className="form-field__wrapper form-field__wrapper--shadowed"> - <textarea - className="text-input form-field__control " - value={text} - rows="5" - cols="40" - placeholder="Vyplňte text vašeho příspěvku" - onChange={onTextInput} - ></textarea> - </div> - {noTextError && ( - <div className="form-field__error"> - Před přidáním příspěvku nezapomeňte vyplnit jeho obsah. - </div> - )} - </div> + <MarkdownEditor + value={text} + onChange={onTextInput} + error={ + noTextError + ? "Před přidáním příspěvku nezapomeňte vyplnit jeho obsah." + : null + } + placeholder="Vyplňte text vašeho příspěvku" + /> <div className="space-x-4"> <Button diff --git a/src/containers/PostsContainer.jsx b/src/containers/PostsContainer.jsx index 25d968abd4b4c37682b4640b0e18ac3ddedce22c..907f7ded861132f2990675fdd292fc68705efe61 100644 --- a/src/containers/PostsContainer.jsx +++ b/src/containers/PostsContainer.jsx @@ -146,14 +146,7 @@ const PostsContainer = ({ className }) => { const sliceStart = (window.page - 1) * window.perPage; const sliceEnd = window.page * window.perPage; - let windowItems = window.items.map((postId) => items[postId]); - - // Only display proposals verified by chairman to other users. - if (!user || user.role !== "chairman") { - windowItems = windowItems.filter( - (item) => item.type === "post" || item.state !== "pending" - ); - } + const windowItems = window.items.map((postId) => items[postId]); return ( <> diff --git a/src/stores.js b/src/stores.js index 6616a4f5ec9a6f9c9555e21cac12bf78ce4680b9..f4e1511b998a6e84afec4917c1608a2f1e30ad06 100644 --- a/src/stores.js +++ b/src/stores.js @@ -39,6 +39,7 @@ const postStoreInitial = { flags: "all", sort: "byDate", type: "all", + showPendingProposals: false, }, }; diff --git a/src/utils.js b/src/utils.js index 65d16be18e5e56c2511ad9ef4788049dafa1c77c..83fb40083240e42090dc552b3dd27bc12d4063f3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -28,6 +28,12 @@ export const filterPosts = (filters, allItems) => { let filteredItems = filter(allItems, predicate); + if (!filters.showPendingProposals) { + filteredItems = filteredItems.filter( + (item) => item.type === "post" || item.state !== "pending" + ); + } + if (filters.sort === "byDate") { return filteredItems.sort((a, b) => b.datetime - a.datetime); } diff --git a/typings/cf2021.d.ts b/typings/cf2021.d.ts index 63f15a564d6993b5dc6d2502a610016b285d4d7f..7c0d20197581b1952f9069d73ce8999558d62e59 100644 --- a/typings/cf2021.d.ts +++ b/typings/cf2021.d.ts @@ -124,6 +124,7 @@ declare namespace CF2021 { flags: "all" | "active" | "archived"; sort: "byDate" | "byScore"; type: "all" | "proposalsOnly" | "discussionOnly"; + showPendingProposals: boolean; } export interface PostStorePayload {