Moved code into src folder. Added build for component library.

This commit is contained in:
Mike Cao 2023-08-21 02:06:09 -07:00
parent 7a7233ead4
commit ede658771e
490 changed files with 749 additions and 442 deletions

View file

@ -0,0 +1,32 @@
import { Container } from 'react-basics';
import Head from 'next/head';
import NavBar from 'components/layout/NavBar';
import UpdateNotice from 'components/common/UpdateNotice';
import { useRequireLogin, useConfig } from 'components/hooks';
import styles from './AppLayout.module.css';
export function AppLayout({ title, children }) {
const { user } = useRequireLogin();
const config = useConfig();
if (!user || !config) {
return null;
}
return (
<div className={styles.layout}>
<UpdateNotice user={user} config={config} />
<Head>
<title>{title ? `${title} | umami` : 'umami'}</title>
</Head>
<nav className={styles.nav}>
<NavBar />
</nav>
<main className={styles.body}>
<Container>{children}</Container>
</main>
</div>
);
}
export default AppLayout;

View file

@ -0,0 +1,23 @@
.layout {
display: grid;
grid-template-rows: max-content 1fr;
grid-template-columns: 1fr;
overflow: hidden;
}
.nav {
height: 60px;
width: 100vw;
grid-column: 1;
grid-row: 1 / 2;
z-index: 1;
}
.body {
grid-column: 1;
grid-row: 2 / 3;
min-height: 0;
height: calc(100vh - 60px);
overflow-y: auto;
padding-bottom: 60px;
}

View file

@ -0,0 +1,14 @@
import { CURRENT_VERSION, HOMEPAGE_URL } from 'lib/constants';
import styles from './Footer.module.css';
export function Footer() {
return (
<footer className={styles.footer}>
<a href={HOMEPAGE_URL}>
<b>umami</b> {`v${CURRENT_VERSION}`}
</a>
</footer>
);
}
export default Footer;

View file

@ -0,0 +1,12 @@
.footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
font-size: var(--font-size-sm);
line-height: 30px;
margin: 40px 0;
}
.footer a {
color: var(--font-color100);
}

View file

@ -0,0 +1,13 @@
import { Row, Column } from 'react-basics';
import classNames from 'classnames';
import styles from './Grid.module.css';
export function GridRow(props) {
const { className, ...otherProps } = props;
return <Row {...otherProps} className={classNames(styles.row, className)} />;
}
export function GridColumn(props) {
const { className, ...otherProps } = props;
return <Column {...otherProps} className={classNames(styles.col, className)} />;
}

View file

@ -0,0 +1,36 @@
.col {
display: flex;
flex-direction: column;
padding: 20px;
}
.row {
border-top: 1px solid var(--base300);
min-height: 430px;
}
.row > .col {
border-inline-start: 1px solid var(--base300);
}
.row > .col:first-child {
border-inline-start: 0;
padding-inline-start: 0;
}
.row > .col:last-child {
padding-inline-end: 0;
}
@media only screen and (max-width: 992px) {
.row {
border: 0;
}
.row > .col {
border-top: 1px solid var(--base300);
border-inline-start: 0;
border-inline-end: 0;
padding: 20px 0;
}
}

View file

@ -0,0 +1,31 @@
import { Column, Icon, Row, Text } from 'react-basics';
import Link from 'next/link';
import LanguageButton from 'components/input/LanguageButton';
import ThemeButton from 'components/input/ThemeButton';
import SettingsButton from 'components/input/SettingsButton';
import Icons from 'components/icons';
import styles from './Header.module.css';
export function Header() {
return (
<header className={styles.header}>
<Row className={styles.row}>
<Column>
<Link href="https://umami.is" target="_blank" className={styles.title}>
<Icon size="lg">
<Icons.Logo />
</Icon>
<Text>umami</Text>
</Link>
</Column>
<Column className={styles.buttons}>
<ThemeButton tooltipPosition="bottom" />
<LanguageButton tooltipPosition="bottom" menuPosition="bottom" />
<SettingsButton />
</Column>
</Row>
</header>
);
}
export default Header;

View file

