Added user button and menu.

This commit is contained in:
Mike Cao 2020-08-05 23:03:07 -07:00
parent a5930f1772
commit 6e23a8a53b
21 changed files with 268 additions and 83 deletions

1
assets/chevron-down.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M441.9 167.3l-19.8-19.8c-4.7-4.7-12.3-4.7-17 0L224 328.2 42.9 147.5c-4.7-4.7-12.3-4.7-17 0L6.1 167.3c-4.7 4.7-4.7 12.3 0 17l209.4 209.4c4.7 4.7 12.3 4.7 17 0l209.4-209.4c4.7-4.7 4.7-12.3 0-17z"/></svg>

After

Width:  |  Height:  |  Size: 272 B

1
assets/user.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1652 1652"><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M1587.07,504.47A828.56,828.56,0,1,0,1652,826,823.13,823.13,0,0,0,1587.07,504.47ZM826,1577a747.29,747.29,0,0,1-464.48-161.26,39.94,39.94,0,0,0,2.8-11.35,458.82,458.82,0,0,1,34.29-135.74,464.15,464.15,0,0,1,854.78,0,458.82,458.82,0,0,1,34.29,135.74,39.94,39.94,0,0,0,2.8,11.35A747.29,747.29,0,0,1,826,1577ZM719.81,866.57A274,274,0,1,1,826,888,272.1,272.1,0,0,1,719.81,866.57Zm641.28,485.87c-36.11-201.1-182.78-363.82-374.86-423,114.28-58.37,192.53-177.22,192.53-314.35,0-194.83-157.94-352.76-352.76-352.76S473.24,420.29,473.24,615.12c0,137.13,78.25,256,192.53,314.35-192.08,59.15-338.75,221.87-374.86,423C157.46,1216.81,75,1030.86,75,826,75,411.9,411.9,75,826,75s751,336.9,751,751C1577,1030.86,1494.54,1216.81,1361.09,1352.44Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 910 B

15
components/Account.js Normal file
View File

@ -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 (
<Page>
<h2>Account</h2>
<div className={styles.label}>username</div>
<div>{user.username}</div>
</Page>
);
}

View File

@ -0,0 +1,4 @@
.label {
font-size: var(--font-size-normal);
font-weight: 600;
}

View File

@ -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;

View File

@ -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 (
<div ref={ref} className={classNames(styles.dropdown, className)} onClick={handleShowMenu}>
<div className={styles.value}>
{options.find(e => e.value === value).label}
<div className={styles.caret} />
{options.find(e => e.value === value)?.label}
<Icon icon={<Chevron />} size="S" className={styles.icon} />
</div>
{showMenu && (
<div className={styles.menu}>
{options.map(({ label, value }) => (
<div key={value} className={styles.option} onClick={e => handleSelect(value, e)}>
{label}
</div>
))}
</div>
)}
{showMenu && <Menu className={menuClassName} options={options} onSelect={handleSelect} />}
</div>
);
}

View File

@ -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;

View File

@ -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() {
<header className={classNames(styles.header, 'container')}>
<div className="row align-items-center">
<div className="col">
<Link href="/" className={styles.title}>
umami
</Link>
<div className={styles.title}>{user ? <Link href="/">umami</Link> : 'umami'}</div>
</div>
{user && (
<div className="col">
<div className={styles.nav}>
<Link href="/">Dashboard</Link>
<Link href="/dashboard">Dashboard</Link>
<Link href="/settings">Settings</Link>
<Link href="/logout">Logout</Link>
<UserButton />
</div>
</div>
)}

View File

@ -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 <div className={classNames(styles.icon, className)}>{icon}</div>;
export default function Icon({ icon, className, size = 'M' }) {
return (
<div
className={classNames(styles.icon, className, {
[styles.large]: size === 'L',
[styles.medium]: size === 'M',
[styles.small]: size === 'S',
})}
>
{icon}
</div>
);
}

View File

@ -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;
}

View File

@ -1,6 +1,5 @@
.form {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform: translateX(-50%);
}

24
components/Menu.js Normal file
View File

@ -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 (
<div
className={classNames(styles.menu, className, {
[styles.left]: align === 'left',
[styles.right]: align === 'right',
})}
>
{options.map(({ label, value, className: optionClassName }) => (
<div
key={value}
className={classNames(styles.option, optionClassName)}
onClick={e => onSelect(value, e)}
>
{label}
</div>
))}
</div>
);
}

View File

@ -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;
}

56
components/UserButton.js Normal file
View File

@ -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 <b>{user.username}</b>
</>
),
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 (
<div ref={ref} className={styles.container}>
<div onClick={() => setShowMenu(state => !state)}>
<Icon icon={<User />} size="L" className={styles.icon} />
<Icon icon={<Chevron />} size="S" />
</div>
{showMenu && <Menu options={menuOptions} onSelect={handleSelect} align="right" />}
</div>
);
}

View File

@ -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);
}

13
hooks/useDocumentClick.js Normal file
View File

@ -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;
}

View File

@ -31,7 +31,7 @@ export default function useRequireLogin() {
return;
}
await dispatch(updateUser({ user }));
await dispatch(updateUser(user));
setUser(user);
setLoading(false);

18
pages/account.js Normal file
View File

@ -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 (
<Layout>
<Account />
</Layout>
);
}

18
pages/dashboard.js Normal file
View File

@ -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 (
<Layout>
<WebsiteList />
</Layout>
);
}

View File

@ -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 (
<Layout>
<WebsiteList />
</Layout>
);
return null;
}

View File

@ -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;
}