From c133a998f20719628842468a110cdb4ec4b824bb Mon Sep 17 00:00:00 2001 From: xaralis <filip.varecha@fragaria.cz> Date: Wed, 6 Jan 2021 22:24:24 +0100 Subject: [PATCH] feat: improve UX for add post form --- package-lock.json | 10 + package.json | 2 + .../annoucements/AnnouncementEditModal.jsx | 2 +- src/components/cards/Card.jsx | 12 +- src/components/cards/CardBody.jsx | 8 +- src/components/mde/MarkdownEditor.jsx | 14 +- src/components/modals/ModalWithActions.jsx | 2 +- src/components/posts/PostEditModal.jsx | 2 +- .../posts/RejectPostModalConfirm.jsx | 2 +- .../program/ProgramEntryEditModal.jsx | 2 +- src/containers/AddPostForm.jsx | 240 ++++++++++++------ src/pages/Home.jsx | 16 +- 12 files changed, 206 insertions(+), 106 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2fa80ea..cc7d0e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1426,6 +1426,16 @@ "resolved": "https://registry.npmjs.org/@rooks/use-interval/-/use-interval-4.5.0.tgz", "integrity": "sha512-As0DueIAGLJLYATKPPOCDGqoIlwbhPAcYP14TNTHaAj9/ODdvUYFXAP3jFCRzDNpjXCIgSe4oBuzVVmM526n+Q==" }, + "@rooks/use-outside-click": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rooks/use-outside-click/-/use-outside-click-4.5.0.tgz", + "integrity": "sha512-oNFSSVdGQUPq6W0K5YyCSfVEFRjrxkBoxW8k46SHu9m80XhHy+C9nOU+DGA9YGR55LIPtC7aVU08KDe4Uargug==" + }, + "@rooks/use-timeout": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@rooks/use-timeout/-/use-timeout-4.5.0.tgz", + "integrity": "sha512-g42L/gYLkC+E1bTX1sMOs8QTOjIwWDmCjASWZPRC0uM1iUWiQ8IrzyjB4m+AQXxLysWAcrq/eX605KkWNnrWhA==" + }, "@rooks/use-window-size": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@rooks/use-window-size/-/use-window-size-4.5.0.tgz", diff --git a/package.json b/package.json index 3409905..5266f39 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "dependencies": { "@react-keycloak/web": "^2.1.4", "@rooks/use-interval": "^4.5.0", + "@rooks/use-outside-click": "^4.5.0", + "@rooks/use-timeout": "^4.5.0", "@rooks/use-window-size": "^4.5.0", "@sentry/integrations": "^5.29.2", "@sentry/react": "^5.29.2", diff --git a/src/components/annoucements/AnnouncementEditModal.jsx b/src/components/annoucements/AnnouncementEditModal.jsx index afff7c9..1e67526 100644 --- a/src/components/annoucements/AnnouncementEditModal.jsx +++ b/src/components/annoucements/AnnouncementEditModal.jsx @@ -78,7 +78,7 @@ const AnnouncementEditModal = ({ return ( <Modal containerClassName="max-w-lg" onRequestClose={onCancel} {...props}> <form onSubmit={confirm}> - <Card> + <Card className="elevation-21"> <CardBody> <div className="flex items-center justify-between mb-4"> <CardHeadline>Upravit oznámenĂ</CardHeadline> diff --git a/src/components/cards/Card.jsx b/src/components/cards/Card.jsx index 58ce9fd..2c2d212 100644 --- a/src/components/cards/Card.jsx +++ b/src/components/cards/Card.jsx @@ -1,9 +1,13 @@ 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>; +const Card = ({ children, className }, ref) => { + const cls = classNames("card", className); + return ( + <div className={cls} ref={ref}> + {children} + </div> + ); }; -export default Card; +export default React.forwardRef(Card); diff --git a/src/components/cards/CardBody.jsx b/src/components/cards/CardBody.jsx index 3bf0815..47524f1 100644 --- a/src/components/cards/CardBody.jsx +++ b/src/components/cards/CardBody.jsx @@ -1,9 +1,13 @@ import React from "react"; import classNames from "classnames"; -const CardBody = ({ children, className }) => { +const CardBody = ({ children, className, ...props }) => { const cls = classNames("card__body", className); - return <div className={cls}>{children}</div>; + return ( + <div className={cls} {...props}> + {children} + </div> + ); }; export default CardBody; diff --git a/src/components/mde/MarkdownEditor.jsx b/src/components/mde/MarkdownEditor.jsx index d0a9c9f..43da567 100644 --- a/src/components/mde/MarkdownEditor.jsx +++ b/src/components/mde/MarkdownEditor.jsx @@ -7,13 +7,10 @@ import { markdownConverter } from "markdown"; import "react-mde/lib/styles/css/react-mde-toolbar.css"; import "./MarkdownEditor.css"; -const MarkdownEditor = ({ - value, - onChange, - error, - placeholder = "", - ...props -}) => { +const MarkdownEditor = ( + { value, onChange, error, placeholder = "", ...props }, + ref +) => { const [selectedTab, setSelectedTab] = useState("write"); const classes = { @@ -36,6 +33,7 @@ const MarkdownEditor = ({ return ( <div className={classNames("form-field", { "form-field--error": !!error })}> <ReactMde + ref={ref} value={value} onChange={onChange} selectedTab={selectedTab} @@ -53,4 +51,4 @@ const MarkdownEditor = ({ ); }; -export default MarkdownEditor; +export default React.forwardRef(MarkdownEditor); diff --git a/src/components/modals/ModalWithActions.jsx b/src/components/modals/ModalWithActions.jsx index acc861b..be8652a 100644 --- a/src/components/modals/ModalWithActions.jsx +++ b/src/components/modals/ModalWithActions.jsx @@ -21,7 +21,7 @@ const ModalConfirm = ({ }) => { return ( <Modal onRequestClose={onClose} {...props}> - <Card> + <Card className="elevation-21"> <CardBody> <div className="flex items-center justify-between mb-4"> <CardHeadline>{title}</CardHeadline> diff --git a/src/components/posts/PostEditModal.jsx b/src/components/posts/PostEditModal.jsx index aa464b8..df19d61 100644 --- a/src/components/posts/PostEditModal.jsx +++ b/src/components/posts/PostEditModal.jsx @@ -42,7 +42,7 @@ const PostEditModal = ({ return ( <Modal containerClassName="max-w-xl" onRequestClose={onCancel} {...props}> <form onSubmit={confirm}> - <Card> + <Card className="elevation-21"> <CardBody> <div className="flex items-center justify-between mb-4"> <CardHeadline>Upravit text pĹ™ĂspÄ›vku</CardHeadline> diff --git a/src/components/posts/RejectPostModalConfirm.jsx b/src/components/posts/RejectPostModalConfirm.jsx index 51f1e3e..2e03e16 100644 --- a/src/components/posts/RejectPostModalConfirm.jsx +++ b/src/components/posts/RejectPostModalConfirm.jsx @@ -27,7 +27,7 @@ const RejectPostModalConfirm = ({ }) => { return ( <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}> - <Card> + <Card className="elevation-21"> <CardBody> <div className="flex items-center justify-between mb-4"> <CardHeadline>{title}</CardHeadline> diff --git a/src/components/program/ProgramEntryEditModal.jsx b/src/components/program/ProgramEntryEditModal.jsx index 37ce3f4..cda61b8 100644 --- a/src/components/program/ProgramEntryEditModal.jsx +++ b/src/components/program/ProgramEntryEditModal.jsx @@ -24,7 +24,7 @@ const ProgramEntryEditModal = ({ return ( <Modal containerClassName="max-w-md" onRequestClose={onCancel} {...props}> - <Card> + <Card className="elevation-21"> <CardBody> <div className="flex items-center justify-between mb-4"> <CardHeadline>Upravit název programovĂ©ho bodu</CardHeadline> diff --git a/src/containers/AddPostForm.jsx b/src/containers/AddPostForm.jsx index df584ff..36a3108 100644 --- a/src/containers/AddPostForm.jsx +++ b/src/containers/AddPostForm.jsx @@ -1,13 +1,21 @@ -import React, { useState } from "react"; +import React, { useCallback, useRef, useState } from "react"; +import useOutsideClick from "@rooks/use-outside-click"; +import useTimeout from "@rooks/use-timeout"; +import classNames from "classnames"; import { addPost, addProposal } from "actions/posts"; import Button from "components/Button"; +import { Card, CardBody } from "components/cards"; import ErrorMessage from "components/ErrorMessage"; import MarkdownEditor from "components/mde/MarkdownEditor"; import { useActionState } from "hooks"; const AddPostForm = ({ className }) => { + const cardRef = useRef(); + const editorRef = useRef(); + const [expanded, setExpanded] = useState(false); const [text, setText] = useState(""); + const [showAddConfirm, setShowAddConfirm] = useState(false); const [type, setType] = useState("post"); const [error, setError] = useState(null); const [addingPost, addingPostError] = useActionState(addPost, { @@ -17,6 +25,29 @@ const AddPostForm = ({ className }) => { content: text, }); + const onOutsideClick = useCallback(() => { + setExpanded(false); + }, [setExpanded]); + + const onWrite = useCallback(() => { + if (!expanded) { + setExpanded(true); + setTimeout(() => { + if (editorRef.current && editorRef.current.finalRefs.textarea.current) { + editorRef.current.finalRefs.textarea.current.focus(); + } + }, 0); + } + }, [setExpanded, expanded]); + + const hideAddConfirm = useCallback(() => { + setShowAddConfirm(false); + }, [setShowAddConfirm]); + + useOutsideClick(cardRef, onOutsideClick); + + const { start: enqueueHideAddConfirm } = useTimeout(hideAddConfirm, 2000); + const onTextInput = (newText) => { setText(newText); @@ -38,6 +69,9 @@ const AddPostForm = ({ className }) => { if (!result.error) { setText(""); + setExpanded(false); + setShowAddConfirm(true); + enqueueHideAddConfirm(); } } } else { @@ -45,90 +79,138 @@ const AddPostForm = ({ className }) => { } }; + const wrapperClass = classNames( + className, + "hover:elevation-16 transition duration-500", + { + "elevation-4 cursor-text": !expanded, + "lg:elevation-16 container-padding--zero lg:container-padding--auto": expanded, + } + ); + return ( - <div className={className}> - {addingPostError && ( - <ErrorMessage> - PĹ™i pĹ™idávánĂ pĹ™ĂspÄ›vku došlo k problĂ©mu: {addingPostError}. - </ErrorMessage> - )} - {addingProposalError && ( - <ErrorMessage> - PĹ™i pĹ™idávánĂ pĹ™ĂspÄ›vku došlo k problĂ©mu: {addingProposalError}. - </ErrorMessage> - )} - - <MarkdownEditor - value={text} - onChange={onTextInput} - error={error} - placeholder="VyplĹte text vašeho pĹ™ĂspÄ›vku" - toolbarCommands={[ - ["header", "bold", "italic", "strikethrough"], - ["link", "quote"], - ["unordered-list", "ordered-list"], - ]} - /> - - <div className="form-field" onChange={(evt) => setType(evt.target.value)}> - <div className="form-field__wrapper form-field__wrapper--freeform flex-col sm:flex-row"> - <div className="radio form-field__control"> - <label> - <input type="radio" name="postType" value="post" defaultChecked /> - <span className="text-sm sm:text-base"> - PĹ™idávám <strong>běžnĂ˝ pĹ™ĂspÄ›vek</strong> - </span> - </label> + <Card className={wrapperClass} ref={cardRef}> + <span + className={classNames("alert items-center transition duration-500", { + "alert--success": showAddConfirm, + "alert--light": !showAddConfirm, + hidden: expanded, + })} + onClick={onWrite} + > + <i + className={classNames("alert__icon text-lg mr-4", { + "ico--checkmark": showAddConfirm, + "ico--pencil": !showAddConfirm, + })} + /> + {showAddConfirm && <span>PĹ™ĂspÄ›vek byl pĹ™idán.</span>} + {!showAddConfirm && <span>Napiš novĂ˝ pĹ™ĂspÄ›vek ...</span>} + </span> + <CardBody + className={ + "p-4 lg:p-8 " + (showAddConfirm || !expanded ? "hidden" : "") + } + > + <div className="space-y-4"> + {addingPostError && ( + <ErrorMessage> + PĹ™i pĹ™idávánĂ pĹ™ĂspÄ›vku došlo k problĂ©mu: {addingPostError}. + </ErrorMessage> + )} + {addingProposalError && ( + <ErrorMessage> + PĹ™i pĹ™idávánĂ pĹ™ĂspÄ›vku došlo k problĂ©mu: {addingProposalError}. + </ErrorMessage> + )} + + <MarkdownEditor + ref={editorRef} + value={text} + onChange={onTextInput} + error={error} + placeholder="VyplĹte text vašeho pĹ™ĂspÄ›vku" + toolbarCommands={[ + ["header", "bold", "italic", "strikethrough"], + ["link", "quote"], + ["unordered-list", "ordered-list"], + ]} + /> + + <div + className="form-field" + onChange={(evt) => setType(evt.target.value)} + > + <div className="form-field__wrapper form-field__wrapper--freeform flex-col sm:flex-row"> + <div className="radio form-field__control"> + <label> + <input + type="radio" + name="postType" + value="post" + defaultChecked + /> + <span className="text-sm sm:text-base"> + PĹ™idávám <strong>běžnĂ˝ pĹ™ĂspÄ›vek</strong> + </span> + </label> + </div> + + <div className="radio form-field__control ml-0 mt-4 sm:mt-0 sm:ml-4"> + <label> + <input + type="radio" + name="postType" + value="procedure-proposal" + /> + <span className="text-sm sm:text-base"> + PĹ™idávám <strong>návrh postupu</strong> + </span> + </label> + </div> + </div> </div> - <div className="radio form-field__control ml-0 mt-4 sm:mt-0 sm:ml-4"> - <label> - <input type="radio" name="postType" value="procedure-proposal" /> - <span className="text-sm sm:text-base"> - PĹ™idávám <strong>návrh postupu</strong> + {type === "procedure-proposal" && ( + <p className="alert alert--light text-sm"> + <i className="alert__icon ico--info mr-2 text-lg hidden md:block" /> + <span> + Návrh postupu se v rozpravÄ› zobrazĂ aĹľ potĂ©, co pĹ™edsedajĂcĂ{" "} + <strong>posoudĂ jeho pĹ™ijatelnost</strong>. Po odeslánĂ proto + nepanikaĹ™, Ĺľe jej hned nevidĂš. </span> - </label> + </p> + )} + + <div className="space-x-4"> + <Button + onClick={onAdd} + disabled={error || addingPost || addingProposal} + loading={addingPost || addingProposal} + fullwidth + hoverActive + className="text-sm xl:text-base" + > + {type === "post" && "PĹ™idat pĹ™ĂspÄ›vek"} + {type === "procedure-proposal" && "Navrhnout postup"} + </Button> + + <span className="text-sm text-grey-200 hidden lg:inline"> + Pro pokroÄŤilejšà formátovánĂ mĹŻĹľete pouĹľĂvat{" "} + <a + href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" + className="underline" + target="_blank" + rel="noreferrer noopener" + > + Markdown + </a> + . + </span> </div> </div> - </div> - - {type === "procedure-proposal" && ( - <p className="alert alert--light text-sm"> - <i className="alert__icon ico--info mr-2 text-lg hidden md:block" /> - <span> - Návrh postupu se v rozpravÄ› zobrazĂ aĹľ potĂ©, co pĹ™edsedajĂcĂ{" "} - <strong>posoudĂ jeho pĹ™ijatelnost</strong>. Po odeslánĂ proto - nepanikaĹ™, Ĺľe jej hned nevidĂš. - </span> - </p> - )} - - <div className="space-x-4"> - <Button - onClick={onAdd} - disabled={error || addingPost || addingProposal} - loading={addingPost || addingProposal} - fullwidth - hoverActive - > - {type === "post" && "PĹ™idat pĹ™ĂspÄ›vek"} - {type === "procedure-proposal" && "Navrhnout postup"} - </Button> - - <span className="text-sm text-grey-200 hidden lg:inline"> - Pro pokroÄŤilejšà formátovánĂ mĹŻĹľete pouĹľĂvat{" "} - <a - href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" - className="underline" - target="_blank" - rel="noreferrer noopener" - > - Markdown - </a> - . - </span> - </div> - </div> + </CardBody> + </Card> ); }; diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 929d73a..c6bdb8b 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -303,20 +303,12 @@ const Home = () => { <PostFilters /> </div> - <PostsContainer - className="container-padding--zero lg:container-padding--auto" - showAddPostCta={programEntry.discussionOpened} - /> {!programEntry.discussionOpened && (!isAuthenticated || (isAuthenticated && !user.isBanned)) && ( <p className="leading-normal"> Rozprava je uzavĹ™ena - pĹ™ĂspÄ›vky teÄŹ nelze pĹ™idávat. </p> )} - {programEntry.discussionOpened && - isAuthenticated && - !user.isBanned && <AddPostForm className="my-8 space-y-4" />} - {programEntry.discussionOpened && isAuthenticated && user.isBanned && ( @@ -325,6 +317,14 @@ const Home = () => { ti ho pĹ™edsedajĂcĂ odebere. </ErrorMessage> )} + {programEntry.discussionOpened && + isAuthenticated && + !user.isBanned && <AddPostForm className="mb-8" />} + + <PostsContainer + className="container-padding--zero lg:container-padding--auto" + showAddPostCta={programEntry.discussionOpened} + /> </section> </article> <ProgramEntryEditModal -- GitLab