diff --git a/assets/chevron-down.svg b/assets/chevron-down.svg new file mode 100644 index 00000000..cb9d8fe1 --- /dev/null +++ b/assets/chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/user.svg b/assets/user.svg new file mode 100644 index 00000000..c0094666 --- /dev/null +++ b/assets/user.svg @@ -0,0 +1 @@ +Asset 1 \ No newline at end of file diff --git a/components/Account.js b/components/Account.js new file mode 100644 index 00000000..7b9e0dda --- /dev/null +++ b/components/Account.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import Page from './Page'; +import styles from './Account.module.css'; + +export default function Account() { + const user = useSelector(state => state.user); + return ( + +

Account

+
username
+
{user.username}
+
+ ); +} diff --git a/components/Account.module.css b/components/Account.module.css new file mode 100644 index 00000000..3152b274 --- /dev/null +++ b/components/Account.module.css @@ -0,0 +1,4 @@ +.label { + font-size: var(--font-size-normal); + font-weight: 600; +} diff --git a/components/Button.module.css b/components/Button.module.css index 79213168..a03d3f8f 100644 --- a/components/Button.module.css +++ b/components/Button.module.css @@ -2,7 +2,8 @@ display: flex; justify-content: center; align-items: center; - background: #f5f5f5; + font-size: var(--font-size-normal); + background: var(--gray100); padding: 8px 16px; border-radius: 4px; border: 0; diff --git a/components/DropDown.js b/components/DropDown.js index 1e795d3d..9538d6f5 100644 --- a/components/DropDown.js +++ b/components/DropDown.js @@ -1,8 +1,18 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useRef } from 'react'; import classNames from 'classnames'; +import Menu from './Menu'; +import useDocumentClick from 'hooks/useDocumentClick'; +import Chevron from 'assets/chevron-down.svg'; import styles from './Dropdown.module.css'; +import Icon from './Icon'; -export default function DropDown({ value, options = [], onChange, className }) { +export default function DropDown({ + value, + className, + menuClassName, + options = [], + onChange = () => {}, +}) { const [showMenu, setShowMenu] = useState(false); const ref = useRef(); @@ -16,35 +26,19 @@ export default function DropDown({ value, options = [], onChange, className }) { onChange(value); } - useEffect(() => { - function hideMenu(e) { - if (!ref.current.contains(e.target)) { - setShowMenu(false); - } + useDocumentClick(e => { + if (!ref.current.contains(e.target)) { + setShowMenu(false); } - - document.addEventListener('click', hideMenu); - - return () => { - document.removeEventListener('click', hideMenu); - }; - }, [ref]); + }); return (
- {options.find(e => e.value === value).label} -
+ {options.find(e => e.value === value)?.label} + } size="S" className={styles.icon} />
- {showMenu && ( -
- {options.map(({ label, value }) => ( -
handleSelect(value, e)}> - {label} -
- ))} -
- )} + {showMenu && }
); } diff --git a/components/Dropdown.module.css b/components/Dropdown.module.css index 28f252c0..e78d544a 100644 --- a/components/Dropdown.module.css +++ b/components/Dropdown.module.css @@ -8,40 +8,14 @@ white-space: nowrap; position: relative; padding: 4px 32px 4px 16px; - border: 1px solid #b3b3b3; + border: 1px solid var(--gray500); border-radius: 4px; cursor: pointer; } -.menu { +.icon { position: absolute; - min-width: 100px; - top: 100%; - margin-top: 4px; - border: 1px solid #b3b3b3; - border-radius: 4px; - overflow: hidden; - z-index: 2; -} - -.option { - background: #fff; - padding: 4px 16px; - cursor: pointer; -} - -.option:hover { - background: #f5f5f5; -} - -.caret { - position: absolute; - height: 8px; - width: 8px; - border-right: 2px solid #8e8e8e; - border-bottom: 2px solid #8e8e8e; - transform: rotate(45deg); - top: -4px; + top: 0; bottom: 0; right: 12px; margin: auto; diff --git a/components/Header.js b/components/Header.js index ce7ca263..b9ba805b 100644 --- a/components/Header.js +++ b/components/Header.js @@ -2,6 +2,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import classNames from 'classnames'; import Link from 'components/Link'; +import UserButton from './UserButton'; import styles from './Header.module.css'; export default function Header() { @@ -11,16 +12,14 @@ export default function Header() {
- - umami - +
{user ? umami : 'umami'}
{user && (
- Dashboard + Dashboard Settings - Logout +
)} diff --git a/components/Icon.js b/components/Icon.js index e5e7c82b..c0073ab0 100644 --- a/components/Icon.js +++ b/components/Icon.js @@ -2,6 +2,16 @@ import React from 'react'; import classNames from 'classnames'; import styles from './Icon.module.css'; -export default function Icon({ icon, className }) { - return
{icon}
; +export default function Icon({ icon, className, size = 'M' }) { + return ( +
+ {icon} +
+ ); } diff --git a/components/Icon.module.css b/components/Icon.module.css index 72f4286e..27ba949f 100644 --- a/components/Icon.module.css +++ b/components/Icon.module.css @@ -5,7 +5,21 @@ vertical-align: middle; } -.icon > svg { +.icon svg { + fill: currentColor; +} + +.large > svg { + width: 24px; + height: 24px; +} + +.medium > svg { width: 16px; height: 16px; } + +.small > svg { + width: 12px; + height: 12px; +} diff --git a/components/Login.module.css b/components/Login.module.css index 113ec401..db0b1286 100644 --- a/components/Login.module.css +++ b/components/Login.module.css @@ -1,6 +1,5 @@ .form { position: absolute; - top: 50%; left: 50%; - transform: translate(-50%, -50%); + transform: translateX(-50%); } diff --git a/components/Menu.js b/components/Menu.js new file mode 100644 index 00000000..861980c5 --- /dev/null +++ b/components/Menu.js @@ -0,0 +1,24 @@ +import React from 'react'; +import classNames from 'classnames'; +import styles from './Menu.module.css'; + +export default function Menu({ options = [], className, align = 'left', onSelect = () => {} }) { + return ( +
+ {options.map(({ label, value, className: optionClassName }) => ( +
onSelect(value, e)} + > + {label} +
+ ))} +
+ ); +} diff --git a/components/Menu.module.css b/components/Menu.module.css new file mode 100644 index 00000000..ade8a8d0 --- /dev/null +++ b/components/Menu.module.css @@ -0,0 +1,31 @@ +.menu { + position: absolute; + min-width: 100px; + top: 100%; + margin-top: 4px; + border: 1px solid var(--gray500); + border-radius: 4px; + overflow: hidden; + z-index: 2; +} + +.option { + font-size: var(--font-size-small); + font-weight: normal; + background: #fff; + padding: 4px 16px; + cursor: pointer; + white-space: nowrap; +} + +.option:hover { + background: #f5f5f5; +} + +.left { + left: 0; +} + +.right { + right: 0; +} diff --git a/components/UserButton.js b/components/UserButton.js new file mode 100644 index 00000000..551c452c --- /dev/null +++ b/components/UserButton.js @@ -0,0 +1,56 @@ +import React, { useState, useRef } from 'react'; +import { useSelector } from 'react-redux'; +import { useRouter } from 'next/router'; +import Menu from './Menu'; +import Icon from './Icon'; +import useDocumentClick from 'hooks/useDocumentClick'; +import User from 'assets/user.svg'; +import Chevron from 'assets/chevron-down.svg'; +import styles from './UserButton.module.css'; + +export default function UserButton() { + const [showMenu, setShowMenu] = useState(false); + const user = useSelector(state => state.user); + const ref = useRef(); + const router = useRouter(); + + const menuOptions = [ + { + label: ( + <> + Logged in as {user.username} + + ), + value: 'username', + className: styles.username, + }, + { label: 'Account', value: 'account' }, + { label: 'Logout', value: 'logout' }, + ]; + + function handleSelect(value) { + setShowMenu(false); + + if (value === 'account') { + router.push('/account'); + } else if (value === 'logout') { + router.push('/logout'); + } + } + + useDocumentClick(e => { + if (!ref.current.contains(e.target)) { + setShowMenu(false); + } + }); + + return ( +
+
setShowMenu(state => !state)}> + } size="L" className={styles.icon} /> + } size="S" /> +
+ {showMenu && } +
+ ); +} diff --git a/components/UserButton.module.css b/components/UserButton.module.css new file mode 100644 index 00000000..bdb0daa5 --- /dev/null +++ b/components/UserButton.module.css @@ -0,0 +1,17 @@ +.container { + display: flex; + position: relative; + cursor: pointer; +} + +.icon { + margin-right: 8px; +} + +.username { + border-bottom: 1px solid var(--gray500); +} + +.username:hover { + background: var(--gray50); +} diff --git a/hooks/useDocumentClick.js b/hooks/useDocumentClick.js new file mode 100644 index 00000000..e1baae7e --- /dev/null +++ b/hooks/useDocumentClick.js @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; + +export default function useDocumentClick(handler) { + useEffect(() => { + document.addEventListener('click', handler); + + return () => { + document.removeEventListener('click', handler); + }; + }, [handler]); + + return null; +} diff --git a/hooks/useRequireLogin.js b/hooks/useRequireLogin.js index 1d9c037b..ffebfca8 100644 --- a/hooks/useRequireLogin.js +++ b/hooks/useRequireLogin.js @@ -31,7 +31,7 @@ export default function useRequireLogin() { return; } - await dispatch(updateUser({ user })); + await dispatch(updateUser(user)); setUser(user); setLoading(false); diff --git a/pages/account.js b/pages/account.js new file mode 100644 index 00000000..0b56dfdc --- /dev/null +++ b/pages/account.js @@ -0,0 +1,18 @@ +import React from 'react'; +import Layout from 'components/Layout'; +import Account from 'components/Account'; +import useRequireLogin from 'hooks/useRequireLogin'; + +export default function AccountPage() { + const { loading } = useRequireLogin(); + + if (loading) { + return null; + } + + return ( + + + + ); +} diff --git a/pages/dashboard.js b/pages/dashboard.js new file mode 100644 index 00000000..63c5be49 --- /dev/null +++ b/pages/dashboard.js @@ -0,0 +1,18 @@ +import React from 'react'; +import Layout from 'components/Layout'; +import WebsiteList from 'components/WebsiteList'; +import useRequireLogin from 'hooks/useRequireLogin'; + +export default function DashboardPage() { + const { loading } = useRequireLogin(); + + if (loading) { + return null; + } + + return ( + + + + ); +} diff --git a/pages/index.js b/pages/index.js index 2c99f2a4..7d93cef1 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,18 +1,12 @@ -import React from 'react'; -import Layout from 'components/Layout'; -import WebsiteList from 'components/WebsiteList'; -import useRequireLogin from 'hooks/useRequireLogin'; +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; -export default function HomePage() { - const { loading } = useRequireLogin(); +export default function DefaultPage() { + const router = useRouter(); - if (loading) { - return null; - } + useEffect(() => { + router.push('/dashboard'); + }, []); - return ( - - - - ); + return null; } diff --git a/styles/index.css b/styles/index.css index c5ac90d5..25ee6baf 100644 --- a/styles/index.css +++ b/styles/index.css @@ -47,11 +47,13 @@ form label { min-width: 100px; } -form input, -form textarea { +input, +textarea { padding: 4px 8px; margin-right: 10px; margin-bottom: 20px; + font-size: var(--font-size-normal); + line-height: 1.8; border: 1px solid var(--gray500); border-radius: 4px; outline: none; @@ -59,7 +61,7 @@ form textarea { select { padding: 4px 8px; - border: 1px solid var(--gray500); + border: 1px solid var(--gray500) !important; border-radius: 4px; }