mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-23 18:04:47 +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;
|
||||
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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
.form {
|
||||
position: absolute;
|
||||
top: 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;
|
||||
}
|
||||
|
||||
await dispatch(updateUser({ user }));
|
||||
await dispatch(updateUser(user));
|
||||
|
||||
setUser(user);
|
||||
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 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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user