Rewrite admin. (#1713)

* Rewrite admin.

* Clean up password forms.

* Fix naming issues.

* CSS Naming.
This commit is contained in:
Brian Cao 2022-12-26 16:57:59 -08:00 committed by GitHub
parent f4db04c3c6
commit e1f99a7d01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
113 changed files with 2054 additions and 1872 deletions

View file

@ -1,63 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactTooltip from 'react-tooltip';
import classNames from 'classnames';
import Icon from './Icon';
import styles from './Button.module.css';
function Button({
type = 'button',
icon,
size,
variant,
children,
className,
tooltip,
tooltipId,
disabled,
iconRight,
onClick = () => {},
...props
}) {
return (
<button
data-tip={tooltip}
data-effect="solid"
data-for={tooltipId}
data-offset={JSON.stringify({ left: 10 })}
type={type}
className={classNames(styles.button, className, {
[styles.large]: size === 'large',
[styles.small]: size === 'small',
[styles.xsmall]: size === 'xsmall',
[styles.action]: variant === 'action',
[styles.danger]: variant === 'danger',
[styles.light]: variant === 'light',
[styles.iconRight]: iconRight,
})}
disabled={disabled}
onClick={!disabled ? onClick : null}
{...props}
>
{icon && <Icon className={styles.icon} icon={icon} size={size} />}
{children && <div className={styles.label}>{children}</div>}
{tooltip && <ReactTooltip id={tooltipId}>{tooltip}</ReactTooltip>}
</button>
);
}
Button.propTypes = {
type: PropTypes.oneOf(['button', 'submit', 'reset']),
icon: PropTypes.node,
size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
variant: PropTypes.oneOf(['action', 'danger', 'light']),
children: PropTypes.node,
className: PropTypes.string,
tooltip: PropTypes.node,
tooltipId: PropTypes.string,
disabled: PropTypes.bool,
iconRight: PropTypes.bool,
onClick: PropTypes.func,
};
export default Button;

View file

@ -1,102 +0,0 @@
.button {
display: flex;
justify-content: center;
align-items: center;
font-size: var(--font-size-md);
color: var(--base900);
background: var(--base100);
padding: 8px 16px;
border-radius: 4px;
border: 0;
outline: none;
cursor: pointer;
position: relative;
}
.button:hover {
background: var(--base200);
}
.button:active {
color: var(--base900);
}
.label {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 300px;
}
.large {
font-size: var(--font-size-lg);
}
.small {
font-size: var(--font-size-sm);
}
.xsmall {
font-size: var(--font-size-xs);
}
.action,
.action:active {
color: var(--base50);
background: var(--base900);
}
.action:hover {
background: var(--base800);
}
.danger,
.danger:active {
color: var(--base50);
background: var(--red500);
}
.danger:hover {
background: var(--red400);
}
.light,
.light:active {
color: var(--base900);
background: transparent;
}
.light:hover {
background: inherit;
}
.button .icon + * {
margin-left: 10px;
}
.button.iconRight .icon {
order: 1;
margin-left: 10px;
}
.button.iconRight .icon + * {
margin: 0;
}
.button:disabled {
cursor: default;
color: var(--base500);
background: var(--base75);
}
.button:disabled:active {
color: var(--base500);
}
.button:disabled:hover {
background: var(--base75);
}
.button.light:disabled {
background: var(--base50);
}

View file

@ -1,42 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Button from './Button';
import styles from './ButtonGroup.module.css';
function ButtonGroup({ items = [], selectedItem, className, size, icon, onClick = () => {} }) {
return (
<div className={classNames(styles.group, className)}>
{items.map(item => {
const { label, value } = item;
return (
<Button
key={value}
className={classNames(styles.button, { [styles.selected]: selectedItem === value })}
size={size}
icon={icon}
onClick={() => onClick(value)}
>
{label}
</Button>
);
})}
</div>
);
}
ButtonGroup.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.node,
value: PropTypes.any.isRequired,
}),
),
selectedItem: PropTypes.any,
className: PropTypes.string,
size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
icon: PropTypes.node,
onClick: PropTypes.func,
};
export default ButtonGroup;

View file

