New mobile menu.

This commit is contained in:
Mike Cao 2022-02-28 18:39:37 -08:00
parent 18efd4d101
commit 34ad1d9c39
14 changed files with 189 additions and 153 deletions

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Pro 6.0.0-alpha1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M424 392H24C11 392 0 403 0 416V416C0 429 11 440 24 440H424C437 440 448 429 448 416V416C448 403 437 392 424 392ZM424 72H24C11 72 0 83 0 96V96C0 109 11 120 24 120H424C437 120 448 109 448 96V96C448 83 437 72 424 72ZM424 232H24C11 232 0 243 0 256V256C0 269 11 280 24 280H424C437 280 448 269 448 256V256C448 243 437 232 424 232Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Pro 6.0.0-alpha2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M424 392H24C10.8 392 0 402.8 0 416V416C0 429.2 10.8 440 24 440H424C437.2 440 448 429.2 448 416V416C448 402.8 437.2 392 424 392ZM424 72H24C10.8 72 0 82.8 0 96V96C0 109.2 10.8 120 24 120H424C437.2 120 448 109.2 448 96V96C448 82.8 437.2 72 424 72ZM424 232H24C10.8 232 0 242.8 0 256V256C0 269.2 10.8 280 24 280H424C437.2 280 448 269.2 448 256V256C448 242.8 437.2 232 424 232Z"/></svg>

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 594 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!-- Font Awesome Pro 6.0.0-alpha1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M345 375C354 384 354 400 345 409S320 418 311 409L192 290L73 409C64 418 48 418 39 409S30 384 39 375L158 256L39 137C30 128 30 112 39 103S64 94 73 103L192 222L311 103C320 94 336 94 345 103S354 128 345 137L226 256L345 375Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!-- Font Awesome Pro 6.0.0-alpha2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M312.973 375.032C322.342 384.401 322.342 399.604 312.973 408.973S288.401 418.342 279.032 408.973L160 289.941L40.968 408.973C31.599 418.342 16.396 418.342 7.027 408.973S-2.342 384.401 7.027 375.032L126.059 256L7.027 136.968C-2.342 127.599 -2.342 112.396 7.027 103.027S31.599 93.658 40.968 103.027L160 222.059L279.032 103.027C288.401 93.658 303.604 93.658 312.973 103.027S322.342 127.599 312.973 136.968L193.941 256L312.973 375.032Z"/></svg>

Before

Width:  |  Height:  |  Size: 441 B

After

Width:  |  Height:  |  Size: 653 B

View File

@ -0,0 +1,44 @@
import Button from 'components/common/Button';
import XMark from 'assets/xmark.svg';
import Bars from 'assets/bars.svg';
import { useState } from 'react';
import styles from './HamburgerButton.module.css';
import MobileMenu from './MobileMenu';
import { FormattedMessage } from 'react-intl';
const menuItems = [
{
label: <FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />,
value: '/dashboard',
},
{ label: <FormattedMessage id="label.realtime" defaultMessage="Realtime" />, value: '/realtime' },
{ label: <FormattedMessage id="label.settings" defaultMessage="Settings" />, value: '/settings' },
{
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
value: '/settings/profile',
},
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: '/logout' },
];
export default function HamburgerButton() {
const [active, setActive] = useState(false);
function handleClick() {
setActive(state => !state);
}
function handleClose() {
setActive(false);
}
return (
<>
<Button
className={styles.button}
icon={active ? <XMark /> : <Bars />}
onClick={handleClick}
/>
{active && <MobileMenu items={menuItems} onClose={handleClose} />}
</>
);
}

View File

@ -0,0 +1,9 @@
.button {
display: none;
}
@media only screen and (max-width: 768px) {
.button {
display: flex;
}
}

View File

@ -5,7 +5,7 @@ import NextLink from 'next/link';
import Icon from './Icon'; import Icon from './Icon';
import styles from './Link.module.css'; import styles from './Link.module.css';
function Link({ className, icon, children, size, iconRight, ...props }) { function Link({ className, icon, children, size, iconRight, onClick, ...props }) {
return ( return (
<NextLink {...props}> <NextLink {...props}>
<a <a
@ -15,6 +15,7 @@ function Link({ className, icon, children, size, iconRight, ...props }) {
[styles.xsmall]: size === 'xsmall', [styles.xsmall]: size === 'xsmall',
[styles.iconRight]: iconRight, [styles.iconRight]: iconRight,
})} })}
onClick={onClick}
> >
{icon && <Icon className={styles.icon} icon={icon} size={size} />} {icon && <Icon className={styles.icon} icon={icon} size={size} />}
{children} {children}

View File

