From 26fbc88c1038971e3c4fb48f40eb09ff15816099 Mon Sep 17 00:00:00 2001 From: xaralis <filip.varecha@fragaria.cz> Date: Thu, 17 Dec 2020 15:11:31 +0100 Subject: [PATCH] feat: edit announcement (wip) --- package-lock.json | 13 +--- package.json | 2 +- src/actions/announcements.js | 58 ++++++++++++++- src/actions/program.js | 4 +- src/api.js | 16 +++++ .../annoucements/AnnouncementEditModal.jsx | 70 +++++++++++++++++++ src/components/cards/Card.jsx | 9 +++ src/components/cards/CardActions.jsx | 13 ++++ src/components/cards/CardBody.jsx | 9 +++ src/components/cards/CardBodyText.jsx | 7 ++ src/components/cards/CardHeadline.jsx | 7 ++ src/components/cards/index.js | 5 ++ src/components/modals/ModalConfirm.jsx | 29 +++++--- src/containers/AnnoucementsContainer.jsx | 69 ++++++++++++++---- 14 files changed, 271 insertions(+), 40 deletions(-) create mode 100644 src/api.js create mode 100644 src/components/annoucements/AnnouncementEditModal.jsx create mode 100644 src/components/cards/Card.jsx create mode 100644 src/components/cards/CardActions.jsx create mode 100644 src/components/cards/CardBody.jsx create mode 100644 src/components/cards/CardBodyText.jsx create mode 100644 src/components/cards/CardHeadline.jsx create mode 100644 src/components/cards/index.js diff --git a/package-lock.json b/package-lock.json index 75b4a90..0609126 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10949,19 +10949,12 @@ } }, "pullstate": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/pullstate/-/pullstate-1.20.4.tgz", - "integrity": "sha512-SksJ70iYNrC+YsGjMx54pXT2/iYYUu3zg9hezjHNjPPwyViglo4lh+N0I4DwB6xw92s+9NagD3AXMGMfiIt76g==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/pullstate/-/pullstate-1.20.5.tgz", + "integrity": "sha512-9+QAXjf5WugIPEFHgMTwKM42uDx8ezB1BDobh7gpg9OCta5rp1XdFxa6tLljB/4NUxnI5YqoiE2s15ZOh+sl4A==", "requires": { "fast-deep-equal": "^3.1.3", "immer": "^7.0.1" - }, - "dependencies": { - "immer": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.15.tgz", - "integrity": "sha512-yM7jo9+hvYgvdCQdqvhCNRRio0SCXc8xDPzA25SvKWa7b1WVPjLwQs1VYU5JPXjcJPTqAa5NP5dqpORGYBQ2AA==" - } } }, "pump": { diff --git a/package.json b/package.json index f872dfd..a0a7779 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "immer": "^7.0.15", "keycloak-js": "^10.0.2", "lodash": "^4.17.20", - "pullstate": "^1.20.4", + "pullstate": "^1.20.5", "react": "^16.13.1", "react-device-detect": "^1.13.1", "react-dom": "^16.13.1", diff --git a/src/actions/announcements.js b/src/actions/announcements.js index f4eee78..2681527 100644 --- a/src/actions/announcements.js +++ b/src/actions/announcements.js @@ -1,3 +1,5 @@ +import findIndex from "lodash/findIndex"; +import remove from "lodash/remove"; import { createAsyncAction, successResult } from "pullstate"; import { AnnouncementStore } from "stores"; @@ -20,9 +22,59 @@ export const addAnnouncement = createAsyncAction( }, { postActionHook: ({ result }) => { - AnnouncementStore.update((state) => { - state.items.push(result.payload); - }); + if (!result.error) { + AnnouncementStore.update((state) => { + state.items.push(result.payload); + }); + } + }, + } +); + +/** + * Delete existing announcement. + */ +export const deleteAnnouncement = createAsyncAction( + /** + * + * @param {CF2021.Announcement} item + */ + async (item) => { + return successResult(item); + }, + { + postActionHook: ({ result }) => { + if (!result.error) { + AnnouncementStore.update((state) => { + remove(state.items, { id: result.payload.id }); + }); + } + }, + } +); + +/** + * Update content of an announcement. + */ +export const updateAnnouncementContent = createAsyncAction( + /** + * + * @param {CF2021.Announcement} item + * @param {string} newContent + */ + async (item, newContent) => { + return successResult({ item, newContent }); + }, + { + postActionHook: ({ result }) => { + if (!result.error) { + AnnouncementStore.update((state) => { + const itemIdx = findIndex(state.items, { + id: result.payload.item.id, + }); + state.items[itemIdx].content = result.payload.newContent; + }); + } }, } ); diff --git a/src/actions/program.js b/src/actions/program.js index 2c45842..7a70b07 100644 --- a/src/actions/program.js +++ b/src/actions/program.js @@ -1,13 +1,13 @@ +import { fetch } from "api"; import pick from "lodash/pick"; import { createAsyncAction, errorResult, successResult } from "pullstate"; -import fetch from "unfetch"; import { ProgramStore } from "stores"; export const loadProgram = createAsyncAction( async () => { try { - const resp = await fetch(`${process.env.REACT_APP_API_BASE_URL}/program`); + const resp = await fetch("/program"); const mappings = await resp.json(); return successResult(mappings); } catch (err) { diff --git a/src/api.js b/src/api.js new file mode 100644 index 0000000..b5674b2 --- /dev/null +++ b/src/api.js @@ -0,0 +1,16 @@ +import baseFetch from "unfetch"; + +import { AuthStore } from "./stores"; + +export const fetch = (url, opts) => { + const { isAuthenticated, user } = AuthStore.getRawState(); + + opts = opts || {}; + opts.headers = opts.headers || {}; + + if (isAuthenticated) { + // opts.headers.Authorization = "Bearer " + user.accessToken; + } + + return baseFetch(process.env.REACT_APP_API_BASE_URL + url, opts); +}; diff --git a/src/components/annoucements/AnnouncementEditModal.jsx b/src/components/annoucements/AnnouncementEditModal.jsx new file mode 100644 index 0000000..29624d8 --- /dev/null +++ b/src/components/annoucements/AnnouncementEditModal.jsx @@ -0,0 +1,70 @@ +import React, { useState } from "react"; + +import Button from "components/Button"; +import { Card, CardActions, CardBody, CardHeadline } from "components/cards"; +import Modal from "components/modals/Modal"; + +const AnnouncementEditModal = ({ + announcement, + onCancel, + onConfirm, + ...props +}) => { + const [text, setText] = useState(announcement.content); + + const onTextInput = (evt) => { + setText(evt.target.value); + }; + + const confirm = (evt) => { + if (!!text) { + onConfirm(text); + } + }; + + return ( + <Modal containerClassName="max-w-md" 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> + <div className="form-field"> + <div className="form-field__wrapper form-field__wrapper--shadowed"> + <textarea + className="text-input form-field__control " + value={text} + rows="8" + placeholder="Vyplňte text oznámení" + onChange={onTextInput} + ></textarea> + </div> + </div> + </CardBody> + <CardActions right className="space-x-1"> + <Button + hoverActive + color="blue-300" + className="text-sm" + onClick={confirm} + > + Uložit + </Button> + <Button + hoverActive + color="red-600" + className="text-sm" + onClick={onCancel} + > + Zrušit + </Button> + </CardActions> + </Card> + </Modal> + ); +}; + +export default AnnouncementEditModal; diff --git a/src/components/cards/Card.jsx b/src/components/cards/Card.jsx new file mode 100644 index 0000000..58ce9fd --- /dev/null +++ b/src/components/cards/Card.jsx @@ -0,0 +1,9 @@ +import React from "react"; +import classNames from "classnames"; + +const Card = ({ children, elevation = 21, className }) => { + const cls = classNames("card", `elevation-${elevation}`, className); + return <div className={cls}>{children}</div>; +}; + +export default Card; diff --git a/src/components/cards/CardActions.jsx b/src/components/cards/CardActions.jsx new file mode 100644 index 0000000..0dc8e51 --- /dev/null +++ b/src/components/cards/CardActions.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import classNames from "classnames"; + +const CardActions = ({ children, right, className }) => { + const cls = classNames( + "card-actions", + { "card-actions--right": !!right }, + className + ); + return <div className={cls}>{children}</div>; +}; + +export default CardActions; diff --git a/src/components/cards/CardBody.jsx b/src/components/cards/CardBody.jsx new file mode 100644 index 0000000..3bf0815 --- /dev/null +++ b/src/components/cards/CardBody.jsx @@ -0,0 +1,9 @@ +import React from "react"; +import classNames from "classnames"; + +const CardBody = ({ children, className }) => { + const cls = classNames("card__body", className); + return <div className={cls}>{children}</div>; +}; + +export default CardBody; diff --git a/src/components/cards/CardBodyText.jsx b/src/components/cards/CardBodyText.jsx new file mode 100644 index 0000000..7fb8944 --- /dev/null +++ b/src/components/cards/CardBodyText.jsx @@ -0,0 +1,7 @@ +import React from "react"; + +const CardBodyText = ({ children }) => { + return <div className="card-body-text">{children}</div>; +}; + +export default CardBodyText; diff --git a/src/components/cards/CardHeadline.jsx b/src/components/cards/CardHeadline.jsx new file mode 100644 index 0000000..fda727c --- /dev/null +++ b/src/components/cards/CardHeadline.jsx @@ -0,0 +1,7 @@ +import React from "react"; + +const CardHeadline = ({ children }) => { + return <h1 className="card-headline">{children}</h1>; +}; + +export default CardHeadline; diff --git a/src/components/cards/index.js b/src/components/cards/index.js new file mode 100644 index 0000000..51bf741 --- /dev/null +++ b/src/components/cards/index.js @@ -0,0 +1,5 @@ +export { default as Card } from "./Card"; +export { default as CardActions } from "./CardActions"; +export { default as CardBody } from "./CardBody"; +export { default as CardBodyText } from "./CardBodyText"; +export { default as CardHeadline } from "./CardHeadline"; diff --git a/src/components/modals/ModalConfirm.jsx b/src/components/modals/ModalConfirm.jsx index 0863fb9..b427aff 100644 --- a/src/components/modals/ModalConfirm.jsx +++ b/src/components/modals/ModalConfirm.jsx @@ -1,6 +1,13 @@ import React from "react"; import Button from "components/Button"; +import { + Card, + CardActions, + CardBody, + CardBodyText, + CardHeadline, +} from "components/cards"; import Modal from "./Modal"; @@ -15,19 +22,19 @@ const ModalConfirm = ({ }) => { return ( <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}> - <div className="card elevation-21"> - <div className="card__body"> + <Card> + <CardBody> <div className="flex items-center justify-between mb-4"> - <h1 className="card-headline">{title}</h1> + <CardHeadline>{title}</CardHeadline> <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"> + <CardBodyText>{children}</CardBodyText> + </CardBody> + <CardActions right className="space-x-1"> <Button - hoveractive + hoverActive color="blue-300" className="text-sm" onClick={onConfirm} @@ -35,15 +42,15 @@ const ModalConfirm = ({ {yesActionLabel} </Button> <Button - hoveractive + hoverActive color="red-600" className="text-sm" - onClick={onConfirm} + onClick={onCancel} > {cancelActionLabel} </Button> - </div> - </div> + </CardActions> + </Card> </Modal> ); }; diff --git a/src/containers/AnnoucementsContainer.jsx b/src/containers/AnnoucementsContainer.jsx index dc7204d..e3b5322 100644 --- a/src/containers/AnnoucementsContainer.jsx +++ b/src/containers/AnnoucementsContainer.jsx @@ -1,25 +1,68 @@ -import React from "react"; +import React, { useCallback, useState } from "react"; +import { + deleteAnnouncement, + updateAnnouncementContent, +} from "actions/announcements"; +import AnnouncementEditModal from "components/annoucements/AnnouncementEditModal"; import AnnouncementList from "components/annoucements/AnnouncementList"; +import ModalConfirm from "components/modals/ModalConfirm"; import { AnnouncementStore } from "stores"; const AnnoucementsContainer = () => { + const [itemToDelete, setItemToDelete] = useState(null); + const [itemToEdit, setItemToEdit] = useState(null); const items = AnnouncementStore.useState((state) => state.items); - const onEdit = (announcement) => { - console.log("edit", announcement); - }; - const onDelete = (announcement) => { - console.log("delete", announcement); - }; + const confirmEdit = useCallback( + async (newContent) => { + if (itemToEdit && newContent) { + await updateAnnouncementContent.run(itemToEdit, newContent); + setItemToEdit(null); + } + }, + [itemToEdit, setItemToEdit] + ); + + const cancelEdit = useCallback(() => { + setItemToEdit(null); + }, [setItemToEdit]); + + const confirmDelete = useCallback(async () => { + await deleteAnnouncement.run(itemToDelete); + setItemToDelete(null); + }, [setItemToDelete, itemToDelete]); + + const cancelDelete = useCallback(() => { + setItemToDelete(null); + }, [setItemToDelete]); return ( - <AnnouncementList - items={items} - displayActions={true} - onDelete={onDelete} - onEdit={onEdit} - /> + <> + <AnnouncementList + items={items} + displayActions={true} + onDelete={setItemToDelete} + onEdit={setItemToEdit} + /> + <ModalConfirm + isOpen={!!itemToDelete} + onConfirm={confirmDelete} + onCancel={cancelDelete} + title="Opravdu chcete toto oznámení smazat?" + yesActionLabel="Smazat" + > + Opravdu chcete ukončit rozpravu? + </ModalConfirm> + {itemToEdit && ( + <AnnouncementEditModal + isOpen={true} + announcement={itemToEdit} + onConfirm={confirmEdit} + onCancel={cancelEdit} + /> + )} + </> ); }; -- GitLab