Skip to content

Commit

Permalink
Added user button and menu.
Browse files Browse the repository at this point in the history
  • Loading branch information
mikecao committed Aug 6, 2020
1 parent a5930f1 commit 6e23a8a
Show file tree
Hide file tree
Showing 21 changed files with 268 additions and 83 deletions.
1 change: 1 addition & 0 deletions assets/chevron-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/user.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions components/Account.js
Original file line number Diff line number Diff line change
@@ -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>
);
}
4 changes: 4 additions & 0 deletions components/Account.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.label {
font-size: var(--font-size-normal);
font-weight: 600;
}
3 changes: 2 additions & 1 deletion components/Button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
44 changes: 19 additions & 25 deletions components/DropDown.js
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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>
);
}
32 changes: 3 additions & 29 deletions components/Dropdown.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 4 additions & 5 deletions components/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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>
)}
Expand Down
14 changes: 12 additions & 2 deletions components/Icon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
}
16 changes: 15 additions & 1 deletion components/Icon.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
3 changes: 1 addition & 2 deletions components/Login.module.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.form {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform: translateX(-50%);
}
24 changes: 24 additions & 0 deletions components/Menu.js
Original file line number Diff line number Diff line change
@@ -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>
);
}
31 changes: 31 additions & 0 deletions components/Menu.module.css
Original file line number Diff line number Diff line change
@@ -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 changes: 56 additions & 0 deletions components/UserButton.js
Original file line number Diff line number Diff line change
@@ -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>
);
}
17 changes: 17 additions & 0 deletions components/UserButton.module.css
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions hooks/useDocumentClick.js
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion hooks/useRequireLogin.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function useRequireLogin() {
return;
}

await dispatch(updateUser({ user }));
await dispatch(updateUser(user));

setUser(user);
setLoading(false);
Expand Down
18 changes: 18 additions & 0 deletions pages/account.js
Original file line number Diff line number Diff line change
@@ -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>
);
}
Loading

0 comments on commit 6e23a8a

Please sign in to comment.