commit
4047a7ec23
378 changed files with 29334 additions and 0 deletions
17
components/layout/ButtonLayout.js
Normal file
17
components/layout/ButtonLayout.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './ButtonLayout.module.css';
|
||||
|
||||
export default function ButtonLayout({ className, children, align = 'center' }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.buttons, className, {
|
||||
[styles.left]: align === 'left',
|
||||
[styles.center]: align === 'center',
|
||||
[styles.right]: align === 'right',
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
components/layout/ButtonLayout.module.css
Normal file
20
components/layout/ButtonLayout.module.css
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.buttons button + * {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
36
components/layout/Footer.js
Normal file
36
components/layout/Footer.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Link from 'components/common/Link';
|
||||
import styles from './Footer.module.css';
|
||||
import useVersion from 'hooks/useVersion';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export default function Footer() {
|
||||
const { current } = useVersion();
|
||||
const { dir } = useLocale();
|
||||
|
||||
return (
|
||||
<footer className="container" dir={dir}>
|
||||
<div 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="https://umami.is">
|
||||
<b>umami</b>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={classNames(styles.version, 'col-12 col-md-4')}>
|
||||
<Link href={`https://github.com/mikecao/umami/releases`}>{`v${current}`}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
18
components/layout/Footer.module.css
Normal file
18
components/layout/Footer.module.css
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-small);
|
||||
min-height: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.version {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.version {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
32
components/layout/FormLayout.js
Normal file
32
components/layout/FormLayout.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import classNames from 'classnames';
|
||||
import { ErrorMessage } from 'formik';
|
||||
import styles from './FormLayout.module.css';
|
||||
|
||||
export default function FormLayout({ className, children }) {
|
||||
return <div className={classNames(styles.form, className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export const FormButtons = ({ className, children }) => (
|
||||
<div className={classNames(styles.buttons, className)}>{children}</div>
|
||||
);
|
||||
|
||||
export const FormError = ({ name }) => {
|
||||
return <ErrorMessage name={name}>{msg => <ErrorTag msg={msg} />}</ErrorMessage>;
|
||||
};
|
||||
|
||||
const ErrorTag = ({ msg }) => {
|
||||
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
|
||||
|
||||
return (
|
||||
<animated.div className={styles.error} style={props}>
|
||||
<div className={styles.msg}>{msg}</div>
|
||||
</animated.div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FormRow = ({ children }) => <div className={styles.row}>{children}</div>;
|
||||
|
||||
export const FormMessage = ({ children }) =>
|
||||
children ? <div className={styles.message}>{children}</div> : null;
|
||||
94
components/layout/FormLayout.module.css
Normal file
94
components/layout/FormLayout.module.css
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form label {
|
||||
display: block;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.8;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.row > div {
|
||||
position: relative;
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
.row > div > input {
|
||||
width: 100%;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.buttons button + button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: calc(100% + 16px);
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.msg {
|
||||
color: var(--msgColor);
|
||||
background: var(--red400);
|
||||
font-size: var(--font-size-small);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.error:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -5px;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: var(--red400);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.message {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
color: var(--gray50);
|
||||
background: var(--gray800);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
.error {
|
||||
align-items: flex-start;
|
||||
top: calc(100% + 7px);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.error:after {
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
31
components/layout/GridLayout.js
Normal file
31
components/layout/GridLayout.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './GridLayout.module.css';
|
||||
|
||||
export default function GridLayout({ className, children }) {
|
||||
return <div className={classNames(styles.grid, className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export const GridRow = ({ className, children }) => {
|
||||
return <div className={classNames(styles.row, className, 'row')}>{children}</div>;
|
||||
};
|
||||
|
||||
export const GridColumn = ({ xs, sm, md, lg, xl, className, children }) => {
|
||||
const classes = [];
|
||||
|
||||
classes.push(xs ? `col-${xs}` : 'col-12');
|
||||
|
||||
if (sm) {
|
||||
classes.push(`col-sm-${sm}`);
|
||||
}
|
||||
if (md) {
|
||||
classes.push(`col-md-${md}`);
|
||||
}
|
||||
if (lg) {
|
||||
classes.push(`col-lg-${lg}`);
|
||||
}
|
||||
if (xl) {
|
||||
classes.push(`col-lg-${xl}`);
|
||||
}
|
||||
return <div className={classNames(styles.col, classes, className)}>{children}</div>;
|
||||
};
|
||||
40
components/layout/GridLayout.module.css
Normal file
40
components/layout/GridLayout.module.css
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
.grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
border-top: 1px solid var(--gray300);
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.row > .col {
|
||||
border-left: 1px solid var(--gray300);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.row > .col:first-child {
|
||||
border-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.row > .col:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.row {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.row > .col {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-left: 0;
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
71
components/layout/Header.js
Normal file
71
components/layout/Header.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'components/common/Link';
|
||||
import Icon from 'components/common/Icon';
|
||||
import LanguageButton from 'components/settings/LanguageButton';
|
||||
import ThemeButton from 'components/settings/ThemeButton';
|
||||
import UpdateNotice from 'components/common/UpdateNotice';
|
||||
import UserButton from 'components/settings/UserButton';
|
||||
import Button from 'components/common/Button';
|
||||
import Logo from 'assets/logo.svg';
|
||||
import styles from './Header.module.css';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import XMark from 'assets/xmark.svg';
|
||||
import Bars from 'assets/bars.svg';
|
||||
|
||||
export default function Header() {
|
||||
const user = useSelector(state => state.user);
|
||||
const [active, setActive] = useState(false);
|
||||
const { dir } = useLocale();
|
||||
|
||||
function handleClick() {
|
||||
setActive(state => !state);
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="container" dir={dir}>
|
||||
{user?.is_admin && <UpdateNotice />}
|
||||
<div className={classNames(styles.header, 'row align-items-center')}>
|
||||
<div className={styles.nav}>
|
||||
<div className="">
|
||||
<div className={styles.title}>
|
||||
<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>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
116
components/layout/Header.module.css
Normal file
116
components/layout/Header.module.css
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
.navbar {
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.burger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
min-height: 100px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-large);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-normal);
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-normal);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav a + a {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.nav {
|
||||
font-size: var(--font-size-large);
|
||||
justify-content: space-between;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.items {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 0.5rem;
|
||||
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;
|
||||
}
|
||||
|
||||
.header .active {
|
||||
display: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
24
components/layout/Layout.js
Normal file
24
components/layout/Layout.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import Head from 'next/head';
|
||||
import Header from 'components/layout/Header';
|
||||
import Footer from 'components/layout/Footer';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export default function Layout({ title, children, header = true, footer = true }) {
|
||||
const { dir } = useLocale();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>umami{title && ` - ${title}`}</title>
|
||||
</Head>
|
||||
|
||||
{header && <Header />}
|
||||
<main className="container" dir={dir}>
|
||||
{children}
|
||||
</main>
|
||||
{footer && <Footer />}
|
||||
<div id="__modals" dir={dir} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
6
components/layout/Layout.module.css
Normal file
6
components/layout/Layout.module.css
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
39
components/layout/MenuLayout.js
Normal file
39
components/layout/MenuLayout.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import classNames from 'classnames';
|
||||
import NavMenu from 'components/common/NavMenu';
|
||||
import styles from './MenuLayout.module.css';
|
||||
|
||||
export default function MenuLayout({
|
||||
menu,
|
||||
selectedOption,
|
||||
className,
|
||||
menuClassName,
|
||||
contentClassName,
|
||||
children,
|
||||
replace = false,
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
function handleSelect(url) {
|
||||
if (replace) {
|
||||
router.replace(url);
|
||||
} else {
|
||||
router.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className, 'row')}>
|
||||
<NavMenu
|
||||
options={menu}
|
||||
selectedOption={selectedOption}
|
||||
className={classNames(styles.menu, menuClassName, 'col-12 col-lg-2')}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
<div className={classNames(styles.content, contentClassName, 'col-12 col-lg-10')}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
components/layout/MenuLayout.module.css
Normal file
31
components/layout/MenuLayout.module.css
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container .menu {
|
||||
padding: 30px 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.container .content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
border-left: 1px solid var(--gray300);
|
||||
padding-left: 30px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.container {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.container .content {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-left: 0;
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
26
components/layout/Page.js
Normal file
26
components/layout/Page.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Page.module.css';
|
||||
|
||||
export default class Page extends React.Component {
|
||||
getSnapshotBeforeUpdate() {
|
||||
if (window.pageXOffset === 0 && window.pageYOffset === 0) return null;
|
||||
|
||||
// Return the scrolled position as the snapshot value
|
||||
return { x: window.pageXOffset, y: window.pageYOffset };
|
||||
}
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
if (snapshot !== null) {
|
||||
// Restore the scrolled position after re-rendering
|
||||
window.scrollTo(snapshot.x, snapshot.y);
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
render() {
|
||||
const { className, children } = this.props;
|
||||
return <div className={classNames(styles.page, className)}>{children}</div>;
|
||||
}
|
||||
}
|
||||
7
components/layout/Page.module.css
Normal file
7
components/layout/Page.module.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 30px;
|
||||
background: var(--gray50);
|
||||
}
|
||||
7
components/layout/PageHeader.js
Normal file
7
components/layout/PageHeader.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './PageHeader.module.css';
|
||||
|
||||
export default function PageHeader({ children, className }) {
|
||||
return <div className={classNames(styles.header, className)}>{children}</div>;
|
||||
}
|
||||
9
components/layout/PageHeader.module.css
Normal file
9
components/layout/PageHeader.module.css
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
min-height: 80px;
|
||||
align-self: stretch;
|
||||
font-weight: bold;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue