mirror of
https://github.com/kremalicious/umami.git
synced 2024-06-30 05:31:50 +02:00
More refactoring.
This commit is contained in:
parent
5f15ad0807
commit
02a1438cfe
|
@ -1,9 +1,9 @@
|
||||||
import { Icon, Button, PopupTrigger, Popup, Tooltip, Icons, Text } from 'react-basics';
|
import { Icon, Button, PopupTrigger, Popup, Tooltip, Text } from 'react-basics';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { languages } from 'lib/lang';
|
import { languages } from 'lib/lang';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import { Globe } from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import { labels } from 'components/messages';
|
import { labels } from 'components/messages';
|
||||||
import styles from './LanguageButton.module.css';
|
import styles from './LanguageButton.module.css';
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export default function LanguageButton({ tooltipPosition = 'top' }) {
|
||||||
<PopupTrigger action="hover">
|
<PopupTrigger action="hover">
|
||||||
<Button variant="quiet">
|
<Button variant="quiet">
|
||||||
<Icon>
|
<Icon>
|
||||||
<Globe />
|
<Icons.Globe />
|
||||||
</Icon>
|
</Icon>
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip position={tooltipPosition}>{formatMessage(labels.language)}</Tooltip>
|
<Tooltip position={tooltipPosition}>{formatMessage(labels.language)}</Tooltip>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useTransition, animated } from 'react-spring';
|
||||||
import { Button, Icon, PopupTrigger, Tooltip } from 'react-basics';
|
import { Button, Icon, PopupTrigger, Tooltip } from 'react-basics';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import useTheme from 'hooks/useTheme';
|
import useTheme from 'hooks/useTheme';
|
||||||
import { Sun, Moon } from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import { labels } from 'components/messages';
|
import { labels } from 'components/messages';
|
||||||
import styles from './ThemeButton.module.css';
|
import styles from './ThemeButton.module.css';
|
||||||
|
|
||||||
|
@ -28,11 +28,11 @@ export default function ThemeButton({ tooltipPosition = 'top' }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopupTrigger action="hover" popupProps={{ position: 'top' }}>
|
<PopupTrigger action="hover">
|
||||||
<Button variant="quiet" className={styles.button} onClick={handleClick}>
|
<Button variant="quiet" className={styles.button} onClick={handleClick}>
|
||||||
{transitions((style, item) => (
|
{transitions((style, item) => (
|
||||||
<animated.div key={item} style={style}>
|
<animated.div key={item} style={style}>
|
||||||
<Icon className={styles.icon}>{item === 'light' ? <Sun /> : <Moon />}</Icon>
|
<Icon className={styles.icon}>{item === 'light' ? <Icons.Sun /> : <Icons.Moon />}</Icon>
|
||||||
</animated.div>
|
</animated.div>
|
||||||
))}
|
))}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -13,7 +13,7 @@ import styles from './UserButton.module.css';
|
||||||
export default function UserButton() {
|
export default function UserButton() {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
const user = useUser();
|
const { user } = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { adminDisabled } = useConfig();
|
const { adminDisabled } = useConfig();
|
||||||
|
|
||||||
|
|
|
@ -1,280 +0,0 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import {
|
|
||||||
startOfWeek,
|
|
||||||
startOfMonth,
|
|
||||||
startOfYear,
|
|
||||||
endOfMonth,
|
|
||||||
addDays,
|
|
||||||
subDays,
|
|
||||||
addYears,
|
|
||||||
subYears,
|
|
||||||
addMonths,
|
|
||||||
setMonth,
|
|
||||||
setYear,
|
|
||||||
isSameDay,
|
|
||||||
isBefore,
|
|
||||||
isAfter,
|
|
||||||
} from 'date-fns';
|
|
||||||
import { Button, Icon, Icons } from 'react-basics';
|
|
||||||
import { chunkArray } from 'next-basics';
|
|
||||||
import useLocale from 'hooks/useLocale';
|
|
||||||
import { dateFormat } from 'lib/date';
|
|
||||||
import { getDateLocale } from 'lib/lang';
|
|
||||||
import styles from './Calendar.module.css';
|
|
||||||
|
|
||||||
export default function Calendar({ date, minDate, maxDate, onChange }) {
|
|
||||||
const { locale } = useLocale();
|
|
||||||
const [selectMonth, setSelectMonth] = useState(false);
|
|
||||||
const [selectYear, setSelectYear] = useState(false);
|
|
||||||
|
|
||||||
const month = dateFormat(date, 'MMMM', locale);
|
|
||||||
const year = date.getFullYear();
|
|
||||||
|
|
||||||
function toggleMonthSelect() {
|
|
||||||
setSelectYear(false);
|
|
||||||
setSelectMonth(state => !state);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleYearSelect() {
|
|
||||||
setSelectMonth(false);
|
|
||||||
setSelectYear(state => !state);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChange(value) {
|
|
||||||
setSelectMonth(false);
|
|
||||||
setSelectYear(false);
|
|
||||||
if (value) {
|
|
||||||
onChange(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.calendar}>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<div>{date.getDate()}</div>
|
|
||||||
<div
|
|
||||||
className={classNames(styles.selector, { [styles.open]: selectMonth })}
|
|
||||||
onClick={toggleMonthSelect}
|
|
||||||
>
|
|
||||||
{month}
|
|
||||||
<Icon className={styles.icon} size="small">
|
|
||||||
{selectMonth ? <Icons.Close /> : <Icons.ChevronDown />}
|
|
||||||
</Icon>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={classNames(styles.selector, { [styles.open]: selectYear })}
|
|
||||||
onClick={toggleYearSelect}
|
|
||||||
>
|
|
||||||
{year}
|
|
||||||
<Icon className={styles.icon} size="small">
|
|
||||||
{selectMonth ? <Icons.Close /> : <Icons.ChevronDown />}
|
|
||||||
</Icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.body}>
|
|
||||||
{!selectMonth && !selectYear && (
|
|
||||||
<DaySelector
|
|
||||||
date={date}
|
|
||||||
minDate={minDate}
|
|
||||||
maxDate={maxDate}
|
|
||||||
locale={locale}
|
|
||||||
onSelect={handleChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectMonth && (
|
|
||||||
<MonthSelector
|
|
||||||
date={date}
|
|
||||||
minDate={minDate}
|
|
||||||
maxDate={maxDate}
|
|
||||||
locale={locale}
|
|
||||||
onSelect={handleChange}
|
|
||||||
onClose={toggleMonthSelect}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectYear && (
|
|
||||||
<YearSelector
|
|
||||||
date={date}
|
|
||||||
minDate={minDate}
|
|
||||||
maxDate={maxDate}
|
|
||||||
onSelect={handleChange}
|
|
||||||
onClose={toggleYearSelect}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => {
|
|
||||||
const dateLocale = getDateLocale(locale);
|
|
||||||
const weekStartsOn = dateLocale?.options?.weekStartsOn || 0;
|
|
||||||
const startWeek = startOfWeek(date, {
|
|
||||||
locale: dateLocale,
|
|
||||||
weekStartsOn,
|
|
||||||
});
|
|
||||||
const startMonth = startOfMonth(date);
|
|
||||||
const startDay = subDays(startMonth, startMonth.getDay() - weekStartsOn);
|
|
||||||
const month = date.getMonth();
|
|
||||||
const year = date.getFullYear();
|
|
||||||
|
|
||||||
const daysOfWeek = [];
|
|
||||||
for (let i = 0; i < 7; i++) {
|
|
||||||
daysOfWeek.push(addDays(startWeek, i));
|
|
||||||
}
|
|
||||||
|
|
||||||
const days = [];
|
|
||||||
for (let i = 0; i < 35; i++) {
|
|
||||||
days.push(addDays(startDay, i));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{daysOfWeek.map((day, i) => (
|
|
||||||
<th key={i} className={locale}>
|
|
||||||
{dateFormat(day, 'EEE', locale)}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{chunkArray(days, 7).map((week, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
{week.map((day, j) => {
|
|
||||||
const disabled = isBefore(day, minDate) || isAfter(day, maxDate);
|
|
||||||
return (
|
|
||||||
<td
|
|
||||||
key={j}
|
|
||||||
className={classNames({
|
|
||||||
[styles.selected]: isSameDay(date, day),
|
|
||||||
[styles.faded]: day.getMonth() !== month || day.getFullYear() !== year,
|
|
||||||
[styles.disabled]: disabled,
|
|
||||||
})}
|
|
||||||
onClick={!disabled ? () => onSelect(day) : null}
|
|
||||||
>
|
|
||||||
{day.getDate()}
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MonthSelector = ({ date, minDate, maxDate, locale, onSelect }) => {
|
|
||||||
const start = startOfYear(date);
|
|
||||||
const months = [];
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
months.push(addMonths(start, i));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSelect(value) {
|
|
||||||
onSelect(setMonth(date, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
{chunkArray(months, 3).map((row, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
{row.map((month, j) => {
|
|
||||||
const disabled =
|
|
||||||
isBefore(endOfMonth(month), minDate) || isAfter(startOfMonth(month), maxDate);
|
|
||||||
return (
|
|
||||||
<td
|
|
||||||
key={j}
|
|
||||||
className={classNames(locale, {
|
|
||||||
[styles.selected]: month.getMonth() === date.getMonth(),
|
|
||||||
[styles.disabled]: disabled,
|
|
||||||
})}
|
|
||||||
onClick={!disabled ? () => handleSelect(month.getMonth()) : null}
|
|
||||||
>
|
|
||||||
{dateFormat(month, 'MMMM', locale)}
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const YearSelector = ({ date, minDate, maxDate, onSelect }) => {
|
|
||||||
const [currentDate, setCurrentDate] = useState(date);
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const currentYear = currentDate.getFullYear();
|
|
||||||
const minYear = minDate.getFullYear();
|
|
||||||
const maxYear = maxDate.getFullYear();
|
|
||||||
const years = [];
|
|
||||||
for (let i = 0; i < 15; i++) {
|
|
||||||
years.push(currentYear - 7 + i);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSelect(value) {
|
|
||||||
onSelect(setYear(date, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePrevClick() {
|
|
||||||
setCurrentDate(state => subYears(state, 15));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleNextClick() {
|
|
||||||
setCurrentDate(state => addYears(state, 15));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.pager}>
|
|
||||||
<div className={styles.left}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={handlePrevClick}
|
|
||||||
disabled={years[0] <= minYear}
|
|
||||||
variant="light"
|
|
||||||
>
|
|
||||||
<Icon>
|
|
||||||
<Icons.ChevronDown />
|
|
||||||
</Icon>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className={styles.middle}>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
{chunkArray(years, 5).map((row, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
{row.map((n, j) => (
|
|
||||||
<td
|
|
||||||
key={j}
|
|
||||||
className={classNames({
|
|
||||||
[styles.selected]: n === year,
|
|
||||||
[styles.disabled]: n < minYear || n > maxYear,
|
|
||||||
})}
|
|
||||||
onClick={() => (n < minYear || n > maxYear ? null : handleSelect(n))}
|
|
||||||
>
|
|
||||||
{n}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div className={styles.right}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={handleNextClick}
|
|
||||||
disabled={years[years.length - 1] > maxYear}
|
|
||||||
variant="light"
|
|
||||||
>
|
|
||||||
<Icon>
|
|
||||||
<Icons.ChevronDown />
|
|
||||||
</Icon>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,110 +0,0 @@
|
||||||
.calendar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 306px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar table {
|
|
||||||
width: 100%;
|
|
||||||
border-spacing: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td {
|
|
||||||
color: var(--base800);
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: center;
|
|
||||||
height: 40px;
|
|
||||||
width: 40px;
|
|
||||||
border-radius: 5px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td:hover {
|
|
||||||
border: 1px solid var(--base300);
|
|
||||||
background: var(--base75);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td.faded {
|
|
||||||
color: var(--base500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td.selected {
|
|
||||||
font-weight: 600;
|
|
||||||
border: 1px solid var(--base600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td.selected:hover {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td.disabled {
|
|
||||||
color: var(--base400);
|
|
||||||
background: var(--base75);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td.disabled:hover {
|
|
||||||
cursor: default;
|
|
||||||
background: var(--base75);
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar td.faded.disabled {
|
|
||||||
background: var(--base100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 40px;
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selector {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pager {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pager button {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.middle {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left,
|
|
||||||
.right {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left svg {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.right svg {
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 992px) {
|
|
||||||
.calendar table {
|
|
||||||
max-width: calc(100vw - 30px);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Button } from 'react-basics';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
const defaultText = (
|
|
||||||
<FormattedMessage id="label.copy-to-clipboard" defaultMessage="Copy to clipboard" />
|
|
||||||
);
|
|
||||||
|
|
||||||
function CopyButton({ element, ...props }) {
|
|
||||||
const [text, setText] = useState(defaultText);
|
|
||||||
|
|
||||||
function handleClick() {
|
|
||||||
if (element?.current) {
|
|
||||||
element.current.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
setText(<FormattedMessage id="message.copied" defaultMessage="Copied!" />);
|
|
||||||
window.getSelection().removeAllRanges();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button {...props} onClick={handleClick}>
|
|
||||||
{text}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CopyButton.propTypes = {
|
|
||||||
element: PropTypes.shape({
|
|
||||||
current: PropTypes.shape({
|
|
||||||
select: PropTypes.func.isRequired,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CopyButton;
|
|
|
@ -1,66 +0,0 @@
|
||||||
import { useState, useRef } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
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 'react-basics';
|
|
||||||
|
|
||||||
function DropDown({ value, className, menuClassName, options = [], onChange = () => {} }) {
|
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
|
||||||
const ref = useRef();
|
|
||||||
const selectedOption = options.find(e => e.value === value);
|
|
||||||
|
|
||||||
function handleShowMenu() {
|
|
||||||
setShowMenu(state => !state);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSelect(selected, e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowMenu(false);
|
|
||||||
|
|
||||||
onChange(selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
useDocumentClick(e => {
|
|
||||||
if (!ref.current?.contains(e.target)) {
|
|
||||||
setShowMenu(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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 className={styles.icon} size="small">
|
|
||||||
<Chevron />
|
|
||||||
</Icon>
|
|
||||||
</div>
|
|
||||||
{showMenu && (
|
|
||||||
<Menu
|
|
||||||
className={menuClassName}
|
|
||||||
options={options}
|
|
||||||
selectedOption={selectedOption}
|
|
||||||
onSelect={handleSelect}
|
|
||||||
float="bottom"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DropDown.propTypes = {
|
|
||||||
value: PropTypes.any,
|
|
||||||
className: PropTypes.string,
|
|
||||||
menuClassName: PropTypes.string,
|
|
||||||
options: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
value: PropTypes.any.isRequired,
|
|
||||||
label: PropTypes.node,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DropDown;
|
|
|
@ -1,27 +0,0 @@
|
||||||
.dropdown {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid var(--base500);
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 4px 16px;
|
|
||||||
min-width: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
|
@ -2,8 +2,7 @@ import classNames from 'classnames';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { safeDecodeURI } from 'next-basics';
|
import { safeDecodeURI } from 'next-basics';
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
import External from 'assets/arrow-up-right-from-square.svg';
|
import { Icon, Icons } from 'react-basics';
|
||||||
import { Icon } from 'react-basics';
|
|
||||||
import styles from './FilterLink.module.css';
|
import styles from './FilterLink.module.css';
|
||||||
|
|
||||||
export default function FilterLink({ id, value, label, externalUrl }) {
|
export default function FilterLink({ id, value, label, externalUrl }) {
|
||||||
|
@ -26,7 +25,7 @@ export default function FilterLink({ id, value, label, externalUrl }) {
|
||||||
{externalUrl && (
|
{externalUrl && (
|
||||||
<a className={styles.link} href={externalUrl} target="_blank" rel="noreferrer noopener">
|
<a className={styles.link} href={externalUrl} target="_blank" rel="noreferrer noopener">
|
||||||
<Icon className={styles.icon}>
|
<Icon className={styles.icon}>
|
||||||
<External />
|
<Icons.External />
|
||||||
</Icon>
|
</Icon>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { Button, Icon } from 'react-basics';
|
import { Button, Icon } from 'react-basics';
|
||||||
import XMark from 'assets/xmark.svg';
|
|
||||||
import Bars from 'assets/bars.svg';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import styles from './HamburgerButton.module.css';
|
|
||||||
import MobileMenu from './MobileMenu';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import MobileMenu from './MobileMenu';
|
||||||
|
import Icons from 'components/icons';
|
||||||
|
import styles from './HamburgerButton.module.css';
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
|
@ -37,7 +36,7 @@ export default function HamburgerButton() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button className={styles.button} onClick={handleClick}>
|
<Button className={styles.button} onClick={handleClick}>
|
||||||
<Icon>{active ? <XMark /> : <Bars />}</Icon>
|
<Icon>{active ? <Icons.Close /> : <Icons.Menu />}</Icon>
|
||||||
</Button>
|
</Button>
|
||||||
{active && <MobileMenu items={menuItems} onClose={handleClose} />}
|
{active && <MobileMenu items={menuItems} onClose={handleClose} />}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,19 +1,25 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Link from './Link';
|
import Link from 'next/link';
|
||||||
import { Button } from 'react-basics';
|
import { Button, Icon } from 'react-basics';
|
||||||
import XMark from 'assets/xmark.svg';
|
import Icons from 'components/icons';
|
||||||
import styles from './MobileMenu.module.css';
|
import styles from './MobileMenu.module.css';
|
||||||
|
|
||||||
export default function MobileMenu({ items = [], onClose }) {
|
export default function MobileMenu({ items = [], onClose }) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.menu, 'container')}>
|
<div className={classNames(styles.menu)}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<Button icon={<XMark />} onClick={onClose} />
|
<Button onClick={onClose}>
|
||||||
|
<Icon>
|
||||||
|
<Icons.Close />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.items}>
|
<div className={styles.items}>
|
||||||
{items.map(({ label, value }) => (
|
{items.map(({ label, value }) => (
|
||||||
<Link key={value} href={value} className={styles.item} onClick={onClose}>
|
<Link key={value} href={value}>
|
||||||
|
<a className={styles.item} onClick={onClose}>
|
||||||
{label}
|
{label}
|
||||||
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import useStore from 'store/queries';
|
import useStore from 'store/queries';
|
||||||
import { setDateRange } from 'store/websites';
|
import { setDateRange } from 'store/websites';
|
||||||
import { Button, Icon } from 'react-basics';
|
import { Button, Icon } from 'react-basics';
|
||||||
import Refresh from 'assets/redo.svg';
|
|
||||||
import Dots from 'assets/ellipsis-h.svg';
|
|
||||||
import useDateRange from 'hooks/useDateRange';
|
import useDateRange from 'hooks/useDateRange';
|
||||||
|
import Icons from 'components/icons';
|
||||||
|
|
||||||
function RefreshButton({ websiteId }) {
|
function RefreshButton({ websiteId }) {
|
||||||
const [dateRange] = useDateRange(websiteId);
|
const [dateRange] = useDateRange(websiteId);
|
||||||
|
@ -17,7 +15,7 @@ function RefreshButton({ websiteId }) {
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (!loading && dateRange) {
|
if (!loading && dateRange) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (/^[\d]+/.test(dateRange.value)) {
|
if (/^\d+/.test(dateRange.value)) {
|
||||||
setDateRange(websiteId, dateRange.value);
|
setDateRange(websiteId, dateRange.value);
|
||||||
} else {
|
} else {
|
||||||
setDateRange(websiteId, dateRange);
|
setDateRange(websiteId, dateRange);
|
||||||
|
@ -36,13 +34,11 @@ function RefreshButton({ websiteId }) {
|
||||||
size="small"
|
size="small"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<Icon>{loading ? <Dots /> : <Refresh />}</Icon>
|
<Icon>
|
||||||
|
<Icons.Refresh />
|
||||||
|
</Icon>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
RefreshButton.propTypes = {
|
|
||||||
websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RefreshButton;
|
export default RefreshButton;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Icons } from 'react-basics';
|
||||||
import Bolt from 'assets/bolt.svg';
|
import Bolt from 'assets/bolt.svg';
|
||||||
import Calendar from 'assets/calendar.svg';
|
import Calendar from 'assets/calendar.svg';
|
||||||
import Clock from 'assets/clock.svg';
|
import Clock from 'assets/clock.svg';
|
||||||
|
@ -12,7 +13,8 @@ import Sun from 'assets/sun.svg';
|
||||||
import User from 'assets/user.svg';
|
import User from 'assets/user.svg';
|
||||||
import Users from 'assets/users.svg';
|
import Users from 'assets/users.svg';
|
||||||
|
|
||||||
export {
|
const icons = {
|
||||||
|
...Icons,
|
||||||
Bolt,
|
Bolt,
|
||||||
Calendar,
|
Calendar,
|
||||||
Clock,
|
Clock,
|
||||||
|
@ -27,3 +29,5 @@ export {
|
||||||
User,
|
User,
|
||||||
Users,
|
Users,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default icons;
|
||||||
|
|
|
@ -5,7 +5,11 @@ import useRequireLogin from 'hooks/useRequireLogin';
|
||||||
import styles from './AppLayout.module.css';
|
import styles from './AppLayout.module.css';
|
||||||
|
|
||||||
export default function AppLayout({ title, children }) {
|
export default function AppLayout({ title, children }) {
|
||||||
useRequireLogin();
|
const { user } = useRequireLogin();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.layout}>
|
<div className={styles.layout}>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import styles from './Header.module.css';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export default function Header({ className }) {
|
export default function Header({ className }) {
|
||||||
const user = useUser();
|
const { user } = useUser();
|
||||||
const { pathname } = useRouter();
|
const { pathname } = useRouter();
|
||||||
const { updatesDisabled, adminDisabled } = useConfig();
|
const { updatesDisabled, adminDisabled } = useConfig();
|
||||||
const isSharePage = pathname.includes('/share/');
|
const isSharePage = pathname.includes('/share/');
|
||||||
|
|
|
@ -1,32 +1,38 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { Icon, Text, Icons } from 'react-basics';
|
import { Icon, Text } from 'react-basics';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Dashboard, Logo, Profile, User, Users, Clock, Globe } from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import ThemeButton from '../buttons/ThemeButton';
|
import ThemeButton from 'components/buttons/ThemeButton';
|
||||||
import LanguageButton from 'components/buttons/LanguageButton';
|
import LanguageButton from 'components/buttons/LanguageButton';
|
||||||
import LogoutButton from 'components/buttons/LogoutButton';
|
import LogoutButton from 'components/buttons/LogoutButton';
|
||||||
import { labels } from 'components/messages';
|
import { labels } from 'components/messages';
|
||||||
|
import useUser from 'hooks/useUser';
|
||||||
import NavGroup from './NavGroup';
|
import NavGroup from './NavGroup';
|
||||||
import styles from './NavBar.module.css';
|
import styles from './NavBar.module.css';
|
||||||
|
|
||||||
export default function NavBar() {
|
export default function NavBar() {
|
||||||
|
const { user } = useUser();
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const [minimized, setMinimized] = useState(false);
|
const [minimized, setMinimized] = useState(false);
|
||||||
const tooltipPosition = minimized ? 'right' : 'top';
|
const tooltipPosition = minimized ? 'right' : 'top';
|
||||||
|
|
||||||
const analytics = [
|
const analytics = [
|
||||||
{ label: formatMessage(labels.dashboard), url: '/dashboard', icon: <Dashboard /> },
|
{ label: formatMessage(labels.dashboard), url: '/dashboard', icon: <Icons.Dashboard /> },
|
||||||
{ label: formatMessage(labels.realtime), url: '/realtime', icon: <Clock /> },
|
{ label: formatMessage(labels.realtime), url: '/realtime', icon: <Icons.Clock /> },
|
||||||
{ label: formatMessage(labels.queries), url: '/queries', icon: <Icons.Search /> },
|
{ label: formatMessage(labels.queries), url: '/queries', icon: <Icons.Search /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const settings = [
|
const settings = [
|
||||||
{ label: formatMessage(labels.websites), url: '/settings/websites', icon: <Globe /> },
|
{ label: formatMessage(labels.websites), url: '/settings/websites', icon: <Icons.Globe /> },
|
||||||
{ label: formatMessage(labels.users), url: '/settings/users', icon: <User /> },
|
user?.isAdmin && {
|
||||||
{ label: formatMessage(labels.teams), url: '/settings/teams', icon: <Users /> },
|
label: formatMessage(labels.users),
|
||||||
{ label: formatMessage(labels.profile), url: '/settings/profile', icon: <Profile /> },
|
url: '/settings/users',
|
||||||
];
|
icon: <Icons.User />,
|
||||||
|
},
|
||||||
|
{ label: formatMessage(labels.teams), url: '/settings/teams', icon: <Icons.Users /> },
|
||||||
|
{ label: formatMessage(labels.profile), url: '/settings/profile', icon: <Icons.Profile /> },
|
||||||
|
].filter(n => n);
|
||||||
|
|
||||||
const handleMinimize = () => setMinimized(state => !state);
|
const handleMinimize = () => setMinimized(state => !state);
|
||||||
|
|
||||||
|
@ -34,7 +40,7 @@ export default function NavBar() {
|
||||||
<div className={classNames(styles.navbar, { [styles.minimized]: minimized })}>
|
<div className={classNames(styles.navbar, { [styles.minimized]: minimized })}>
|
||||||
<div className={styles.header} onClick={handleMinimize}>
|
<div className={styles.header} onClick={handleMinimize}>
|
||||||
<Icon size="lg">
|
<Icon size="lg">
|
||||||
<Logo />
|
<Icons.Logo />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Text className={styles.text}>umami</Text>
|
<Text className={styles.text}>umami</Text>
|
||||||
<Icon size="sm" rotate={minimized ? -90 : 90} className={styles.icon}>
|
<Icon size="sm" rotate={minimized ? -90 : 90} className={styles.icon}>
|
||||||
|
|
|
@ -1,24 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Button, Icon } from 'react-basics';
|
|
||||||
import styles from './PageHeader.module.css';
|
import styles from './PageHeader.module.css';
|
||||||
|
|
||||||
export default function PageHeader({ title, backUrl, children, className, style }) {
|
export default function PageHeader({ title, children, className, style }) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.header, className)} style={style}>
|
<div className={classNames(styles.header, className)} style={style}>
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>{title}</div>
|
||||||
{backUrl && (
|
|
||||||
<Link href={backUrl}>
|
|
||||||
<a>
|
|
||||||
<Button>
|
|
||||||
<Icon icon="arrow-left" /> Back
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,8 +5,6 @@
|
||||||
align-content: center;
|
align-content: center;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
height: 50px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,5 +19,7 @@
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ export const labels = defineMessages({
|
||||||
newPassword: { id: 'label.new-password', defaultMessage: 'New password' },
|
newPassword: { id: 'label.new-password', defaultMessage: 'New password' },
|
||||||
confirmPassword: { id: 'label.confirm-password', defaultMessage: 'Confirm password' },
|
confirmPassword: { id: 'label.confirm-password', defaultMessage: 'Confirm password' },
|
||||||
timezone: { id: 'label.timezone', defaultMessage: 'Timezone' },
|
timezone: { id: 'label.timezone', defaultMessage: 'Timezone' },
|
||||||
dateRange: { id: 'label.default-date-range', defaultMessage: 'Default date range' },
|
defaultDateRange: { id: 'label.default-date-range', defaultMessage: 'Default date range' },
|
||||||
language: { id: 'label.language', defaultMessage: 'Language' },
|
language: { id: 'label.language', defaultMessage: 'Language' },
|
||||||
theme: { id: 'label.theme', defaultMessage: 'Theme' },
|
theme: { id: 'label.theme', defaultMessage: 'Theme' },
|
||||||
profile: { id: 'label.profile', defaultMessage: 'Profile' },
|
profile: { id: 'label.profile', defaultMessage: 'Profile' },
|
||||||
|
@ -59,6 +59,8 @@ export const labels = defineMessages({
|
||||||
teams: { id: 'label.teams', defaultMessage: 'Teams' },
|
teams: { id: 'label.teams', defaultMessage: 'Teams' },
|
||||||
analytics: { id: 'label.analytics', defaultMessage: 'Analytics' },
|
analytics: { id: 'label.analytics', defaultMessage: 'Analytics' },
|
||||||
logout: { id: 'label.logout', defaultMessage: 'Logout' },
|
logout: { id: 'label.logout', defaultMessage: 'Logout' },
|
||||||
|
singleDay: { id: 'label.single-day', defaultMessage: 'Single day' },
|
||||||
|
dateRange: { id: 'label.date-range', defaultMessage: 'Date range' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import Calendar from 'components/common/Calendar';
|
|
||||||
import { FormButtons } from 'components/layout/FormLayout';
|
|
||||||
import { isAfter, isBefore, isSameDay } from 'date-fns';
|
|
||||||
import { getDateRangeValues } from 'lib/date';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, ButtonGroup } from 'react-basics';
|
import { Button, ButtonGroup, Calendar } from 'react-basics';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
import { isAfter, isBefore, isSameDay } from 'date-fns';
|
||||||
|
import useLocale from 'hooks/useLocale';
|
||||||
|
import { getDateRangeValues } from 'lib/date';
|
||||||
|
import { getDateLocale } from 'lib/lang';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
import styles from './DatePickerForm.module.css';
|
import styles from './DatePickerForm.module.css';
|
||||||
|
|
||||||
const FILTER_DAY = 0;
|
const FILTER_DAY = 'day';
|
||||||
const FILTER_RANGE = 1;
|
const FILTER_RANGE = 'range';
|
||||||
|
|
||||||
export default function DatePickerForm({
|
export default function DatePickerForm({
|
||||||
startDate: defaultStartDate,
|
startDate: defaultStartDate,
|
||||||
|
@ -24,59 +25,59 @@ export default function DatePickerForm({
|
||||||
const [date, setDate] = useState(defaultStartDate);
|
const [date, setDate] = useState(defaultStartDate);
|
||||||
const [startDate, setStartDate] = useState(defaultStartDate);
|
const [startDate, setStartDate] = useState(defaultStartDate);
|
||||||
const [endDate, setEndDate] = useState(defaultEndDate);
|
const [endDate, setEndDate] = useState(defaultEndDate);
|
||||||
|
const { locale } = useLocale();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const disabled =
|
const disabled =
|
||||||
selected === FILTER_DAY
|
selected === FILTER_DAY
|
||||||
? isAfter(minDate, date) && isBefore(maxDate, date)
|
? isAfter(minDate, date) && isBefore(maxDate, date)
|
||||||
: isAfter(startDate, endDate);
|
: isAfter(startDate, endDate);
|
||||||
|
|
||||||
const buttons = [
|
const handleSave = () => {
|
||||||
{
|
|
||||||
label: <FormattedMessage id="label.single-day" defaultMessage="Single day" />,
|
|
||||||
value: FILTER_DAY,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: <FormattedMessage id="label.date-range" defaultMessage="Date range" />,
|
|
||||||
value: FILTER_RANGE,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function handleSave() {
|
|
||||||
if (selected === FILTER_DAY) {
|
if (selected === FILTER_DAY) {
|
||||||
onChange({ ...getDateRangeValues(date, date), value: 'custom' });
|
onChange({ ...getDateRangeValues(date, date), value: 'custom' });
|
||||||
} else {
|
} else {
|
||||||
onChange({ ...getDateRangeValues(startDate, endDate), value: 'custom' });
|
onChange({ ...getDateRangeValues(startDate, endDate), value: 'custom' });
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.filter}>
|
<div className={styles.filter}>
|
||||||
<ButtonGroup size="small" items={buttons} selectedItem={selected} onClick={setSelected} />
|
<ButtonGroup size="sm" selectedKey={selected} onSelect={setSelected}>
|
||||||
|
<Button key={FILTER_DAY}>{formatMessage(labels.singleDay)}</Button>
|
||||||
|
<Button key={FILTER_RANGE}>{formatMessage(labels.dateRange)}</Button>
|
||||||
|
</ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.calendars}>
|
<div className={styles.calendars}>
|
||||||
{selected === FILTER_DAY ? (
|
{selected === FILTER_DAY && (
|
||||||
<Calendar date={date} minDate={minDate} maxDate={maxDate} onChange={setDate} />
|
<Calendar date={date} minDate={minDate} maxDate={maxDate} onChange={setDate} />
|
||||||
) : (
|
)}
|
||||||
|
{selected === FILTER_RANGE && (
|
||||||
<>
|
<>
|
||||||
<Calendar
|
<Calendar
|
||||||
date={startDate}
|
date={startDate}
|
||||||
minDate={minDate}
|
minDate={minDate}
|
||||||
maxDate={endDate}
|
maxDate={endDate}
|
||||||
|
locale={getDateLocale(locale)}
|
||||||
onChange={setStartDate}
|
onChange={setStartDate}
|
||||||
/>
|
/>
|
||||||
<Calendar date={endDate} minDate={startDate} maxDate={maxDate} onChange={setEndDate} />
|
<Calendar
|
||||||
|
date={endDate}
|
||||||
|
minDate={startDate}
|
||||||
|
maxDate={maxDate}
|
||||||
|
locale={getDateLocale(locale)}
|
||||||
|
onChange={setEndDate}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<FormButtons>
|
<div className={styles.buttons}>
|
||||||
<Button variant="action" onClick={handleSave} disabled={disabled}>
|
<Button variant="primary" onClick={handleSave} disabled={disabled}>
|
||||||
<FormattedMessage id="label.save" defaultMessage="Save" />
|
{formatMessage(labels.save)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onClose}>
|
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
</div>
|
||||||
</Button>
|
|
||||||
</FormButtons>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,14 @@
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
@media only screen and (max-width: 768px) {
|
||||||
.calendars {
|
.calendars {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Loading } from 'react-basics';
|
import { Loading, Icons } from 'react-basics';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import firstBy from 'thenby';
|
import firstBy from 'thenby';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Link from 'components/common/Link';
|
import Link from 'components/common/Link';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import Arrow from 'assets/arrow-right.svg';
|
|
||||||
import { percentFilter } from 'lib/filters';
|
import { percentFilter } from 'lib/filters';
|
||||||
import useDateRange from 'hooks/useDateRange';
|
import useDateRange from 'hooks/useDateRange';
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
|
@ -80,7 +79,7 @@ export default function MetricsTable({
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
{data && !error && limit && (
|
{data && !error && limit && (
|
||||||
<Link
|
<Link
|
||||||
icon={<Arrow />}
|
icon={<Icons.ArrowRight />}
|
||||||
href={router.pathname}
|
href={router.pathname}
|
||||||
as={resolve({ view: type })}
|
as={resolve({ view: type })}
|
||||||
size="small"
|
size="small"
|
||||||
|
|
|
@ -2,25 +2,11 @@ import { useMemo } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { differenceInMinutes } from 'date-fns';
|
import { differenceInMinutes } from 'date-fns';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import DropDown from 'components/common/DropDown';
|
|
||||||
import ActiveUsers from './ActiveUsers';
|
import ActiveUsers from './ActiveUsers';
|
||||||
import MetricCard from './MetricCard';
|
import MetricCard from './MetricCard';
|
||||||
import styles from './RealtimeHeader.module.css';
|
import styles from './RealtimeHeader.module.css';
|
||||||
|
|
||||||
export default function RealtimeHeader({ websites, data, websiteId, onSelect }) {
|
export default function RealtimeHeader({ data, websiteId }) {
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
label: <FormattedMessage id="label.all-websites" defaultMessage="All websites" />,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
].concat(
|
|
||||||
websites.map(({ name, id }, index) => ({
|
|
||||||
label: name,
|
|
||||||
value: id,
|
|
||||||
divider: index === 0,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { pageviews, sessions, events, countries } = data;
|
const { pageviews, sessions, events, countries } = data;
|
||||||
|
|
||||||
const count = useMemo(() => {
|
const count = useMemo(() => {
|
||||||
|
@ -38,7 +24,6 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect })
|
||||||
<div>
|
<div>
|
||||||
<ActiveUsers className={styles.active} value={count} />
|
<ActiveUsers className={styles.active} value={count} />
|
||||||
</div>
|
</div>
|
||||||
<DropDown value={websiteId} options={options} onChange={onSelect} />
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<div className={styles.metrics}>
|
<div className={styles.metrics}>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { Button, Column, Loading, Row } from 'react-basics';
|
import { Button, Column, Row, Dropdown, Item } from 'react-basics';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import DropDown from 'components/common/DropDown';
|
|
||||||
import Page from 'components/layout/Page';
|
import Page from 'components/layout/Page';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import EventsChart from 'components/metrics/EventsChart';
|
import EventsChart from 'components/metrics/EventsChart';
|
||||||
|
@ -12,29 +11,14 @@ import styles from './TestConsole.module.css';
|
||||||
|
|
||||||
export default function TestConsole() {
|
export default function TestConsole() {
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { data, isLoading } = useQuery(['websites:test-console'], () =>
|
const { data, isLoading, error } = useQuery(['websites:test-console'], () => get('/websites'));
|
||||||
get('/websites?include_all=true'),
|
|
||||||
);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const {
|
const {
|
||||||
basePath,
|
basePath,
|
||||||
query: { id },
|
query: { id },
|
||||||
} = router;
|
} = router;
|
||||||
const websiteId = id?.[0];
|
|
||||||
|
|
||||||
if (isLoading) {
|
function handleChange(value) {
|
||||||
return <Loading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = data.map(({ name, id }) => ({ label: name, value: id }));
|
|
||||||
const website = data.find(({ id }) => websiteId === id);
|
|
||||||
const selectedValue = options.find(({ value }) => value === website?.id)?.value;
|
|
||||||
|
|
||||||
function handleSelect(value) {
|
|
||||||
router.push(`/console/${value}`);
|
router.push(`/console/${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,8 +29,15 @@ export default function TestConsole() {
|
||||||
window.umami.trackEvent('track-event-with-data', { test: 'test-data', time: Date.now() });
|
window.umami.trackEvent('track-event-with-data', { test: 'test-data', time: Date.now() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const websiteId = id?.[0];
|
||||||
|
const website = data.find(({ id }) => websiteId === id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page loading={isLoading} error={error}>
|
||||||
<Head>
|
<Head>
|
||||||
{typeof window !== 'undefined' && website && (
|
{typeof window !== 'undefined' && website && (
|
||||||
<script
|
<script
|
||||||
|
@ -58,19 +49,22 @@ export default function TestConsole() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Head>
|
</Head>
|
||||||
<PageHeader>
|
<PageHeader title="Test console">
|
||||||
<div>Test Console</div>
|
<Dropdown
|
||||||
<DropDown
|
items={data}
|
||||||
value={selectedValue || 'Select website'}
|
renderValue={() => website?.name || 'Select website'}
|
||||||
options={options}
|
value={website?.id}
|
||||||
onChange={handleSelect}
|
onChange={handleChange}
|
||||||
/>
|
style={{ width: 300 }}
|
||||||
|
>
|
||||||
|
{({ id, name }) => <Item key={id}>{name}</Item>}
|
||||||
|
</Dropdown>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
{website && (
|
{website && (
|
||||||
<>
|
<>
|
||||||
<Row className={styles.test}>
|
<Row className={styles.test}>
|
||||||
<Column xs="4">
|
<Column xs="4">
|
||||||
<PageHeader>Page links</PageHeader>
|
<div className={styles.header}>Page links</div>
|
||||||
<div>
|
<div>
|
||||||
<Link href={`/console/${websiteId}?page=1`}>
|
<Link href={`/console/${websiteId}?page=1`}>
|
||||||
<a>page one</a>
|
<a>page one</a>
|
||||||
|
@ -95,13 +89,13 @@ export default function TestConsole() {
|
||||||
</div>
|
</div>
|
||||||
</Column>
|
</Column>
|
||||||
<Column xs="4">
|
<Column xs="4">
|
||||||
<PageHeader>CSS events</PageHeader>
|
<div className={styles.header}>CSS events</div>
|
||||||
<Button id="primary-button" className="umami--click--button-click" variant="action">
|
<Button id="primary-button" className="umami--click--button-click" variant="action">
|
||||||
Send event
|
Send event
|
||||||
</Button>
|
</Button>
|
||||||
</Column>
|
</Column>
|
||||||
<Column xs="4">
|
<Column xs="4">
|
||||||
<PageHeader>Javascript events</PageHeader>
|
<div className={styles.header}>Javascript events</div>
|
||||||
<Button id="manual-button" variant="action" onClick={handleClick}>
|
<Button id="manual-button" variant="action" onClick={handleClick}>
|
||||||
Run script
|
Run script
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -109,13 +103,14 @@ export default function TestConsole() {
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Column>
|
<Column>
|
||||||
|
<div className={styles.header}>Statistics</div>
|
||||||
<WebsiteChart
|
<WebsiteChart
|
||||||
websiteId={website.id}
|
websiteId={website.id}
|
||||||
title={website.name}
|
title={website.name}
|
||||||
domain={website.domain}
|
domain={website.domain}
|
||||||
showLink
|
showLink
|
||||||
/>
|
/>
|
||||||
<PageHeader>Events</PageHeader>
|
<div className={styles.header}>Events</div>
|
||||||
<EventsChart websiteId={website.id} />
|
<EventsChart websiteId={website.id} />
|
||||||
</Column>
|
</Column>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -4,6 +4,12 @@
|
||||||
padding: 0 20px 20px 20px;
|
padding: 0 20px 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { Button, Icon, Text, useToast, ModalTrigger, Modal } from 'react-basics';
|
import { Button, Icon, Text, useToast, ModalTrigger, Modal } from 'react-basics';
|
||||||
import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm';
|
import PasswordEditForm from 'components/pages/settings/profile/PasswordEditForm';
|
||||||
import { Lock } from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import { labels, messages } from 'components/messages';
|
import { labels, messages } from 'components/messages';
|
||||||
|
|
||||||
export default function PasswordChangeButton() {
|
export default function PasswordChangeButton() {
|
||||||
|
@ -18,7 +18,7 @@ export default function PasswordChangeButton() {
|
||||||
<ModalTrigger modalProps={{ title: formatMessage(labels.changePassword) }}>
|
<ModalTrigger modalProps={{ title: formatMessage(labels.changePassword) }}>
|
||||||
<Button>
|
<Button>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Lock />
|
<Icons.Lock />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Text>{formatMessage(labels.changePassword)}</Text>
|
<Text>{formatMessage(labels.changePassword)}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -21,15 +21,15 @@ export default function ProfileDetails() {
|
||||||
<Form>
|
<Form>
|
||||||
<FormRow label={formatMessage(labels.username)}>{username}</FormRow>
|
<FormRow label={formatMessage(labels.username)}>{username}</FormRow>
|
||||||
<FormRow label={formatMessage(labels.role)}>{role}</FormRow>
|
<FormRow label={formatMessage(labels.role)}>{role}</FormRow>
|
||||||
|
<FormRow label={formatMessage(labels.defaultDateRange)}>
|
||||||
|
<DateRangeSetting />
|
||||||
|
</FormRow>
|
||||||
<FormRow label={formatMessage(labels.language)}>
|
<FormRow label={formatMessage(labels.language)}>
|
||||||
<LanguageSetting />
|
<LanguageSetting />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(labels.timezone)}>
|
<FormRow label={formatMessage(labels.timezone)}>
|
||||||
<TimezoneSetting />
|
<TimezoneSetting />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(labels.dateRange)}>
|
|
||||||
<DateRangeSetting />
|
|
||||||
</FormRow>
|
|
||||||
<FormRow label={formatMessage(labels.theme)}>
|
<FormRow label={formatMessage(labels.theme)}>
|
||||||
<ThemeSetting />
|
<ThemeSetting />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default function TeamEditForm({ teamId, data, onSave }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data}>
|
<Form ref={ref} onSubmit={handleSubmit} error={error} values={data} style={{ width: 600 }}>
|
||||||
<FormRow label={formatMessage(labels.teamId)}>
|
<FormRow label={formatMessage(labels.teamId)}>
|
||||||
<TextField value={teamId} readOnly allowCopy />
|
<TextField value={teamId} readOnly allowCopy />
|
||||||
</FormRow>
|
</FormRow>
|
||||||
|
|
|
@ -14,14 +14,16 @@ import {
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { ROLES } from 'lib/constants';
|
import { ROLES } from 'lib/constants';
|
||||||
import { labels } from 'components/messages';
|
import { labels } from 'components/messages';
|
||||||
|
import useUser from 'hooks/useUser';
|
||||||
|
|
||||||
export default function TeamMembersTable({ data = [] }) {
|
export default function TeamMembersTable({ data = [] }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'username', label: formatMessage(labels.username), style: { flex: 4 } },
|
{ name: 'username', label: formatMessage(labels.username), style: { flex: 2 } },
|
||||||
{ name: 'role', label: formatMessage(labels.role) },
|
{ name: 'role', label: formatMessage(labels.role), style: { flex: 1 } },
|
||||||
{ name: 'action', label: '' },
|
{ name: 'action', label: '', style: { flex: 1 } },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -43,14 +45,14 @@ export default function TeamMembersTable({ data = [] }) {
|
||||||
labels[Object.keys(ROLES).find(key => ROLES[key] === row.role) || labels.unknown],
|
labels[Object.keys(ROLES).find(key => ROLES[key] === row.role) || labels.unknown],
|
||||||
),
|
),
|
||||||
action: (
|
action: (
|
||||||
<div>
|
<Flexbox flex={1} justifyContent="end">
|
||||||
<Button>
|
<Button disabled={user.id === row?.user?.id}>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Icons.Close />
|
<Icons.Close />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Text>{formatMessage(labels.remove)}</Text>
|
<Text>{formatMessage(labels.remove)}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Flexbox>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,11 +61,7 @@ export default function TeamMembersTable({ data = [] }) {
|
||||||
{(data, key, colIndex) => {
|
{(data, key, colIndex) => {
|
||||||
return (
|
return (
|
||||||
<TableCell key={colIndex} style={{ ...columns[colIndex]?.style }}>
|
<TableCell key={colIndex} style={{ ...columns[colIndex]?.style }}>
|
||||||
<Flexbox
|
<Flexbox flex={1} alignItems="center">
|
||||||
flex={1}
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent={key === 'action' ? 'end' : undefined}
|
|
||||||
>
|
|
||||||
{data[key]}
|
{data[key]}
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
|
@ -7,23 +7,20 @@ import {
|
||||||
TableCell,
|
TableCell,
|
||||||
TableColumn,
|
TableColumn,
|
||||||
Button,
|
Button,
|
||||||
|
Text,
|
||||||
Icon,
|
Icon,
|
||||||
Icons,
|
Icons,
|
||||||
Flexbox,
|
Flexbox,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
import { labels } from 'components/messages';
|
||||||
const messages = defineMessages({
|
|
||||||
name: { id: 'label.name', defaultMessage: 'Name' },
|
|
||||||
domain: { id: 'label.domain', defaultMessage: 'Domain' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function WebsitesTable({ data = [] }) {
|
export default function WebsitesTable({ data = [] }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'name', label: formatMessage(messages.name), style: { flex: 2 } },
|
{ name: 'name', label: formatMessage(labels.name), style: { flex: 2 } },
|
||||||
{ name: 'domain', label: formatMessage(messages.domain) },
|
{ name: 'domain', label: formatMessage(labels.domain) },
|
||||||
{ name: 'action', label: ' ' },
|
{ name: 'action', label: ' ' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -50,7 +47,7 @@ export default function WebsitesTable({ data = [] }) {
|
||||||
<Icon>
|
<Icon>
|
||||||
<Icons.ArrowRight />
|
<Icons.ArrowRight />
|
||||||
</Icon>
|
</Icon>
|
||||||
Settings
|
<Text>Settings</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -60,7 +57,7 @@ export default function WebsitesTable({ data = [] }) {
|
||||||
<Icon>
|
<Icon>
|
||||||
<Icons.External />
|
<Icons.External />
|
||||||
</Icon>
|
</Icon>
|
||||||
View
|
<Text>View</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Arrow from 'assets/arrow-right.svg';
|
import { Icons } from 'react-basics';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Link from 'components/common/Link';
|
import Link from 'components/common/Link';
|
||||||
import WorldMap from 'components/common/WorldMap';
|
import WorldMap from 'components/common/WorldMap';
|
||||||
|
@ -67,7 +67,12 @@ export default function WebsiteDetails({ websiteId }) {
|
||||||
|
|
||||||
const BackButton = () => (
|
const BackButton = () => (
|
||||||
<div key="back-button" className={classNames(styles.backButton, 'col-12')}>
|
<div key="back-button" className={classNames(styles.backButton, 'col-12')}>
|
||||||
<Link key="back-button" href={resolve({ view: undefined })} icon={<Arrow />} sizes="small">
|
<Link
|
||||||
|
key="back-button"
|
||||||
|
href={resolve({ view: undefined })}
|
||||||
|
icon={<Icons.ArrowRight />}
|
||||||
|
sizes="small"
|
||||||
|
>
|
||||||
<FormattedMessage id="label.back" defaultMessage="Back" />
|
<FormattedMessage id="label.back" defaultMessage="Back" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,10 +11,12 @@ import { getUser } from '../queries';
|
||||||
|
|
||||||
const log = debug('umami:middleware');
|
const log = debug('umami:middleware');
|
||||||
|
|
||||||
export const useCors = createMiddleware(cors({
|
export const useCors = createMiddleware(
|
||||||
|
cors({
|
||||||
// Cache CORS preflight request 24 hours by default
|
// Cache CORS preflight request 24 hours by default
|
||||||
maxAge: process.env.CORS_MAX_AGE || 86400,
|
maxAge: process.env.CORS_MAX_AGE || 86400,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export const useSession = createMiddleware(async (req, res, next) => {
|
export const useSession = createMiddleware(async (req, res, next) => {
|
||||||
const session = await findSession(req);
|
const session = await findSession(req);
|
||||||
|
@ -34,17 +36,17 @@ export const useAuth = createMiddleware(async (req, res, next) => {
|
||||||
const shareToken = await parseShareToken(req);
|
const shareToken = await parseShareToken(req);
|
||||||
|
|
||||||
let user = null;
|
let user = null;
|
||||||
const { userId, key } = payload || {};
|
const { userId, authKey } = payload || {};
|
||||||
|
|
||||||
if (validate(userId)) {
|
if (validate(userId)) {
|
||||||
user = await getUser({ id: userId });
|
user = await getUser({ id: userId });
|
||||||
} else if (redis.enabled && key) {
|
} else if (redis.enabled && authKey) {
|
||||||
user = await redis.get(key);
|
user = await redis.get(authKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
log({ token, payload, user, shareToken });
|
log({ token, payload, user, shareToken });
|
||||||
|
|
||||||
if (!user && !shareToken) {
|
if (!user?.id && !shareToken) {
|
||||||
log('useAuth: User not authorized');
|
log('useAuth: User not authorized');
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
@ -53,6 +55,6 @@ export const useAuth = createMiddleware(async (req, res, next) => {
|
||||||
user.isAdmin = user.role === ROLES.admin;
|
user.isAdmin = user.role === ROLES.admin;
|
||||||
}
|
}
|
||||||
|
|
||||||
(req as any).auth = { user, token, shareToken, key };
|
(req as any).auth = { user, token, shareToken, authKey };
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "2.0.0-beta.2",
|
"version": "2.0.0-beta.3",
|
||||||
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
||||||
"author": "Mike Cao <mike@mikecao.com>",
|
"author": "Mike Cao <mike@mikecao.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -94,7 +94,7 @@
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-basics": "^0.63.0",
|
"react-basics": "^0.64.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-intl": "^5.24.7",
|
"react-intl": "^5.24.7",
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
import AppLayout from 'components/layout/AppLayout';
|
import AppLayout from 'components/layout/AppLayout';
|
||||||
import TestConsole from 'components/pages/console/TestConsole';
|
import TestConsole from 'components/pages/console/TestConsole';
|
||||||
import useUser from 'hooks/useUser';
|
|
||||||
|
|
||||||
export default function ConsolePage({ pageDisabled }) {
|
export default function ConsolePage({ pageDisabled }) {
|
||||||
const { user } = useUser();
|
if (pageDisabled) {
|
||||||
|
|
||||||
if (pageDisabled || !user || !user.isAdmin) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import useUser from 'hooks/useUser';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
export default function TeamDetailPage() {
|
export default function TeamDetailPage() {
|
||||||
const user = useUser();
|
const { user } = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import TeamsList from 'components/pages/settings/teams/TeamsList';
|
||||||
import useUser from 'hooks/useUser';
|
import useUser from 'hooks/useUser';
|
||||||
|
|
||||||
export default function TeamsPage() {
|
export default function TeamsPage() {
|
||||||
const user = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import useUser from 'hooks/useUser';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
export default function TeamDetailPage() {
|
export default function TeamDetailPage() {
|
||||||
const user = useUser();
|
const { user } = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import useUser from 'hooks/useUser';
|
||||||
import UsersList from 'components/pages/settings/users/UsersList';
|
import UsersList from 'components/pages/settings/users/UsersList';
|
||||||
|
|
||||||
export default function UsersPage() {
|
export default function UsersPage() {
|
||||||
const user = useUser();
|
const { user } = useUser();
|
||||||
const { adminDisabled } = useConfig();
|
const { adminDisabled } = useConfig();
|
||||||
|
|
||||||
if (adminDisabled || !user) {
|
if (adminDisabled || !user) {
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import WebsiteSettings from 'components/pages/settings/websites/WebsiteSettings';
|
import WebsiteSettings from 'components/pages/settings/websites/WebsiteSettings';
|
||||||
import useUser from 'hooks/useUser';
|
|
||||||
import AppLayout from 'components/layout/AppLayout';
|
import AppLayout from 'components/layout/AppLayout';
|
||||||
|
|
||||||
export default function WebsiteSettingsPage() {
|
export default function WebsiteSettingsPage() {
|
||||||
const user = useUser();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
|
||||||
if (!id || !user) {
|
if (!id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { Loading } from 'react-basics';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { setClientAuthToken } from 'lib/client';
|
import { setClientAuthToken } from 'lib/client';
|
||||||
|
|
||||||
|
@ -14,5 +15,5 @@ export default function SingleSignOnPage() {
|
||||||
}
|
}
|
||||||
}, [router, url, token]);
|
}, [router, url, token]);
|
||||||
|
|
||||||
return null;
|
return <Loading size="xl" />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6586,10 +6586,10 @@ rc@^1.2.7:
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
react-basics@^0.63.0:
|
react-basics@^0.64.0:
|
||||||
version "0.63.0"
|
version "0.64.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.63.0.tgz#1b203c701f6936076633994ed8175234bde93694"
|
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.64.0.tgz#b921dab7e437db6655f033cae15b8c963b93b7b2"
|
||||||
integrity sha512-G6+1Z921kC/TyjZCABrDlNeB22YkN6q7V70xREGSiO55OXU73MLT+Kk96GDWYfKa5lB1zI5rCbbvGz3ELuU+mA==
|
integrity sha512-MY/F5+VBqqi+Hx58PdRONoeu3W0sitPOFbvAGxiM9vpajQL1DD//0Xgl/MahW8sIDbMy00lFghouex5JS93C8Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
classnames "^2.3.1"
|
classnames "^2.3.1"
|
||||||
date-fns "^2.29.3"
|
date-fns "^2.29.3"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user