@ -8,20 +8,14 @@ a.link:visited {
align-items: center; align-items: center;
} }
a.link:before { a.link:hover:before {
content: ''; content: '';
position: absolute; position: absolute;
bottom: -2px; bottom: -2px;
width: 0; width: 100%;
height: 2px; height: 2px;
background: var(--primary400); background: var(--primary400);
opacity: 0.5; opacity: 0.5;
transition: width 100ms;
}
a.link:hover:before {
width: 100%;
transition: width 100ms;
} }
a.link.large { a.link.large {

View File

@ -0,0 +1,21 @@
import Link from './Link';
import Button from './Button';
import XMark from 'assets/xmark.svg';
import styles from './MobileMenu.module.css';
export default function MobileMenu({ items = [], onClose }) {
return (
<div className={styles.menu}>
<div className={styles.header}>
<Button className={styles.button} icon={<XMark />} onClick={onClose} />
</div>
<div className={styles.items}>
{items.map(({ label, value }) => (
<Link key={value} href={value} className={styles.item} onClick={onClose}>
{label}
</Link>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,40 @@
.menu {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
z-index: 100;
display: flex;
flex-direction: column;
background-color: var(--gray50);
overflow: auto;
}
.items {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.item {
font-size: var(--font-size-large);
}
.item + .item {
margin-top: 20px;
}
.button {
margin-right: 15px;
}
.header {
display: flex;
justify-content: flex-end;
align-items: center;
height: 100px;
}

View File

@ -4,32 +4,29 @@ import { FormattedMessage } from 'react-intl';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
import styles from './Footer.module.css'; import styles from './Footer.module.css';
import useVersion from 'hooks/useVersion'; import useVersion from 'hooks/useVersion';
import useLocale from 'hooks/useLocale'; import { HOMEPAGE_URL, VERSION_URL } from 'lib/constants';
export default function Footer() { export default function Footer() {
const { current } = useVersion(); const { current } = useVersion();
const { dir } = useLocale();
return ( return (
<footer className="container" dir={dir}> <footer className={classNames(styles.footer, 'row')}>
<div className={classNames(styles.footer, 'row')}> <div className="col-12 col-md-4" />
<div className="col-12 col-md-4" /> <div className="col-12 col-md-4">
<div className="col-12 col-md-4"> <FormattedMessage
<FormattedMessage id="message.powered-by"
id="message.powered-by" defaultMessage="Powered by {name}"
defaultMessage="Powered by {name}" values={{
values={{ name: (
name: ( <Link href={HOMEPAGE_URL}>
<Link href="https://umami.is"> <b>umami</b>
<b>umami</b> </Link>
</Link> ),
), }}
}} />
/> </div>
</div> <div className={classNames(styles.version, 'col-12 col-md-4')}>
<div className={classNames(styles.version, 'col-12 col-md-4')}> <Link href={VERSION_URL}>{`v${current}`}</Link>
<Link href={`https://github.com/mikecao/umami/releases`}>{`v${current}`}</Link>
</div>
</div> </div>
</footer> </footer>
); );

View File

@ -3,8 +3,8 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-size: var(--font-size-small); font-size: var(--font-size-small);
min-height: 100px;
text-align: center; text-align: center;
margin: 20px 0;
} }
.version { .version {

View File

@ -1,71 +1,48 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
import Icon from 'components/common/Icon'; import Icon from 'components/common/Icon';
import LanguageButton from 'components/settings/LanguageButton'; import LanguageButton from 'components/settings/LanguageButton';
import ThemeButton from 'components/settings/ThemeButton'; import ThemeButton from 'components/settings/ThemeButton';
import HamburgerButton from 'components/common/HamburgerButton';
import UpdateNotice from 'components/common/UpdateNotice'; import UpdateNotice from 'components/common/UpdateNotice';
import UserButton from 'components/settings/UserButton'; import UserButton from 'components/settings/UserButton';
import Button from 'components/common/Button';
import Logo from 'assets/logo.svg'; import Logo from 'assets/logo.svg';
import styles from './Header.module.css'; import styles from './Header.module.css';
import useLocale from 'hooks/useLocale';
import XMark from 'assets/xmark.svg';
import Bars from 'assets/bars.svg';
import useUser from 'hooks/useUser'; import useUser from 'hooks/useUser';
import { HOMEPAGE_URL } from 'lib/constants';
export default function Header() { export default function Header() {
const { user } = useUser(); const { user } = useUser();
const [active, setActive] = useState(false);
const { dir } = useLocale();
function handleClick() {
setActive(state => !state);
}
return ( return (
<nav className="container" dir={dir}> <>
{user?.is_admin && <UpdateNotice />} {user?.is_admin && <UpdateNotice />}
<div className={classNames(styles.header, 'row align-items-center')}> <header className={classNames(styles.header, 'row')}>
<div className={styles.nav}> <div className={styles.title}>
<div className=""> <Icon icon={<Logo />} size="large" className={styles.logo} />
<div className={styles.title}> <Link href={user ? '/' : HOMEPAGE_URL}>umami</Link>
<Icon icon={<Logo />} size="large" className={styles.logo} />
<Link href={user ? '/' : 'https://umami.is'}>umami</Link>
</div>
</div>
<Button
className={styles.burger}
icon={active ? <XMark /> : <Bars />}
onClick={handleClick}
/>
{user && (
<div className={styles.items}>
<div className={active ? classNames(styles.active) : ''}>
<Link href="/dashboard">
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
</Link>
<Link href="/realtime">
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
</Link>
<Link href="/settings">
<FormattedMessage id="label.settings" defaultMessage="Settings" />
</Link>
</div>
</div>
)}
<div className={styles.items}>
<div className={active ? classNames(styles.active) : ''}>
<div className={styles.buttons}>
<ThemeButton />
<LanguageButton menuAlign="right" />
{user && <UserButton />}
</div>
</div>
</div>
</div> </div>
</div> <HamburgerButton />
</nav> {user && (
<div className={styles.links}>
<Link href="/dashboard">
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
</Link>
<Link href="/realtime">
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
</Link>
<Link href="/settings">
<FormattedMessage id="label.settings" defaultMessage="Settings" />
</Link>
</div>
)}
<div className={styles.buttons}>
<ThemeButton />
<LanguageButton menuAlign="right" />
{user && <UserButton />}
</div>
</header>
</>
); );
} }

View File

@ -1,17 +1,6 @@
.navbar {
align-items: stretch;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
}
.burger {
display: none;
}
.header { .header {
display: flex; display: flex;
align-items: center;
min-height: 100px; min-height: 100px;
width: 100%; width: 100%;
} }
@ -27,16 +16,8 @@
margin-right: 12px; margin-right: 12px;
} }
.nav { .links {
display: flex; flex: 1;
align-items: center;
font-size: var(--font-size-normal);
font-weight: 600;
width: 100%;
justify-content: space-between;
}
.items {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -44,7 +25,7 @@
font-weight: 600; font-weight: 600;
} }
.nav a + a { .links a + a {
margin-left: 40px; margin-left: 40px;
} }
@ -55,13 +36,13 @@
} }
@media only screen and (max-width: 992px) { @media only screen and (max-width: 992px) {
.nav { .header .buttons {
font-size: var(--font-size-large); flex: 1;
justify-content: space-between;
margin: 20px 0;
} }
.items { .links {
flex-wrap: wrap; order: 2;
margin: 20px 0;
min-width: 100%;
} }
} }
@ -70,47 +51,14 @@
padding: 0 15px; padding: 0 15px;
} }
.title { .buttons,
padding: 0.5rem; .links {
margin-bottom: 0.5rem;
}
.nav {
font-size: var(--font-size-normal);
flex-wrap: wrap;
justify-content: center;
flex-direction: column;
position: relative;
}
.items {
display: flex;
justify-content: unset;
font-size: var(--font-size-normal);
font-weight: 600;
}
.items > div {
display: none; display: none;
} }
.header .active { .title {
display: inherit; flex: 1;
width: 100%; padding: 0.5rem;
} margin-bottom: 0.5rem;
.items a {
width: 100%;
}
.burger {
display: block;
background: none;
border: 1px solid var(--gray900);
border-radius: 4px;
cursor: pointer;
position: absolute;
top: 0;
right: 0;
} }
} }

View File

@ -5,6 +5,8 @@ export const DATE_RANGE_CONFIG = 'umami.date-range';
export const THEME_CONFIG = 'umami.theme'; export const THEME_CONFIG = 'umami.theme';
export const VERSION_CHECK = 'umami.version-check'; export const VERSION_CHECK = 'umami.version-check';
export const TOKEN_HEADER = 'x-umami-token'; export const TOKEN_HEADER = 'x-umami-token';
export const HOMEPAGE_URL = 'https://umami.is';
export const VERSION_URL = 'https://github.com/mikecao/umami/releases';
export const DEFAULT_LOCALE = 'en-US'; export const DEFAULT_LOCALE = 'en-US';
export const DEFAULT_THEME = 'light'; export const DEFAULT_THEME = 'light';

View File

@ -23,6 +23,7 @@ const Intl = ({ children }) => {
export default function App({ Component, pageProps }) { export default function App({ Component, pageProps }) {
const { basePath } = useRouter(); const { basePath } = useRouter();
const { dir } = useLocale();
return ( return (
<Intl> <Intl>
@ -38,7 +39,9 @@ export default function App({ Component, pageProps }) {
<meta name="theme-color" content="#2f2f2f" media="(prefers-color-scheme: dark)" /> <meta name="theme-color" content="#2f2f2f" media="(prefers-color-scheme: dark)" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
</Head> </Head>
<Component {...pageProps} /> <div className="container" dir={dir}>
<Component {...pageProps} />
</div>
</Intl> </Intl>
); );
} }