@ -0,0 +1,47 @@
.header {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
height: 100px;
}
.row {
align-items: center;
}
.title {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
font-size: var(--font-size-lg);
font-weight: 700;
color: var(--font-color100) !important;
}
.buttons {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
@media only screen and (max-width: 992px) {
.header .buttons {
flex: 1;
}
.links {
order: 2;
margin: 20px 0;
min-width: 100%;
}
}
@media only screen and (max-width: 768px) {
.buttons,
.links {
display: none;
}
}

View file

@ -0,0 +1,65 @@
import { Icon, Text, Row, Column } from 'react-basics';
import Link from 'next/link';
import classNames from 'classnames';
import Icons from 'components/icons';
import ThemeButton from 'components/input/ThemeButton';
import LanguageButton from 'components/input/LanguageButton';
import ProfileButton from 'components/input/ProfileButton';
import styles from './NavBar.module.css';
import useConfig from 'components/hooks/useConfig';
import useMessages from 'components/hooks/useMessages';
import { useRouter } from 'next/router';
import HamburgerButton from '../common/HamburgerButton';
export function NavBar() {
const { pathname } = useRouter();
const { cloudMode } = useConfig();
const { formatMessage, labels } = useMessages();
const links = [
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
{ label: formatMessage(labels.websites), url: '/websites' },
{ label: formatMessage(labels.reports), url: '/reports' },
!cloudMode && { label: formatMessage(labels.settings), url: '/settings' },
].filter(n => n);
return (
<div className={classNames(styles.navbar)}>
<Row>
<Column className={styles.left}>
<div className={styles.logo}>
<Icon size="lg">
<Icons.Logo />
</Icon>
<Text className={styles.text}>umami</Text>
</div>
<div className={styles.links}>
{links.map(({ url, label }) => {
return (
<Link
key={url}
href={url}
className={classNames({ [styles.selected]: pathname.startsWith(url) })}
>
<Text>{label}</Text>
</Link>
);
})}
</div>
</Column>
<Column className={styles.right}>
<div className={styles.actions}>
<ThemeButton />
<LanguageButton />
<ProfileButton />
</div>
<div className={styles.mobile}>
<HamburgerButton />
</div>
</Column>
</Row>
</div>
);
}
export default NavBar;

View file

@ -0,0 +1,87 @@
.navbar {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
height: 60px;
background: var(--base75);
border-bottom: 1px solid var(--base300);
padding: 0 20px;
}
.left,
.right {
display: flex;
flex-direction: row;
align-items: center;
}
.right {
justify-content: flex-end;
}
.logo {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
font-size: 16px;
font-weight: 700;
min-width: 0;
}
.links {
display: flex;
flex-direction: row;
gap: 30px;
padding: 0 40px;
flex: 1;
font-weight: 700;
}
.links a {
display: flex;
align-items: center;
gap: 10px;
line-height: 60px;
color: var(--font-color200);
border-bottom: 2px solid transparent;
}
.links span {
white-space: nowrap;
}
.links a:hover {
color: var(--font-color100);
border-bottom: 2px solid var(--primary400);
}
.links .selected {
color: var(--font-color100);
border-bottom: 2px solid var(--primary400);
}
.actions,
.mobile {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
min-width: 0;
}
.mobile {
display: none;
}
@media only screen and (max-width: 768px) {
.links,
.actions {
display: none;
}
.mobile {
display: flex;
}
}

View file

@ -0,0 +1,58 @@
import { useState } from 'react';
import { Icon, Text, TooltipPopup } from 'react-basics';
import classNames from 'classnames';
import { useRouter } from 'next/router';
import Link from 'next/link';
import Icons from 'components/icons';
import styles from './NavGroup.module.css';
export 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,
})}
>
{title && (
<div className={styles.header} onClick={allowExpand ? handleExpand : undefined}>
<Text>{title}</Text>
<Icon size="sm" rotate={expanded ? 0 : -90}>
<Icons.ChevronDown />
</Icon>
</div>
)}
<div className={styles.body}>
{items.map(({ label, url, icon, divider }) => {
return (
<TooltipPopup key={label} label={label} position="right" disabled={!minimized}>
<Link
href={url}
className={classNames(styles.item, {
[styles.divider]: divider,
[styles.selected]: pathname.startsWith(url),
})}
>
<Icon>{icon}</Icon>
<Text className={styles.text}>{label}</Text>
</Link>
</TooltipPopup>
);
})}
</div>
</div>
);
}
export default NavGroup;

View file

@ -0,0 +1,85 @@
.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;
}
.item {
position: relative;
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;
margin-right: -2px;
}
a.item {
color: var(--base700);
}
.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;
}
.minimized .text,
.minimized .header {
display: none;
}
.minimized .item {
width: 60px;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.divider:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
margin: auto;
border-top: 1px solid var(--base300);
width: 160px;
}
.minimized .divider:before {
width: 60px;
}

View file

@ -0,0 +1,20 @@
import classNames from 'classnames';
import { Banner, Loading } from 'react-basics';
import useMessages from 'components/hooks/useMessages';
import styles from './Page.module.css';
export function Page({ className, error, loading, children }) {
const { formatMessage, messages } = useMessages();
if (error) {
return <Banner variant="error">{formatMessage(messages.error)}</Banner>;
}
if (loading) {
return <Loading icon="spinner" size="xl" position="page" />;
}
return <div className={classNames(styles.page, className)}>{children}</div>;
}
export default Page;

View file

@ -0,0 +1,7 @@
.page {
flex: 1;
display: flex;
flex-direction: column;
background: var(--base50);
position: relative;
}

View file