@ -1,31 +0,0 @@
.group {
display: inline-flex;
border-radius: 4px;
overflow: hidden;
border: 1px solid var(--base500);
}
.group .button {
border-radius: 0;
color: var(--base800);
background: var(--base50);
border-left: 1px solid var(--base500);
padding: 4px 8px;
}
.group .button:first-child {
border: 0;
}
.group .button:hover {
background: var(--base100);
}
.group .button + .button {
margin: 0;
}
.group .button.selected {
color: var(--base900);
font-weight: 600;
}

View file

@ -16,7 +16,7 @@ import {
isBefore,
isAfter,
} from 'date-fns';
import Button from './Button';
import { Button, Icon } from 'react-basics';
import useLocale from 'hooks/useLocale';
import { dateFormat } from 'lib/date';
import { chunk } from 'lib/array';
@ -24,7 +24,6 @@ import { getDateLocale } from 'lib/lang';
import Chevron from 'assets/chevron-down.svg';
import Cross from 'assets/times.svg';
import styles from './Calendar.module.css';
import Icon from './Icon';
export default function Calendar({ date, minDate, maxDate, onChange }) {
const { locale } = useLocale();
@ -61,14 +60,18 @@ export default function Calendar({ date, minDate, maxDate, onChange }) {
onClick={toggleMonthSelect}
>
{month}
<Icon className={styles.icon} icon={selectMonth ? <Cross /> : <Chevron />} size="small" />
<Icon className={styles.icon} size="small">
{selectMonth ? <Cross /> : <Chevron />}
</Icon>
</div>
<div
className={classNames(styles.selector, { [styles.open]: selectYear })}
onClick={toggleYearSelect}
>
{year}
<Icon className={styles.icon} icon={selectYear ? <Cross /> : <Chevron />} size="small" />
<Icon className={styles.icon} size="small">
{selectMonth ? <Cross /> : <Chevron />}
</Icon>
</div>
</div>
<div className={styles.body}>
@ -230,12 +233,15 @@ const YearSelector = ({ date, minDate, maxDate, onSelect }) => {
<div className={styles.pager}>
<div className={styles.left}>
<Button
icon={<Chevron />}
size="small"
onClick={handlePrevClick}
disabled={years[0] <= minYear}
variant="light"
/>
>
<Icon>
<Chevron />
</Icon>
</Button>
</div>
<div className={styles.middle}>
<table>
@ -261,12 +267,15 @@ const YearSelector = ({ date, minDate, maxDate, onSelect }) => {
</div>
<div className={styles.right}>
<Button
icon={<Chevron />}
size="small"
onClick={handleNextClick}
disabled={years[years.length - 1] > maxYear}
variant="light"
/>
>
<Icon>
<Chevron />
</Icon>
</Button>
</div>
</div>
);

View file

@ -1,39 +0,0 @@
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import Icon from 'components/common/Icon';
import Check from 'assets/check.svg';
import styles from './Checkbox.module.css';
function Checkbox({ name, value, label, onChange }) {
const ref = useRef();
const onClick = () => ref.current.click();
return (
<div className={styles.container}>
<div className={styles.checkbox} onClick={onClick}>
{value && <Icon icon={<Check />} size="small" />}
</div>
<label className={styles.label} htmlFor={name} onClick={onClick}>
{label}
</label>
<input
ref={ref}
className={styles.input}
type="checkbox"
name={name}
defaultChecked={value}
onChange={onChange}
/>
</div>
);
}
Checkbox.propTypes = {
name: PropTypes.string,
value: PropTypes.any,
label: PropTypes.node,
onChange: PropTypes.func,
};
export default Checkbox;

View file

@ -1,30 +0,0 @@
.container {
display: flex;
align-items: center;
position: relative;
overflow: hidden;
}
.checkbox {
display: flex;
justify-content: center;
align-items: center;
width: 20px;
height: 20px;
border: 1px solid var(--base500);
border-radius: 4px;
}
.label {
margin-left: 10px;
user-select: none; /* disable text selection when clicking to toggle the checkbox */
}
.input {
position: absolute;
visibility: hidden;
height: 0;
width: 0;
bottom: 100%;
right: 100%;
}

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Button from './Button';
import { Button } from 'react-basics';
import { FormattedMessage } from 'react-intl';
const defaultText = (

View file

@ -1,14 +1,13 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { endOfYear, isSameDay } from 'date-fns';
import Modal from './Modal';
import DropDown from './DropDown';
import Calendar from 'assets/calendar-alt.svg';
import DatePickerForm from 'components/forms/DatePickerForm';
import { endOfYear, isSameDay } from 'date-fns';
import useLocale from 'hooks/useLocale';
import { dateFormat } from 'lib/date';
import Calendar from 'assets/calendar-alt.svg';
import Icon from './Icon';
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { Icon, Modal } from 'react-basics';
import { FormattedMessage } from 'react-intl';
import DropDown from './DropDown';
export const filterOptions = [
{ label: <FormattedMessage id="label.today" defaultMessage="Today" />, value: '1day' },
@ -120,7 +119,9 @@ const CustomRange = ({ startDate, endDate, onClick }) => {
return (
<>
<Icon icon={<Calendar />} className="mr-2" onClick={handleClick} />
<Icon className="mr-2" onClick={handleClick}>
<Calendar />
</Icon>
{dateFormat(startDate, 'd LLL y', locale)}
{!isSameDay(startDate, endDate) && `${dateFormat(endDate, 'd LLL y', locale)}`}
</>

View file

@ -5,7 +5,7 @@ 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';
import { Icon } from 'react-basics';
function DropDown({ value, className, menuClassName, options = [], onChange = () => {} }) {
const [showMenu, setShowMenu] = useState(false);
@ -33,7 +33,9 @@ function DropDown({ value, className, menuClassName, options = [], onChange = ()
<div ref={ref} className={classNames(styles.dropdown, className)} onClick={handleShowMenu}>
<div className={styles.value}>
<div className={styles.text}>{options.find(e => e.value === value)?.label || value}</div>
<Icon icon={<Chevron />} className={styles.icon} size="small" />
<Icon className={styles.icon} size="small">
<Chevron />
</Icon>
</div>
{showMenu && (
<Menu

View file

@ -1,15 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'components/common/Icon';
import { Icon, Flexbox } from 'react-basics';
import Logo from 'assets/logo.svg';
import styles from './EmptyPlaceholder.module.css';
function EmptyPlaceholder({ msg, children }) {
return (
<div className={styles.placeholder}>
<Icon className={styles.icon} icon={<Logo />} size="xlarge" />
<Icon className={styles.icon} size="xl">
<Logo />
</Icon>
<h2 className={styles.msg}>{msg}</h2>
{children}
<Flexbox justifyContent="center" alignItems="center">
{children}
</Flexbox>
</div>
);
}

View file

@ -1,13 +1,15 @@
import Exclamation from 'assets/exclamation-triangle.svg';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import Icon from './Icon';
import Exclamation from 'assets/exclamation-triangle.svg';
import styles from './ErrorMessage.module.css';
import { Icon } from 'react-basics';
export default function ErrorMessage() {
return (
<div className={styles.error}>
<Icon icon={<Exclamation />} className={styles.icon} size="large" />
<Icon className={styles.icon} size="large">
<Exclamation />
</Icon>
<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />
</div>
);

View file

@ -1,10 +1,9 @@
import List from 'assets/list-ul.svg';
import Modal from 'components/common/Modal';
import EventDataForm from 'components/forms/EventDataForm';
import PropTypes from 'prop-types';
import { useState } from 'react';
import { Button, Icon, Modal } from 'react-basics';
import { FormattedMessage } from 'react-intl';
import Button from './Button';
import EventDataForm from 'components/forms/EventDataForm';
import styles from './EventDataButton.module.css';
function EventDataButton({ websiteId }) {
@ -23,13 +22,15 @@ function EventDataButton({ websiteId }) {
return (
<>
<Button
icon={<List />}
tooltip={<FormattedMessage id="label.event-data" defaultMessage="Event" />}
tooltipId="button-event"
size="small"
onClick={handleClick}
className={styles.button}
>
<Icon>
<List />
</Icon>
Event Data
</Button>
{showEventData && (

View file

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ButtonLayout from 'components/layout/ButtonLayout';
import ButtonGroup from './ButtonGroup';
import { ButtonGroup } from 'react-basics';
function FilterButtons({ buttons, selected, onClick }) {
return (

View file

@ -4,7 +4,7 @@ import Link from 'next/link';
import { safeDecodeURI } from 'next-basics';
import usePageQuery from 'hooks/usePageQuery';
import External from 'assets/arrow-up-right-from-square.svg';
import Icon from './Icon';
import { Icon } from 'react-basics';
import styles from './FilterLink.module.css';
export default function FilterLink({ id, value, label, externalUrl }) {
@ -26,7 +26,9 @@ export default function FilterLink({ id, value, label, externalUrl }) {
</Link>
{externalUrl && (
<a className={styles.link} href={externalUrl} target="_blank" rel="noreferrer noopener">
<Icon icon={<External />} className={styles.icon} />
<Icon className={styles.icon}>
<External />
</Icon>
</a>
)}
</div>

View file

@ -1,4 +1,4 @@
import Button from 'components/common/Button';
import { Button, Icon } from 'react-basics';
import XMark from 'assets/xmark.svg';
import Bars from 'assets/bars.svg';
import { useState } from 'react';
@ -33,11 +33,9 @@ export default function HamburgerButton() {
return (
<>
<Button
className={styles.button}
icon={active ? <XMark /> : <Bars />}
onClick={handleClick}
/>
<Button className={styles.button} onClick={handleClick}>
<Icon>{active ? <XMark /> : <Bars />}</Icon>
</Button>
{active && <MobileMenu items={menuItems} onClose={handleClose} />}
</>
);

View file

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import NextLink from 'next/link';
import Icon from './Icon';
import { Icon } from 'react-basics';
import styles from './Link.module.css';
function Link({ className, icon, children, size, iconRight, onClick, ...props }) {

View file

@ -2,12 +2,12 @@ import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Menu from 'components/common/Menu';
import Button from 'components/common/Button';
import useDocumentClick from 'hooks/useDocumentClick';
import styles from './MenuButton.module.css';
import { Button } from 'react-basics';
function MenuButton({
icon,
children,
value,
options,
buttonClassName,
@ -41,7 +41,6 @@ function MenuButton({
return (
<div className={styles.container} ref={ref}>
<Button
icon={icon}
className={classNames(styles.button, buttonClassName, { [styles.open]: showMenu })}
onClick={toggleMenu}
variant={buttonVariant}
@ -49,6 +48,7 @@ function MenuButton({
{!hideLabel && (
<div className={styles.text}>{renderValue ? renderValue(selectedOption) : value}</div>
)}
{children}
</Button>
{showMenu && (
<Menu

View file

@ -1,6 +1,6 @@
import classNames from 'classnames';
import Link from './Link';
import Button from './Button';
import { Button } from 'react-basics';
import XMark from 'assets/xmark.svg';
import styles from './MobileMenu.module.css';

View file

@ -1,26 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { useSpring, animated } from 'react-spring';
import styles from './Modal.module.css';
function Modal({ title, children }) {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return ReactDOM.createPortal(
<animated.div className={styles.modal} style={props}>
<div className={styles.content}>
{title && <div className={styles.header}>{title}</div>}
<div className={styles.body}>{children}</div>
</div>
</animated.div>,
document.getElementById('__modals'),
);
}
Modal.propTypes = {
title: PropTypes.node,
children: PropTypes.node,
};
export default Modal;

View file

@ -1,46 +0,0 @@
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
z-index: 2;
}
.modal:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
background: #000;
opacity: 0.5;
}
.content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--base50);
min-width: 400px;
min-height: 100px;
max-width: 100vw;
z-index: 1;
border: 1px solid var(--base300);
padding: 30px;
border-radius: 4px;
}
.header {
font-weight: 600;
margin-bottom: 20px;
}
.body {
display: flex;
flex-direction: column;
}

View file

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import useStore from 'store/queries';
import { setDateRange } from 'store/websites';
import Button from './Button';
import { Button, Icon } from 'react-basics';
import Refresh from 'assets/redo.svg';
import Dots from 'assets/ellipsis-h.svg';
import useDateRange from 'hooks/useDateRange';
@ -31,12 +31,13 @@ function RefreshButton({ websiteId }) {
return (
<Button
icon={loading ? <Dots /> : <Refresh />}
tooltip={<FormattedMessage id="label.refresh" defaultMessage="Refresh" />}
tooltipId="button-refresh"
size="small"
onClick={handleClick}
/>
>
<Icon>{loading ? <Dots /> : <Refresh />}</Icon>
</Button>
);
}

View file

@ -1,90 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import NoData from 'components/common/NoData';
import styles from './Table.module.css';
function Table({
columns,
rows,
empty,
className,
bodyClassName,
rowKey,
showHeader = true,
children,
}) {
if (empty && rows.length === 0) {
return empty;
}
return (
<div className={classNames(styles.table, className)}>
{showHeader && (
<div className={classNames(styles.header, 'row')}>
{columns.map(({ key, label, className, style, header }) => (
<div
key={key}
className={classNames(styles.head, className, header?.className)}
style={{ ...style, ...header?.style }}
>
{label}
</div>
))}
</div>
)}
<div className={classNames(styles.body, bodyClassName)}>
{rows.length === 0 && <NoData />}
{!children &&
rows.map((row, index) => {
const id = rowKey ? rowKey(row) : index;
return <TableRow key={id} columns={columns} row={row} />;
})}
{children}
</div>
</div>
);
}
const styledObject = PropTypes.shape({
className: PropTypes.string,
style: PropTypes.object,
});
Table.propTypes = {
columns: PropTypes.arrayOf(
PropTypes.shape({
cell: styledObject,
className: PropTypes.string,
header: styledObject,
key: PropTypes.string,
label: PropTypes.node,
render: PropTypes.func,
style: PropTypes.object,
}),
),
rows: PropTypes.arrayOf(PropTypes.object),
empty: PropTypes.node,
className: PropTypes.string,
bodyClassName: PropTypes.string,
rowKey: PropTypes.func,
showHeader: PropTypes.bool,
children: PropTypes.node,
};
export default Table;
export const TableRow = ({ columns, row }) => (
<div className={classNames(styles.row, 'row')}>
{columns.map(({ key, label, render, className, style, cell }, index) => (
<div
key={`${key}-${index}`}
className={classNames(styles.cell, className, cell?.className)}
style={{ ...style, ...cell?.style }}
>
{label && <label>{label}</label>}
{render ? render(row) : row[key]}
</div>
))}
</div>
);

View file

@ -1,55 +0,0 @@
.table {
display: flex;
flex-direction: column;
}
.table label {
display: none;
font-size: var(--font-size-xs);
font-weight: bold;
}
.header {
border-bottom: 1px solid var(--base300);
}
.head {
font-size: var(--font-size-sm);
font-weight: 600;
line-height: 40px;
}
.body {
position: relative;
display: flex;
flex-direction: column;
}
.row {
border-bottom: 1px solid var(--base300);
padding: 10px 0;
}
.cell {
display: flex;
flex-direction: column;
align-items: flex-start;
}
@media only screen and (max-width: 992px) {
.table label {
display: block;
}
.header {
display: none;
}
.row {
flex-direction: column;
}
.cell {
margin-bottom: 20px;
}
}

View file

@ -1,35 +0,0 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { useSpring, animated } from 'react-spring';
import Icon from 'components/common/Icon';
import Close from 'assets/times.svg';
import styles from './Toast.module.css';
function Toast({ message, timeout = 3000, onClose }) {
const props = useSpring({
opacity: 1,
transform: 'translate3d(0,0px,0)',
from: { opacity: 0, transform: 'translate3d(0,-40px,0)' },
});
useEffect(() => {
setTimeout(onClose, timeout);
}, []);
return ReactDOM.createPortal(
<animated.div className={styles.toast} style={props} onClick={onClose}>
<div className={styles.message}>{message}</div>
<Icon className={styles.close} icon={<Close />} size="small" />
</animated.div>,
document.getElementById('__modals'),
);
}
Toast.propTypes = {
message: PropTypes.node,
timeout: PropTypes.number,
onClose: PropTypes.func,
};
export default Toast;

View file

@ -1,25 +0,0 @@
.toast {
position: fixed;
top: 30px;
left: 0;
right: 0;
width: 300px;
border-radius: 5px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
color: var(--msgColor);
background: var(--green400);
margin: auto;
z-index: 2;
cursor: pointer;
}
.message {
font-size: var(--font-size-md);
}
.close {
margin-left: 20px;
}

View file

@ -4,7 +4,7 @@ import { setItem } from 'next-basics';
import ButtonLayout from 'components/layout/ButtonLayout';
import useStore, { checkVersion } from 'store/version';
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
import Button from './Button';
import { Button } from 'react-basics';
import styles from './UpdateNotice.module.css';
export default function UpdateNotice() {