Refactored layout. Added NavBar component.

This commit is contained in:
Mike Cao 2023-01-18 15:05:39 -08:00
parent fad38dc180
commit 1d9c3133f0
56 changed files with 601 additions and 429 deletions

View file

@ -1,22 +1,25 @@
import { Container } from 'react-basics';
import Head from 'next/head';
import Header from 'components/layout/Header';
import Footer from 'components/layout/Footer';
import useLocale from 'hooks/useLocale';
import NavBar from 'components/layout/NavBar';
import useRequireLogin from 'hooks/useRequireLogin';
import styles from './AppLayout.module.css';
export default function AppLayout({ title, children }) {
useRequireLogin();
const { dir } = useLocale();
return (
<Container dir={dir} style={{ maxWidth: 1140 }}>
<div className={styles.layout}>
<Head>
<title>{title ? `${title} | umami` : 'umami'}</title>
</Head>
<Header />
<main>{children}</main>
<Footer />
</Container>
<div className={styles.nav}>
<NavBar />
</div>
<div className={styles.body}>
<Container>
<main>{children}</main>
</Container>
</div>
</div>
);
}

View file

@ -1,6 +1,16 @@
.layout {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
display: grid;
grid-template-rows: 1fr;
grid-template-columns: minmax(60px, 200px) 1fr;
height: 100vh;
overflow: hidden;
}
.nav {
grid-row: 1 / 3;
}
.body {
grid-area: 1 / 2;
overflow: auto;
}

View file

