mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Added user button and menu.
This commit is contained in:
parent
a5930f1772
commit
6e23a8a53b
1
assets/chevron-down.svg
Normal file
1
assets/chevron-down.svg
Normal 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
1
assets/user.svg
Normal 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
15
components/Account.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
4
components/Account.module.css
Normal file
4
components/Account.module.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.label {
|
||||||
|
font-size: var(--font-size-normal);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
@ -2,7 +2,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #f5f5f5;
|
font-size: var(--font-size-normal);
|
||||||
|
background: var(--gray100);
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import classNames from 'classnames';
|
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 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 [showMenu, setShowMenu] = useState(false);
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
|
|
||||||
@ -16,35 +26,19 @@ export default function DropDown({ value, options = [], onChange, className }) {
|
|||||||
onChange(value);
|
onChange(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useDocumentClick(e => {
|
||||||
function hideMenu(e) {
|
if (!ref.current.contains(e.target)) {
|
||||||
if (!ref.current.contains(e.target)) {
|
setShowMenu(false);
|
||||||
setShowMenu(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
document.addEventListener('click', hideMenu);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('click', hideMenu);
|
|
||||||
};
|
|
||||||
}, [ref]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={classNames(styles.dropdown, className)} onClick={handleShowMenu}>
|
<div ref={ref} className={classNames(styles.dropdown, className)} onClick={handleShowMenu}>
|
||||||
<div className={styles.value}>
|
<div className={styles.value}>
|
||||||
{options.find(e => e.value === value).label}
|
{options.find(e => e.value === value)?.label}
|
||||||
<div className={styles.caret} />
|
<Icon icon={<Chevron />} size="S" className={styles.icon} />
|
||||||
</div>
|
</div>
|
||||||
{showMenu && (
|
{showMenu && <Menu className={menuClassName} options={options} onSelect={handleSelect} />}
|
||||||
<div className={styles.menu}>
|
|
||||||
{options.map(({ label, value }) => (
|
|
||||||
<div key={value} className={styles.option} onClick={e => handleSelect(value, e)}>
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,40 +8,14 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 4px 32px 4px 16px;
|
padding: 4px 32px 4px 16px;
|
||||||
border: 1px solid #b3b3b3;
|
border: 1px solid var(--gray500);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu {
|
.icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
min-width: 100px;
|
top: 0;
|
||||||
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;
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Link from 'components/Link';
|
import Link from 'components/Link';
|
||||||
|
import UserButton from './UserButton';
|
||||||
import styles from './Header.module.css';
|
import styles from './Header.module.css';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
@ -11,16 +12,14 @@ export default function Header() {
|
|||||||
<header className={classNames(styles.header, 'container')}>
|
<header className={classNames(styles.header, 'container')}>
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<Link href="/" className={styles.title}>
|
<div className={styles.title}>{user ? <Link href="/">umami</Link> : 'umami'}</div>
|
||||||
umami
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
{user && (
|
{user && (
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<div className={styles.nav}>
|
<div className={styles.nav}>
|
||||||
<Link href="/">Dashboard</Link>
|
<Link href="/dashboard">Dashboard</Link>
|
||||||
<Link href="/settings">Settings</Link>
|
<Link href="/settings">Settings</Link>
|
||||||
<Link href="/logout">Logout</Link>
|
<UserButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -2,6 +2,16 @@ import React from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import styles from './Icon.module.css';
|
import styles from './Icon.module.css';
|
||||||
|
|
||||||
export default function Icon({ icon, className }) {
|
export default function Icon({ icon, className, size = 'M' }) {
|
||||||
return <div className={classNames(styles.icon, className)}>{icon}</div>;
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.icon, className, {
|
||||||
|
[styles.large]: size === 'L',
|
||||||
|
[styles.medium]: size === 'M',
|
||||||
|
[styles.small]: size === 'S',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,21 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon > svg {
|
.icon svg {
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large > svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium > svg {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.small > svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
.form {
|
.form {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
24
components/Menu.js
Normal file
24
components/Menu.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
31
components/Menu.module.css
Normal file
31
components/Menu.module.css
Normal 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
56
components/UserButton.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
17
components/UserButton.module.css
Normal file
17
components/UserButton.module.css
Normal 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
13
hooks/useDocumentClick.js
Normal 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;
|
||||||
|
}
|
@ -31,7 +31,7 @@ export default function useRequireLogin() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await dispatch(updateUser({ user }));
|
await dispatch(updateUser(user));
|
||||||
|
|
||||||
setUser(user);
|
setUser(user);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
18
pages/account.js
Normal file
18
pages/account.js
Normal 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
18
pages/dashboard.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,18 +1,12 @@
|
|||||||
import React from 'react';
|
import { useEffect } from 'react';
|
||||||
import Layout from 'components/Layout';
|
import { useRouter } from 'next/router';
|
||||||
import WebsiteList from 'components/WebsiteList';
|
|
||||||
import useRequireLogin from 'hooks/useRequireLogin';
|
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function DefaultPage() {
|
||||||
const { loading } = useRequireLogin();
|
const router = useRouter();
|
||||||
|
|
||||||
if (loading) {
|
useEffect(() => {
|
||||||
return null;
|
router.push('/dashboard');
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
return (
|
return null;
|
||||||
<Layout>
|
|
||||||
<WebsiteList />
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -47,11 +47,13 @@ form label {
|
|||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
form input,
|
input,
|
||||||
form textarea {
|
textarea {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
font-size: var(--font-size-normal);
|
||||||
|
line-height: 1.8;
|
||||||
border: 1px solid var(--gray500);
|
border: 1px solid var(--gray500);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
outline: none;
|
outline: none;
|
||||||
@ -59,7 +61,7 @@ form textarea {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: 1px solid var(--gray500);
|
border: 1px solid var(--gray500) !important;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user