@ -0,0 +1,14 @@
import classNames from 'classnames';
import React from 'react';
import styles from './PageHeader.module.css';
export function PageHeader({ title, children, className }) {
return (
<div className={classNames(styles.header, className)}>
{title && <div className={styles.title}>{title}</div>}
<div className={styles.actions}>{children}</div>
</div>
);
}
export default PageHeader;

View file

@ -0,0 +1,48 @@
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
align-content: center;
align-self: stretch;
flex-wrap: wrap;
height: 100px;
}
.header a {
color: var(--base600);
}
.header a:hover {
color: var(--base900);
}
.title {
display: flex;
align-items: center;
font-size: 24px;
font-weight: 700;
gap: 20px;
height: 60px;
flex: 1;
}
.actions {
display: flex;
justify-content: flex-end;
}
@media only screen and (max-width: 992px) {
.header {
margin-bottom: 10px;
}
.title {
font-size: 18px;
}
.actions {
flex-basis: 100%;
order: -1;
}
}

View file

@ -0,0 +1,23 @@
import { Column, Row } from 'react-basics';
import styles from './ReportsLayout.module.css';
export function ReportsLayout({ children, filter, header }) {
return (
<>
<Row>{header}</Row>
<Row>
{filter && (
<Column className={styles.filter} defaultSize={12} md={4} lg={3} xl={3}>
<h2>Filters</h2>
{filter}
</Column>
)}
<Column className={styles.content} defaultSize={12} md={8} lg={9} xl={9}>
{children}
</Column>
</Row>
</>
);
}
export default ReportsLayout;

View file

@ -0,0 +1,23 @@
.filter {
margin-top: 30px;
min-width: 200px;
max-width: 100vw;
padding: 10px;
background: var(--base50);
border-radius: 5px;
border: 1px solid var(--border-color);
}
.filter h2 {
padding-bottom: 20px;
}
.content {
min-height: 50vh;
}
@media only screen and (max-width: 768px) {
.menu {
display: none;
}
}

View file

@ -0,0 +1,38 @@
import { Row, Column } from 'react-basics';
import { useRouter } from 'next/router';
import SideNav from './SideNav';
import useUser from 'components/hooks/useUser';
import useMessages from 'components/hooks/useMessages';
import useConfig from 'components/hooks/useConfig';
import styles from './SettingsLayout.module.css';
export function SettingsLayout({ children }) {
const { user } = useUser();
const { pathname } = useRouter();
const { formatMessage, labels } = useMessages();
const { cloudMode } = useConfig();
const items = [
{ key: 'websites', label: formatMessage(labels.websites), url: '/settings/websites' },
{ key: 'teams', label: formatMessage(labels.teams), url: '/settings/teams' },
user.isAdmin && { key: 'users', label: formatMessage(labels.users), url: '/settings/users' },
{ key: 'profile', label: formatMessage(labels.profile), url: '/settings/profile' },
].filter(n => n);
const getKey = () => items.find(({ url }) => pathname === url)?.key;
return (
<Row>
{!cloudMode && (
<Column className={styles.menu} defaultSize={12} md={4} lg={3} xl={2}>
<SideNav items={items} shallow={true} selectedKey={getKey()} />
</Column>
)}
<Column className={styles.content} defaultSize={12} md={8} lg={9} xl={10}>
{children}
</Column>
</Row>
);
}
export default SettingsLayout;

View file

@ -0,0 +1,16 @@
.menu {
display: flex;
flex-direction: column;
padding-top: 40px;
padding-right: 20px;
}
.content {
min-height: 50vh;
}
@media only screen and (max-width: 768px) {
.menu {
display: none;
}
}

View file

@ -0,0 +1,15 @@
import { Container } from 'react-basics';
import Header from './Header';
import Footer from './Footer';
export function ShareLayout({ children }) {
return (
<Container>
<Header />
<main>{children}</main>
<Footer />
</Container>
);
}
export default ShareLayout;

View file

@ -0,0 +1,25 @@
import classNames from 'classnames';
import { Menu, Item } from 'react-basics';
import { useRouter } from 'next/router';
import Link from 'next/link';
import styles from './SideNav.module.css';
export function SideNav({ selectedKey, items, shallow, onSelect = () => {} }) {
const { asPath } = useRouter();
return (
<Menu items={items} selectedKey={selectedKey} className={styles.menu} onSelect={onSelect}>
{({ key, label, url }) => (
<Item
key={key}
className={classNames(styles.item, { [styles.selected]: asPath.startsWith(url) })}
>
<Link href={url} shallow={shallow}>
{label}
</Link>
</Item>
)}
</Menu>
);
}
export default SideNav;

View file

@ -0,0 +1,19 @@
.menu {
display: flex;
flex-direction: column;
}
.item a {
color: var(--font-color100);
flex: 1;
padding: var(--size300) var(--size600);
}
.item {
padding: 0;
border-radius: var(--border-radius);
}
.selected {
font-weight: 700;
}