@ -1,33 +1,38 @@
import { Row, Column } from 'react-basics';
import { useRouter } from 'next/router';
import Script from 'next/script';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { useIntl, defineMessages } from 'react-intl';
import Link from 'components/common/Link';
import styles from './Footer.module.css';
import { CURRENT_VERSION, HOMEPAGE_URL, REPO_URL } from 'lib/constants';
import styles from './Footer.module.css';
export default function Footer() {
const messages = defineMessages({
poweredBy: { id: 'message.powered-by', defaultMessage: 'Powered by {name}' },
});
export default function Footer({ className }) {
const { pathname } = useRouter();
const { formatMessage } = useIntl();
return (
<footer className={classNames(styles.footer, 'row')}>
<div className="col-12 col-md-4" />
<div className="col-12 col-md-4">
<FormattedMessage
id="message.powered-by"
defaultMessage="Powered by {name}"
values={{
name: (
<Link href={HOMEPAGE_URL}>
<b>umami</b>
</Link>
),
}}
/>
</div>
<div className={classNames(styles.version, 'col-12 col-md-4')}>
<Link href={REPO_URL}>{`v${CURRENT_VERSION}`}</Link>
</div>
<footer className={classNames(styles.footer, className)}>
<Row>
<Column>
<div>
{formatMessage(messages.poweredBy, {
name: (
<Link href={HOMEPAGE_URL}>
<b>umami</b>
</Link>
),
})}
</div>
</Column>
<Column className={styles.version}>
<Link href={REPO_URL}>{`v${CURRENT_VERSION}`}</Link>
</Column>
</Row>
{!pathname.includes('/share/') && <Script src={`/telemetry.js`} />}
</footer>
);

View file

@ -1,19 +1,17 @@
import Logo from 'assets/logo.svg';
import HamburgerButton from 'components/common/HamburgerButton';
import Link from 'components/common/Link';
import UpdateNotice from 'components/common/UpdateNotice';
import LanguageButton from 'components/settings/LanguageButton';
import ThemeButton from 'components/settings/ThemeButton';
import UserButton from 'components/settings/UserButton';
import LanguageButton from 'components/buttons/LanguageButton';
import ThemeButton from 'components/buttons/ThemeButton';
import UserButton from 'components/buttons/UserButton';
import useConfig from 'hooks/useConfig';
import useUser from 'hooks/useUser';
import { HOMEPAGE_URL } from 'lib/constants';
import { useRouter } from 'next/router';
import { Column, Icon, Row } from 'react-basics';
import SettingsButton from '../settings/SettingsButton';
import { Column, Row } from 'react-basics';
import SettingsButton from '../buttons/SettingsButton';
import styles from './Header.module.css';
import classNames from 'classnames';
export default function Header() {
export default function Header({ className }) {
const user = useUser();
const { pathname } = useRouter();
const { updatesDisabled, adminDisabled } = useConfig();
@ -23,14 +21,9 @@ export default function Header() {
return (
<>
{allowUpdate && <UpdateNotice />}
<header className={styles.header}>
<header className={classNames(styles.header, className)}>
<Row>
<Column className={styles.title}>
<Icon size="lg" className={styles.logo}>
<Logo />
</Icon>
<Link href={isSharePage ? HOMEPAGE_URL : '/'}>umami</Link>
</Column>
<Column className={styles.title}></Column>
<HamburgerButton />
<Column className={styles.buttons}>
<ThemeButton />

View file

@ -1,8 +1,9 @@
.header {
display: flex;
align-items: center;
min-height: 100px;
width: 100%;
height: 50px;
border-bottom: 1px solid var(--base300);
}
.title {

View file

@ -0,0 +1,48 @@
import { useState } from 'react';
import { Icon, Text, Icons } from 'react-basics';
import classNames from 'classnames';
import { Dashboard, Logo, Profile, User, Users, Clock, Globe } from 'components/icons';
import NavGroup from './NavGroup';
import styles from './NavBar.module.css';
const { ChevronDown, Search } = Icons;
const analytics = [
{ key: 'dashboard', label: 'Dashboard', url: '/dashboard', icon: <Dashboard /> },
{ key: 'realtime', label: 'Realtime', url: '/realtime', icon: <Clock /> },
{ key: 'queries', label: 'Queries', url: '/queries', icon: <Search /> },
];
const settings = [
{ key: 'websites', label: 'Websites', url: '/settings/websites', icon: <Globe /> },
{ key: 'users', label: 'Users', url: '/settings/users', icon: <User /> },
{ key: 'teams', label: 'Teams', url: '/settings/teams', icon: <Users /> },
];
export default function NavBar() {
const [minimized, setMinimized] = useState(false);
const handleMinimize = () => setMinimized(state => !state);
return (
<div className={classNames(styles.navbar, { [styles.minimized]: minimized })}>
<div className={styles.header} onClick={handleMinimize}>
<Icon size="lg">
<Logo />
</Icon>
<Text className={styles.text}>umami</Text>
<Icon size="sm" rotate={minimized ? -90 : 90} className={styles.icon}>
<ChevronDown />
</Icon>
</div>
<NavGroup title="Analytics" items={analytics} minimized={minimized} />
<NavGroup title="Settings" items={settings} minimized={minimized} />
<div className={styles.footer}>
<Icon>
<Profile />
</Icon>
<Text>Profile</Text>
</div>
</div>
);
}

View file

@ -0,0 +1,47 @@
.navbar {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
background: var(--base75);
height: 100%;
width: 200px;
border-right: 2px solid var(--base200);
}
.header {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 16px;
font-weight: 700;
padding: 20px 0;
cursor: pointer;
}
.header:hover .icon {
visibility: visible;
}
.icon {
position: absolute;
right: 0;
visibility: hidden;
}
.minimized.navbar {
width: 60px;
}
.minimized .text {
display: none;
}
.footer {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 20px;
}

View file

@ -0,0 +1,51 @@
import { useState } from 'react';
import { Icon, Text, Icons } from 'react-basics';
import classNames from 'classnames';
import { useRouter } from 'next/router';
import Link from 'next/link';
import styles from './NavGroup.module.css';
const { ChevronDown } = Icons;
export default function NavGroup({
title,
items,
defaultExpanded = true,
allowExpand = true,
minimized = false,
}) {
const { pathname } = useRouter();
const [expanded, setExpanded] = useState(defaultExpanded);
const handleExpand = () => setExpanded(state => !state);
return (
<div
className={classNames(styles.group, {
[styles.expanded]: expanded,
[styles.minimized]: minimized,
})}
>
<div className={styles.header} onClick={allowExpand ? handleExpand : undefined}>
<Text>{title}</Text>
<Icon size="sm" rotate={expanded ? 0 : -90}>
<ChevronDown />
</Icon>
</div>
<div className={styles.body}>
{items.map(({ key, label, url, icon }) => {
return (
<Link key={key} href={url}>
<a
className={classNames(styles.item, { [styles.selected]: pathname.startsWith(url) })}
>
<Icon>{icon}</Icon>
<Text className={styles.text}>{label}</Text>
</a>
</Link>
);
})}
</div>
</div>
);
}

View file

@ -0,0 +1,76 @@
.group {
display: flex;
flex-direction: column;
width: 100%;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
color: var(--base600);
font-size: 11px;
font-weight: 600;
padding: 10px 20px;
text-transform: uppercase;
cursor: pointer;
}
.body {
display: none;
}
.expanded .body {
display: block;
}
.items {
display: flex;
flex-direction: column;
gap: 20px;
margin-right: -2px;
width: 200px;
}
.item {
display: flex;
flex-direction: row;
align-items: center;
border-right: 2px solid var(--base200);
padding: 1rem 2rem;
gap: var(--size500);
font-weight: 600;
width: 200px;
}
.item.selected {
color: var(--base900);
border-right-color: var(--primary400);
background: var(--blue100);
}
.item:hover {
color: var(--base900);
}
.item.disabled {
color: var(--base500) !important;
pointer-events: none;
}
a.item {
color: var(--base600);
}
.minimized .text,
.minimized .header {
display: none;
}
.minimized .item {
width: 60px;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}

View file

@ -1,3 +1,4 @@
import React from 'react';
import Link from 'next/link';
import classNames from 'classnames';
import { Button, Icon } from 'react-basics';