diff options
Diffstat (limited to 'components')
-rw-r--r-- | components/App.js | 38 | ||||
-rw-r--r-- | components/Form.js | 46 | ||||
-rw-r--r-- | components/Header.js | 132 | ||||
-rw-r--r-- | components/Layout.js | 67 | ||||
-rw-r--r-- | components/Popup.js | 32 | ||||
-rw-r--r-- | components/index.js | 4 |
6 files changed, 319 insertions, 0 deletions
diff --git a/components/App.js b/components/App.js new file mode 100644 index 0000000..02443f3 --- /dev/null +++ b/components/App.js @@ -0,0 +1,38 @@ +import React, {useState, useEffect, useRef} from 'react' +import useUser from 'lib/useUser' +import fetchJson from 'lib/fetchJson' +import {close, toggleMin, toggleMax, move} from 'helpers/windowActions' +import {Layout} from 'components' + +const App = ({children, app, apps, setApps}) => { + const winRef = useRef(null); + const [errorMsg, setErrorMsg] = useState('') + + useEffect(() => { + move(app.name, winRef, apps, setApps) + }, []) + + return ( + <> + <div + ref={winRef} + className={ + 'list window' + + (app.min ? ' hidden' : '') + + (app.max ? ' maximized' : '') + } + style={app.pos.length ? {top: app.pos[1], left: app.pos[0]} : {}} + > + <div className='window__title'>Notes</div> + <div className='window__title-buttons'> + <span onClick={() => toggleMin('Notes', apps, setApps)}>_</span> + <span onClick={() => toggleMax('Notes', apps, setApps)}>+</span> + <span onClick={() => close('Notes', apps, setApps)}>x</span> + </div> + <div className='window__content'>{children}</div> + </div> + </> + ) +} + +export default App; diff --git a/components/Form.js b/components/Form.js new file mode 100644 index 0000000..fc14a75 --- /dev/null +++ b/components/Form.js @@ -0,0 +1,46 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const Form = ({errorMessage, onSubmit, isLogin}) => ( + <form className='window window--popup' onSubmit={onSubmit}> + <div className="window__content--popup"> + {isLogin ? 'Login to access your notes' : 'Register new user'} + </div> + <input type="email" name="email" placeholder="email" required /> + <input type="password" name="password" minLength="6" placeholder="password" required /> + + <input className='window__button' type="submit" value={isLogin ? 'Login' : 'Register'} /> + + {errorMessage && <p className="error">{errorMessage}</p>} + + <style jsx>{` + form, + label { + display: flex; + flex-flow: column; + } + label > span { + font-weight: 600; + } + input[type=email], + input[type=password] { + padding: .5em; + margin: .5em 0; + border: 1px solid #ccc; + border-radius: .25px; + } + .error { + text-align: center; + color: brown; + margin: 1rem 0 0; + } + `}</style> + </form> +) + +export default Form + +Form.propTypes = { + errorMessage: PropTypes.string, + onSubmit: PropTypes.func, +} diff --git a/components/Header.js b/components/Header.js new file mode 100644 index 0000000..4302af9 --- /dev/null +++ b/components/Header.js @@ -0,0 +1,132 @@ +import React, {useState} from 'react' +import Link from 'next/link' +import useUser from 'lib/useUser' +import fetchJson from 'lib/fetchJson' +import {toggleMin} from 'helpers/windowActions' +import {useRouter} from 'next/router' + +const Header = ({apps, setApps}) => { + const [userMenu, setUserMenu] = useState(false); + const {user, mutateUser} = useUser() + const router = useRouter() + + const handleLogout = async (e) => { + e.preventDefault() + mutateUser( + await fetchJson('/api/logout', {method: 'POST'}), + false + ) + router.push('/login') + } + + return ( + <header> + <nav> + <ul className='header__apps'> + { + apps && apps.map(app => ( + <li + key={app.name} + style={app.min ? {fontWeight: 600} : {}} + onClick={() => toggleMin(app.name, apps, setApps)} + > + {app.name} + </li> + )) + } + </ul> + <ul> + {!user?.isLoggedIn && ( + <li> + <Link href="/register"> + <a>Register</a> + </Link> + </li> + )} + {!user?.isLoggedIn && ( + <li> + <Link href="/login"> + <a>Login</a> + </Link> + </li> + )} + {user?.isLoggedIn && ( + <li> + <p + className='user-menu' + onClick={() => setUserMenu(!userMenu)} + >{user?.email}</p> + { + userMenu && ( + <ul className='user-submenu'> + <li> + <a href="/api/logout" onClick={handleLogout}> + Logout + </a> + </li> + </ul> + ) + } + </li> + )} + </ul> + </nav> + <style jsx>{` + header { + height: 2em; + padding: 0.5rem; + background-color: rgba(255, 255, 255, 0.4); + border-bottom: 1px solid rgba(255, 255, 255, 0.5) + } + + nav { + display: flex; + } + + .header__apps { + flex-grow: 1; + overflow: auto; + } + + li { + margin-left: 1em; + margin-right: 1em; + display: inline-block; + cursor: pointer; + } + + .header__separator { + margin-right: auto; + } + + a { + color: #333; + font-weight: 600; + text-decoration: none; + align-items: center; + } + + .user-menu { + font-weight: 600; + cursor: pointer; + position: relative; + } + + .user-submenu { + position: absolute; + right: 0; + top: 2.1em; + width: 10em; + padding: .5em; + background-color: rgba(255, 255, 255, .9); + border: 1px solid rgba(255, 255, 255, .5) + } + .user-submenu a { + text-align: right; + } + `}</style> + </header> + ) +} + +export default Header diff --git a/components/Layout.js b/components/Layout.js new file mode 100644 index 0000000..f77ea0c --- /dev/null +++ b/components/Layout.js @@ -0,0 +1,67 @@ +import React, {useState} from 'react' +import Head from 'next/head' +import Context from '../context'; +import Header from './Header' +import Popup from './Popup' +import PropTypes from 'prop-types' + +const Layout = ({ children, apps, setApps}) => { + const [popup, setPopup] = useState({}) + + return ( + <Context.Provider value={{ setPopup }}> + <Head> + <title>Notes App</title> + </Head> + <main> + <div className="container">{children}</div> + </main> + <Header apps={apps} setApps={setApps} /> + <Popup popup={popup} /> + <style jsx global>{` + main { + position: fixed; + top: 2em; + left: 0; + bottom: 0; + right: 0; + } + + body { + margin: 0; + color: #222; + height: 100vh; + overflow: hidden; + + background: #50a3a2; + background: -webkit-linear-gradient(top left, #50a3a2 0%, #53e3a6 100%); + background: -moz-linear-gradient(top left, #50a3a2 0%, #53e3a6 100%); + background: -o-linear-gradient(top left, #50a3a2 0%, #53e3a6 100%); + background: linear-gradient(to bottom right, #50a3a2 0%, #53e3a6 100%); + + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, Noto Sans, sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + } + + textarea, input { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, Noto Sans, sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + } + + .container { + margin: 1.5rem auto; + padding-left: 2rem; + padding-right: 2rem; + } + `}</style> + </Context.Provider> + ) +} + +export default Layout + +Layout.propTypes = { + children: PropTypes.node, +} diff --git a/components/Popup.js b/components/Popup.js new file mode 100644 index 0000000..911d2fa --- /dev/null +++ b/components/Popup.js @@ -0,0 +1,32 @@ +import React, {useState, useEffect} from 'react' + +const Popup = ({popup}) => { + const [visible, setVisible] = useState(false) + const { + content = null, + time = null, + error = null, + yes = null, + no = null, + } = popup + + useEffect(() => { + setVisible(true) + time && setTimeout(() => setVisible(false), time) + }, [popup]) + + if (!content) return null + + return visible ? ( + <div className={`window window--popup${error ? ' window--error' : ''}`}> + <div className="window__content--popup">{ content }</div> + { + (yes || no) && (<div className="window__buttons--popup"> + {[yes, no].map(a => a && <input key={a.label} className='window__button' type="button" onClick={() => { setVisible(false); a.action() }} value={a.label} />)} + </div>) + } + </div> + ) : null +} + +export default Popup diff --git a/components/index.js b/components/index.js new file mode 100644 index 0000000..4875559 --- /dev/null +++ b/components/index.js @@ -0,0 +1,4 @@ +export {default as Form} from './Form' +export {default as Header} from './Header' +export {default as Layout} from './Layout' +export {default as App} from './App' |