diff options
Diffstat (limited to 'public/projects/react-small-apps/apps/notebook/src/components/layout')
14 files changed, 457 insertions, 0 deletions
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 }; |
