diff options
Diffstat (limited to 'public/projects/react-small-apps/apps/notebook/src/components')
23 files changed, 691 insertions, 0 deletions
diff --git a/public/projects/react-small-apps/apps/notebook/src/components/commons/Button/Button.css b/public/projects/react-small-apps/apps/notebook/src/components/commons/Button/Button.css new file mode 100644 index 0000000..16268d3 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/commons/Button/Button.css @@ -0,0 +1,78 @@ +.btn { + cursor: pointer; +} + +.btn--delete { + background: hsl(0, 44%, 44%); + border: none; + border-radius: 50%; + box-shadow: 0 0 0 2px hsl(0, 44%, 29%), 1px 2px 1px 2px hsl(0, 44%, 29%); + width: 2.4rem; + height: 2.4rem; + padding: 0.5rem; + transition: transform 0.3s ease-in-out 0s; +} + +.btn--delete:hover, +.btn--delete:focus { + background: hsl(0, 44%, 50%); + transform: scale(1.1); +} + +.btn--delete:active { + background: hsl(0, 44%, 40%); + transform: scale(1); +} + +.btn .icon { + height: 100%; + width: 100%; +} + +.btn--delete #trash-lid-handle { + stroke: #fff; + stroke-width: 4; +} + +.btn--delete #trash-container, +.btn--delete #trash-lid { + fill: hsl(0, 44%, 49%); + stroke: #fff; + stroke-width: 5; +} + +.btn--delete #trash-stroke1, +.btn--delete #trash-stroke2, +.btn--delete #trash-stroke3 { + fill: #fff; + stroke: #fff; + stroke-width: 1; +} + +.btn--restore { + background: hsl(212, 44%, 44%); + border: none; + border-radius: 50%; + box-shadow: 0 0 0 2px hsl(212, 44%, 29%), 1px 2px 1px 2px hsl(212, 44%, 29%); + width: 2.4rem; + height: 2.4rem; + padding: 0.5rem; + transition: transform 0.3s ease-in-out 0s; +} + +.btn--restore:hover { + background: hsl(212, 44%, 50%); + transform: scale(1.1); +} + +.btn--restore:active { + background: hsl(212, 44%, 40%); + transform: scale(1); +} + +.btn--restore #restore-circle, +.btn--restore #restore-arrow, +.btn--restore #restore-first-clock-hand, +.btn--restore #restore-second-clock-hand { + fill: #fff; +} diff --git a/public/projects/react-small-apps/apps/notebook/src/components/commons/Button/Button.js b/public/projects/react-small-apps/apps/notebook/src/components/commons/Button/Button.js new file mode 100644 index 0000000..4580815 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/commons/Button/Button.js @@ -0,0 +1,26 @@ +import "./Button.css"; + +function Button({ + children, + onClickHandler, + onBlurHandler, + modifier, + additionalClassnames, +}) { + let classNames = modifier ? `btn btn--${modifier}` : "btn"; + classNames = additionalClassnames + ? `${classNames} ${additionalClassnames}` + : classNames; + + return ( + <button + className={classNames} + onClick={onClickHandler} + onBlur={onBlurHandler} + > + {children} + </button> + ); +} + +export default Button; diff --git a/public/projects/react-small-apps/apps/notebook/src/components/commons/FormElements/Input/Input.js b/public/projects/react-small-apps/apps/notebook/src/components/commons/FormElements/Input/Input.js new file mode 100644 index 0000000..7d8cb45 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/commons/FormElements/Input/Input.js @@ -0,0 +1,31 @@ +import { forwardRef } from "react"; + +function Input( + { + type = "text", + name, + value, + onChangeHandler, + onBlurHandler, + additionalClasses, + }, + ref +) { + const classNames = additionalClasses + ? `form__input ${additionalClasses}` + : "form__input"; + + return ( + <input + ref={ref} + type={type} + name={name} + className={classNames} + value={value} + onChange={onChangeHandler} + onBlur={onBlurHandler} + /> + ); +} + +export default forwardRef(Input); diff --git a/public/projects/react-small-apps/apps/notebook/src/components/commons/FormElements/TextArea/TextArea.js b/public/projects/react-small-apps/apps/notebook/src/components/commons/FormElements/TextArea/TextArea.js new file mode 100644 index 0000000..ca2a52e --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/commons/FormElements/TextArea/TextArea.js @@ -0,0 +1,52 @@ +import { forwardRef, useEffect, useState } from "react"; + +function autoGrow(field, initialValue = null) { + let fieldHeight = initialValue ?? field.style.height; + if (field.scrollHeight > field.clientHeight) { + fieldHeight = field.scrollHeight + "px"; + } + return fieldHeight; +} + +function isSetHeightNeeded(e) { + const key = e.key; + const isBackspace = key === "Backspace"; + const isDelete = key === "Delete"; + const isCtrlZ = e.ctrlKey && e.key === "z"; + const isCut = e.ctrlKey && e.key === "x"; + return isBackspace || isDelete || isCtrlZ || isCut; +} + +function TextArea( + { value, name, onBlurHandler, onChangeHandler, additionalClasses }, + ref +) { + const [fieldHeight, setFieldHeight] = useState(); + const classNames = additionalClasses + ? `form__textarea ${additionalClasses}` + : "form__textarea"; + + useEffect(() => { + ref && setFieldHeight(autoGrow(ref.current)); + }, [ref]); + + return ( + <textarea + ref={ref} + className={classNames} + name={name} + value={value} + onChange={(e) => { + onChangeHandler(e); + setFieldHeight(autoGrow(e.target)); + }} + onKeyDown={(e) => { + if (isSetHeightNeeded(e)) setFieldHeight(autoGrow(e.target, "auto")); + }} + onBlur={onBlurHandler} + style={{ height: fieldHeight }} + /> + ); +} + +export default forwardRef(TextArea); diff --git a/public/projects/react-small-apps/apps/notebook/src/components/commons/FormElements/index.js b/public/projects/react-small-apps/apps/notebook/src/components/commons/FormElements/index.js new file mode 100644 index 0000000..1d1f610 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/commons/FormElements/index.js @@ -0,0 +1,4 @@ +import Input from "./Input/Input"; +import TextArea from "./TextArea/TextArea"; + +export { Input, TextArea }; diff --git a/public/projects/react-small-apps/apps/notebook/src/components/commons/List/List.css b/public/projects/react-small-apps/apps/notebook/src/components/commons/List/List.css new file mode 100644 index 0000000..ae897a5 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/commons/List/List.css @@ -0,0 +1,3 @@ +.list__item + .list__item { + margin-top: 0.5rem; +} diff --git a/public/projects/react-small-apps/apps/notebook/src/components/commons/List/List.js b/public/projects/react-small-apps/apps/notebook/src/components/commons/List/List.js new file mode 100644 index 0000000..631e6a5 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/commons/List/List.js @@ -0,0 +1,25 @@ +import "./List.css"; + +function List({ type = "ul", data = [], modifier = "" }) { + const classNames = modifier ? `list list--${modifier}` : "list"; + + const listItems = data.map((object) => { + return ( + <li key={object.id} className="list__item"> + {object.body} + </li> + ); + }); + + return ( + <> + {type === "ol" ? ( + <ol className={classNames}>{listItems}</ol> + ) : ( + <ul className={classNames}>{listItems}</ul> + )} + </> + ); +} + +export default List; diff --git a/public/projects/react-small-apps/apps/notebook/src/components/commons/index.js b/public/projects/react-small-apps/apps/notebook/src/components/commons/index.js new file mode 100644 index 0000000..f0ca17b --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/commons/index.js @@ -0,0 +1,5 @@ +import Button from "./Button/Button"; +import { Input, TextArea } from "./FormElements"; +import List from "./List/List"; + +export { Button, Input, List, TextArea }; diff --git a/public/projects/react-small-apps/apps/notebook/src/components/helpers/hooks/useToggle.js b/public/projects/react-small-apps/apps/notebook/src/components/helpers/hooks/useToggle.js new file mode 100644 index 0000000..0291324 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/helpers/hooks/useToggle.js @@ -0,0 +1,10 @@ +import { useCallback, useState } from "react"; + +function useToggle(initialState = false) { + const [state, setState] = useState(initialState); + const toggle = useCallback(() => setState((state) => !state), []); + + return [state, toggle]; +} + +export default useToggle; diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/Footer/Footer.css b/public/projects/react-small-apps/apps/notebook/src/components/layout/Footer/Footer.css new file mode 100644 index 0000000..31db439 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/Footer/Footer.css @@ -0,0 +1,8 @@ +.footer { + padding: 0 0 1rem; +} + +.footer__copyright { + font-size: 0.9rem; + text-align: center; +} diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/Footer/Footer.js b/public/projects/react-small-apps/apps/notebook/src/components/layout/Footer/Footer.js new file mode 100644 index 0000000..20a87f2 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/Footer/Footer.js @@ -0,0 +1,11 @@ +import "./Footer.css"; + +function Footer() { + return ( + <footer className="footer"> + <p className="footer__copyright">Notebook. MIT 2021. Armand Philippot.</p> + </footer> + ); +} + +export default Footer; diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/Header/Header.css b/public/projects/react-small-apps/apps/notebook/src/components/layout/Header/Header.css new file mode 100644 index 0000000..a413a4f --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/Header/Header.css @@ -0,0 +1,7 @@ +.header { + padding: 1rem 0 0; +} + +.header__branding { + font-size: 2.5rem; +} diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/Header/Header.js b/public/projects/react-small-apps/apps/notebook/src/components/layout/Header/Header.js new file mode 100644 index 0000000..03757fa --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/Header/Header.js @@ -0,0 +1,12 @@ +import "./Header.css"; + +function Header({ children }) { + return ( + <header className="header"> + <h1 className="header__branding">Notebook</h1> + {children} + </header> + ); +} + +export default Header; diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/Main/Main.css b/public/projects/react-small-apps/apps/notebook/src/components/layout/Main/Main.css new file mode 100644 index 0000000..f6ee8fe --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/Main/Main.css @@ -0,0 +1,3 @@ +.main { + flex: 1; +} diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/Main/Main.js b/public/projects/react-small-apps/apps/notebook/src/components/layout/Main/Main.js new file mode 100644 index 0000000..23e7b9d --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/Main/Main.js @@ -0,0 +1,7 @@ +import "./Main.css"; + +function Main({ children }) { + return <main className="main">{children}</main>; +} + +export default Main; diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/Nav/Nav.css b/public/projects/react-small-apps/apps/notebook/src/components/layout/Nav/Nav.css new file mode 100644 index 0000000..9f5f90c --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/Nav/Nav.css @@ -0,0 +1,65 @@ +.nav { + position: relative; + display: flex; + flex-flow: row wrap; + justify-content: center; + margin: 2rem auto 0; +} + +.nav .list--nav { + list-style-type: none; + position: absolute; + bottom: 100%; + width: 80vw; + margin: 0; + padding: 1rem; + background: #fff; + border: 1px solid #ccc; +} + +@media screen and (min-width: 1024px) { + .nav .list--nav { + width: 50%; + } +} + +.nav__link { + background: #fff; + border: none; + color: hsl(212, 46%, 34%); + text-decoration: underline; + display: inline-block; + margin: 0 1px; + padding: 0.8rem 1rem; +} + +.nav .list__link { + display: block; + padding: 0.2rem; +} + +.nav .list__link--current { + background: hsl(212, 46%, 34%); + color: #fff; +} + +.nav__link:hover, +.nav__link:focus, +.nav .list__link:hover, +.nav .list__link:focus { + text-decoration-thickness: 4px; +} + +.nav__link:focus, +.nav .list__link:focus { + color: inherit; +} + +.nav .list__link--current:focus { + color: hsl(0, 0%, 89%); +} + +.nav__link:active { + color: hsl(212, 46%, 20%); + text-decoration-thickness: 2px; +} diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/Nav/Nav.js b/public/projects/react-small-apps/apps/notebook/src/components/layout/Nav/Nav.js new file mode 100644 index 0000000..4e2a916 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/Nav/Nav.js @@ -0,0 +1,62 @@ +import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { Button } from "../../commons"; +import NavJump from "./NavJump"; +import "./Nav.css"; + +function Nav({ pages, currentPage, addNewPage, isPageExists }) { + const [isJumpEnabled, setIsJumpEnabled] = useState(false); + + const isCover = () => currentPage && currentPage.id === 0; + const isFirstPage = () => currentPage && currentPage.id === 1; + const is404 = () => currentPage && currentPage.id === null; + + useEffect(() => { + setIsJumpEnabled(false); + }, [currentPage.id]); + + return ( + <nav + className="nav" + onBlur={(e) => !e.relatedTarget && setIsJumpEnabled(false)} + > + {!isCover() && ( + <Link className="nav__link" to="/" onClick={(e) => e.target.blur()}> + Back to cover + </Link> + )} + {!isCover() && !isFirstPage() && isPageExists(currentPage.id - 1) && ( + <Link + className="nav__link" + to={`/page/${currentPage.id - 1}`} + onFocus={() => setIsJumpEnabled(false)} + onClick={(e) => e.target.blur()} + > + Previous page + </Link> + )} + <Button + additionalClassnames="nav__link" + onClickHandler={() => setIsJumpEnabled(!isJumpEnabled)} + > + Jump to + </Button> + {isJumpEnabled && <NavJump pages={pages} />} + {!is404() && ( + <Link + className="nav__link" + to={`/page/${currentPage.id + 1}`} + onClick={(e) => { + !isPageExists(currentPage.id + 1) && addNewPage(); + e.target.blur(); + }} + onFocus={() => setIsJumpEnabled(false)} + > + Next page + </Link> + )} + </nav> + ); +} + +export default Nav; diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/Nav/NavJump.js b/public/projects/react-small-apps/apps/notebook/src/components/layout/Nav/NavJump.js new file mode 100644 index 0000000..9d2a049 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/Nav/NavJump.js @@ -0,0 +1,28 @@ +import { NavLink } from "react-router-dom"; +import { List } from "../../commons"; + +function NavJump({ pages }) { + const links = pages + .filter((page) => page.id > 0) + .map((page) => { + return { + id: page.id, + body: ( + <NavLink + key={page.id} + className={({ isActive }) => + isActive ? "list__link--current" : "list__link" + } + aria-current="page" + to={page.url} + > + {page.title} + </NavLink> + ), + }; + }); + + return <List data={links} modifier="nav" />; +} + +export default NavJump; diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/Page/Cover.css b/public/projects/react-small-apps/apps/notebook/src/components/layout/Page/Cover.css new file mode 100644 index 0000000..bf915dd --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/Page/Cover.css @@ -0,0 +1,30 @@ +.notebook--cover .notebook-page--mirror { + display: none; +} + +.notebook--cover .notebook-page { + grid-column: 1 / -1; + background: hsl(0, 4%, 20%); + color: rgb(234, 235, 236); + letter-spacing: 1px; + text-shadow: 1px 1px 0 #000; + box-shadow: none; +} + +.notebook--cover .notebook-page { + justify-content: center; + text-align: center; +} + +.notebook--cover .notebook-page__title { + font-size: 3rem; + font-weight: 600; +} + +.notebook--cover .notebook-page .notebook-page__content { + flex: 0; +} + +.notebook--cover .notebook-page .notebook-page__title .form__input { + text-align: center; +} diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/Page/Page.css b/public/projects/react-small-apps/apps/notebook/src/components/layout/Page/Page.css new file mode 100644 index 0000000..873df47 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/Page/Page.css @@ -0,0 +1,83 @@ +.notebook-page { + display: flex; + flex-flow: column nowrap; + padding: clamp(1rem, 3vw, 2rem) 0; + background: #fff; + border: 1px solid #cacaca; + box-shadow: 1px 1px 0 0 #ebebeb, 1px 1px 0 1px #bebebe, 2px 2px 0 1px #ebebeb, + 2px 2px 0 2px #bebebe, 3px 3px 0 2px #ebebeb, 3px 3px 0 3px #bebebe; +} + +.notebook-page--mirror { + box-shadow: -1px 1px 0 0 #ebebeb, -1px 1px 0 1px #bebebe, + -2px 2px 0 1px #ebebeb, -2px 2px 0 2px #bebebe, -3px 3px 0 2px #ebebeb, + -3px 3px 0 3px #bebebe; +} + +@media screen and (max-width: 1023px) { + .notebook-page { + grid-column: 1/-1; + } + .notebook-page--mirror { + display: none; + } +} + +.notebook-page__header, +.notebook-page__footer, +.notebook-page__content { + padding: 0 3rem 0 2rem; +} + +.notebook-page__title { + font-size: 1.8rem; + font-weight: 600; + margin: 1rem 0 0; +} + +.notebook-page__title .form__input { + border: none; + font-weight: inherit; + padding: 0; + width: 100%; +} + +.notebook-page__title .form__input:focus { + outline: none; +} + +.notebook-page__content { + flex: 1; + display: flex; + margin: clamp(1rem, 3vw, 2rem) 0 clamp(2rem, 3vw, 3rem); + white-space: pre-wrap; + word-break: break-all; + hyphens: auto; +} + +.notebook-page__content .form__textarea { + border: none; + line-height: inherit; + width: 100%; + height: 100%; + overflow: hidden; + padding: 0; +} + +.notebook-page__content .form__textarea:focus { + outline: none; + resize: none; +} + +.notebook-page__footer { + text-align: right; +} + +.notebook-page__toolbar { + display: flex; + flex-flow: row; + gap: 1rem; + position: absolute; + top: -4rem; + right: 1rem; +} diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/Page/Page.js b/public/projects/react-small-apps/apps/notebook/src/components/layout/Page/Page.js new file mode 100644 index 0000000..19e072c --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/Page/Page.js @@ -0,0 +1,107 @@ +import { useEffect, useRef } from "react"; +import { Input, TextArea } from "../../commons"; +import useToggle from "../../helpers/hooks/useToggle"; +import PageToolbar from "./PageToolbar"; +import "./Cover.css"; +import "./Page.css"; + +function Page({ page, setPage, removePage, restorePage, deletedPages }) { + const [isTitleEditable, setIsTitleEditable] = useToggle(); + const [isBodyEditable, setIsBodyEditable] = useToggle(); + const inputRef = useRef(null); + const textareaRef = useRef(null); + + const isCover = () => page && page.id === 0; + const is404 = () => page && page.id === null; + + useEffect(() => { + inputRef.current && inputRef.current.focus(); + textareaRef.current && textareaRef.current.focus(); + }); + + const handleSubmit = (e) => { + e.preventDefault(); + }; + + const handleOnChange = (e) => { + let newValue = {}; + + switch (e.target.name) { + case "notebook-title": + newValue = { title: e.target.value }; + break; + case "notebook-body": + newValue = { body: e.target.value }; + break; + default: + break; + } + + setPage((previous) => { + return { ...previous, ...newValue }; + }); + }; + + return ( + <article + className={`notebook-page ${isCover() ? "notebook-page--cover" : ""}`} + > + <header className="notebook-page__header"> + {!isTitleEditable && ( + <h2 + className="notebook-page__title" + onClick={() => { + if (!is404()) setIsTitleEditable(); + }} + > + {page.title} + </h2> + )} + {isTitleEditable && ( + <form className="notebook-page__title" onSubmit={handleSubmit}> + <Input + ref={inputRef} + name="notebook-title" + value={page.title} + onChangeHandler={handleOnChange} + onBlurHandler={setIsTitleEditable} + /> + </form> + )} + </header> + {!isBodyEditable && ( + <div + className="notebook-page__content" + onClick={() => { + if (!is404()) setIsBodyEditable(); + }} + > + {page.body} + </div> + )} + {isBodyEditable && ( + <form className="notebook-page__content" onSubmit={handleSubmit}> + <TextArea + ref={textareaRef} + name="notebook-body" + value={page.body} + onChangeHandler={handleOnChange} + onBlurHandler={setIsBodyEditable} + /> + </form> + )} + <footer className="notebook-page__footer"> + {!isCover() && <div className="notebook-page__number">{page.id}</div>} + {!isCover() && ( + <PageToolbar + removePage={removePage} + restorePage={restorePage} + deletedPages={deletedPages} + /> + )} + </footer> + </article> + ); +} + +export default Page; diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/Page/PageToolbar.js b/public/projects/react-small-apps/apps/notebook/src/components/layout/Page/PageToolbar.js new file mode 100644 index 0000000..a16aa22 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/Page/PageToolbar.js @@ -0,0 +1,27 @@ +import { Button } from "../../commons"; +import { ReactComponent as TrashIcon } from "../../../images/trash.svg"; +import { ReactComponent as RestoreIcon } from "../../../images/restore.svg"; + +function PageToolbar({ removePage, restorePage, deletedPages }) { + return ( + <div className="notebook-page__toolbar toolbar"> + <div className="toolbar__item"> + {deletedPages.length > 0 && ( + <Button modifier="restore" onClickHandler={restorePage}> + <RestoreIcon + title="Undo page deletion" + className="icon icon--restore" + /> + </Button> + )} + </div> + <div className="toolbar__item"> + <Button modifier="delete" onClickHandler={removePage}> + <TrashIcon title="Delete this page" className="icon icon--trash" /> + </Button> + </div> + </div> + ); +} + +export default PageToolbar; diff --git a/public/projects/react-small-apps/apps/notebook/src/components/layout/index.js b/public/projects/react-small-apps/apps/notebook/src/components/layout/index.js new file mode 100644 index 0000000..1b8d583 --- /dev/null +++ b/public/projects/react-small-apps/apps/notebook/src/components/layout/index.js @@ -0,0 +1,7 @@ +import Footer from "./Footer/Footer"; +import Header from "./Header/Header"; +import Main from "./Main/Main"; +import Nav from "./Nav/Nav"; +import Page from "./Page/Page"; + +export { Footer, Header, Main, Nav, Page }; |
