commit
d4f4ae9437
|
@ -1,10 +1,11 @@
|
|||
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';
|
||||
|
||||
export default function Button({
|
||||
function Button({
|
||||
type = 'button',
|
||||
icon,
|
||||
size,
|
||||
|
@ -43,3 +44,19 @@ export default function Button({
|
|||
</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;
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Button from './Button';
|
||||
import styles from './ButtonGroup.module.css';
|
||||
|
||||
export default function ButtonGroup({
|
||||
items = [],
|
||||
selectedItem,
|
||||
className,
|
||||
size,
|
||||
icon,
|
||||
onClick = () => {},
|
||||
}) {
|
||||
function ButtonGroup({ items = [], selectedItem, className, size, icon, onClick = () => {} }) {
|
||||
return (
|
||||
<div className={classNames(styles.group, className)}>
|
||||
{items.map(item => {
|
||||
|
@ -30,3 +24,19 @@ export default function ButtonGroup({
|
|||
</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;
|
||||
|
|
|
@ -18,8 +18,9 @@ import {
|
|||
} from 'date-fns';
|
||||
import Button from './Button';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { dateFormat } from 'lib/lang';
|
||||
import { dateFormat } from 'lib/date';
|
||||
import { chunk } from 'lib/array';
|
||||
import { dateLocales } from 'lib/lang';
|
||||
import Chevron from 'assets/chevron-down.svg';
|
||||
import Cross from 'assets/times.svg';
|
||||
import styles from './Calendar.module.css';
|
||||
|
@ -105,8 +106,8 @@ export default function Calendar({ date, minDate, maxDate, onChange }) {
|
|||
}
|
||||
|
||||
const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => {
|
||||
const startWeek = startOfWeek(date);
|
||||
const startMonth = startOfMonth(date);
|
||||
const startWeek = startOfWeek(date, { locale: dateLocales[locale] });
|
||||
const startMonth = startOfMonth(date, { locale: dateLocales[locale] });
|
||||
const startDay = subDays(startMonth, startMonth.getDay());
|
||||
const month = date.getMonth();
|
||||
const year = date.getFullYear();
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
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';
|
||||
|
||||
export default function Checkbox({ name, value, label, onChange }) {
|
||||
function Checkbox({ name, value, label, onChange }) {
|
||||
const ref = useRef();
|
||||
|
||||
return (
|
||||
|
@ -25,3 +26,12 @@ export default function Checkbox({ name, value, label, onChange }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Checkbox.propTypes = {
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
label: PropTypes.node,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Checkbox;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from './Button';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
|
@ -6,7 +7,7 @@ const defaultText = (
|
|||
<FormattedMessage id="label.copy-to-clipboard" defaultMessage="Copy to clipboard" />
|
||||
);
|
||||
|
||||
export default function CopyButton({ element, ...props }) {
|
||||
function CopyButton({ element, ...props }) {
|
||||
const [text, setText] = useState(defaultText);
|
||||
|
||||
function handleClick() {
|
||||
|
@ -24,3 +25,13 @@ export default function CopyButton({ element, ...props }) {
|
|||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
CopyButton.propTypes = {
|
||||
element: PropTypes.shape({
|
||||
current: PropTypes.shape({
|
||||
select: PropTypes.func.isRequired,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
export default CopyButton;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
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 DatePickerForm from 'components/forms/DatePickerForm';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { getDateRange } from 'lib/date';
|
||||
import { dateFormat } from 'lib/lang';
|
||||
import { getDateRange, dateFormat } from 'lib/date';
|
||||
import Calendar from 'assets/calendar-alt.svg';
|
||||
import Icon from './Icon';
|
||||
|
||||
|
@ -54,7 +54,8 @@ const filterOptions = [
|
|||
},
|
||||
];
|
||||
|
||||
export default function DateFilter({ value, startDate, endDate, onChange, className }) {
|
||||
function DateFilter({ value, startDate, endDate, onChange, className }) {
|
||||
const [locale] = useLocale();
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const displayValue =
|
||||
value === 'custom' ? (
|
||||
|
@ -68,7 +69,7 @@ export default function DateFilter({ value, startDate, endDate, onChange, classN
|
|||
setShowPicker(true);
|
||||
return;
|
||||
}
|
||||
onChange(getDateRange(value));
|
||||
onChange(getDateRange(value, locale));
|
||||
}
|
||||
|
||||
function handlePickerChange(value) {
|
||||
|
@ -117,3 +118,13 @@ const CustomRange = ({ startDate, endDate, onClick }) => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DateFilter.propTypes = {
|
||||
value: PropTypes.string,
|
||||
startDate: PropTypes.instanceOf(Date),
|
||||
endDate: PropTypes.instanceOf(Date),
|
||||
onChange: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default DateFilter;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Dot.module.css';
|
||||
|
||||
export default function Dot({ color, size, className }) {
|
||||
function Dot({ color, size, className }) {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div
|
||||
|
@ -15,3 +16,11 @@ export default function Dot({ color, size, className }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Dot.propTypes = {
|
||||
color: PropTypes.string,
|
||||
size: PropTypes.oneOf(['small', 'large']),
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Dot;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Menu from './Menu';
|
||||
import useDocumentClick from 'hooks/useDocumentClick';
|
||||
|
@ -6,13 +7,7 @@ import Chevron from 'assets/chevron-down.svg';
|
|||
import styles from './Dropdown.module.css';
|
||||
import Icon from './Icon';
|
||||
|
||||
export default function DropDown({
|
||||
value,
|
||||
className,
|
||||
menuClassName,
|
||||
options = [],
|
||||
onChange = () => {},
|
||||
}) {
|
||||
function DropDown({ value, className, menuClassName, options = [], onChange = () => {} }) {
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const ref = useRef();
|
||||
const selectedOption = options.find(e => e.value === value);
|
||||
|
@ -52,3 +47,18 @@ export default function DropDown({
|
|||
</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,14 +1,22 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from 'components/common/Icon';
|
||||
import Logo from 'assets/logo.svg';
|
||||
import styles from './EmptyPlaceholder.module.css';
|
||||
|
||||
export default function EmptyPlaceholder({ msg, children }) {
|
||||
function EmptyPlaceholder({ msg, children }) {
|
||||
return (
|
||||
<div className={styles.placeholder}>
|
||||
<Icon className={styles.icon} icon={<Logo />} size="xlarge" />
|
||||
<h2>{msg}</h2>
|
||||
<h2 className={styles.msg}>{msg}</h2>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EmptyPlaceholder.propTypes = {
|
||||
msg: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default EmptyPlaceholder;
|
||||
|
|
|
@ -9,3 +9,7 @@
|
|||
.icon {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.msg {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Favicon.module.css';
|
||||
|
||||
function getHostName(url) {
|
||||
|
@ -6,7 +7,7 @@ function getHostName(url) {
|
|||
return match && match.length > 1 ? match[1] : null;
|
||||
}
|
||||
|
||||
export default function Favicon({ domain, ...props }) {
|
||||
function Favicon({ domain, ...props }) {
|
||||
const hostName = domain ? getHostName(domain) : null;
|
||||
|
||||
return hostName ? (
|
||||
|
@ -19,3 +20,9 @@ export default function Favicon({ domain, ...props }) {
|
|||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
Favicon.propTypes = {
|
||||
domain: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Favicon;
|
||||
|
|
|
@ -1,11 +1,25 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import ButtonGroup from './ButtonGroup';
|
||||
|
||||
export default function FilterButtons({ buttons, selected, onClick }) {
|
||||
function FilterButtons({ buttons, selected, onClick }) {
|
||||
return (
|
||||
<ButtonLayout>
|
||||
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
||||
</ButtonLayout>
|
||||
);
|
||||
}
|
||||
|
||||
FilterButtons.propTypes = {
|
||||
buttons: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any.isRequired,
|
||||
}),
|
||||
),
|
||||
selected: PropTypes.any,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default FilterButtons;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Icon.module.css';
|
||||
|
||||
export default function Icon({ icon, className, size = 'medium', ...props }) {
|
||||
function Icon({ icon, className, size = 'medium', ...props }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.icon, className, {
|
||||
|
@ -18,3 +19,11 @@ export default function Icon({ icon, className, size = 'medium', ...props }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Icon.propTypes = {
|
||||
className: PropTypes.string,
|
||||
icon: PropTypes.node.isRequired,
|
||||
size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import NextLink from 'next/link';
|
||||
import Icon from './Icon';
|
||||
import styles from './Link.module.css';
|
||||
|
||||
export default function Link({ className, icon, children, size, iconRight, ...props }) {
|
||||
function Link({ className, icon, children, size, iconRight, ...props }) {
|
||||
return (
|
||||
<NextLink {...props}>
|
||||
<a
|
||||
|
@ -21,3 +22,13 @@ export default function Link({ className, icon, children, size, iconRight, ...pr
|
|||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
||||
Link.propTypes = {
|
||||
className: PropTypes.string,
|
||||
icon: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
size: PropTypes.oneOf(['large', 'small', 'xsmall']),
|
||||
iconRight: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Link;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Loading.module.css';
|
||||
|
||||
export default function Loading({ className }) {
|
||||
function Loading({ className }) {
|
||||
return (
|
||||
<div className={classNames(styles.loading, className)}>
|
||||
<div />
|
||||
|
@ -11,3 +12,9 @@ export default function Loading({ className }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Menu.module.css';
|
||||
|
||||
export default function Menu({
|
||||
function Menu({
|
||||
options = [],
|
||||
selectedOption,
|
||||
className,
|
||||
|
@ -46,3 +47,24 @@ export default function Menu({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Menu.propTypes = {
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
render: PropTypes.func,
|
||||
divider: PropTypes.bool,
|
||||
}),
|
||||
),
|
||||
selectedOption: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
float: PropTypes.oneOf(['top', 'bottom']),
|
||||
align: PropTypes.oneOf(['left', 'right']),
|
||||
optionClassName: PropTypes.string,
|
||||
selectedClassName: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
|
|
|
@ -1,11 +1,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';
|
||||
|
||||
export default function MenuButton({
|
||||
function MenuButton({
|
||||
icon,
|
||||
value,
|
||||
options,
|
||||
|
@ -58,3 +59,25 @@ export default function MenuButton({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MenuButton.propTypes = {
|
||||
icon: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
render: PropTypes.func,
|
||||
divider: PropTypes.bool,
|
||||
}),
|
||||
),
|
||||
buttonClassName: PropTypes.string,
|
||||
menuClassName: PropTypes.string,
|
||||
menuPosition: PropTypes.oneOf(['top', 'bottom']),
|
||||
menuAlign: PropTypes.oneOf(['left', 'right']),
|
||||
onSelect: PropTypes.func,
|
||||
renderValue: PropTypes.func,
|
||||
};
|
||||
|
||||
export default MenuButton;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
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';
|
||||
|
||||
export default function Modal({ title, children }) {
|
||||
function Modal({ title, children }) {
|
||||
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
|
@ -16,3 +17,10 @@ export default function Modal({ title, children }) {
|
|||
document.getElementById('__modals'),
|
||||
);
|
||||
}
|
||||
|
||||
Modal.propTypes = {
|
||||
title: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useRouter } from 'next/router';
|
||||
import classNames from 'classnames';
|
||||
import styles from './NavMenu.module.css';
|
||||
|
||||
export default function NavMenu({ options = [], className, onSelect = () => {} }) {
|
||||
function NavMenu({ options = [], className, onSelect = () => {} }) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
|
@ -30,3 +31,17 @@ export default function NavMenu({ options = [], className, onSelect = () => {} }
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NavMenu.propTypes = {
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
render: PropTypes.func,
|
||||
}),
|
||||
),
|
||||
className: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
export default NavMenu;
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import styles from './NoData.module.css';
|
||||
|
||||
export default function NoData({ className }) {
|
||||
function NoData({ className }) {
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<FormattedMessage id="message.no-data-available" defaultMessage="No data available." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NoData.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default NoData;
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
.container {
|
||||
color: var(--gray500);
|
||||
font-size: var(--font-size-normal);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { setDateRange } from 'redux/actions/websites';
|
||||
|
@ -7,9 +8,11 @@ import Refresh from 'assets/redo.svg';
|
|||
import Dots from 'assets/ellipsis-h.svg';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import { getDateRange } from '../../lib/date';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export default function RefreshButton({ websiteId }) {
|
||||
function RefreshButton({ websiteId }) {
|
||||
const dispatch = useDispatch();
|
||||
const [locale] = useLocale();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const completed = useSelector(state => state.queries[`/api/website/${websiteId}/stats`]);
|
||||
|
@ -17,7 +20,7 @@ export default function RefreshButton({ websiteId }) {
|
|||
function handleClick() {
|
||||
if (dateRange) {
|
||||
setLoading(true);
|
||||
dispatch(setDateRange(websiteId, getDateRange(dateRange.value)));
|
||||
dispatch(setDateRange(websiteId, getDateRange(dateRange.value, locale)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,3 +38,9 @@ export default function RefreshButton({ websiteId }) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
RefreshButton.propTypes = {
|
||||
websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
export default RefreshButton;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
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';
|
||||
|
||||
export default function Table({
|
||||
function Table({
|
||||
columns,
|
||||
rows,
|
||||
empty,
|
||||
|
@ -45,6 +46,34 @@ export default function Table({
|
|||
);
|
||||
}
|
||||
|
||||
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, render, className, style, cell }, index) => (
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Tag.module.css';
|
||||
|
||||
export default function Tag({ className, children }) {
|
||||
function Tag({ className, children }) {
|
||||
return <span className={classNames(styles.tag, className)}>{children}</span>;
|
||||
}
|
||||
|
||||
Tag.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Tag;
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import styles from './Toast.module.css';
|
||||
import Icon from 'components/common/Icon';
|
||||
import Close from 'assets/times.svg';
|
||||
|
||||
export default function Toast({ message, timeout = 3000, onClose }) {
|
||||
function Toast({ message, timeout = 3000, onClose }) {
|
||||
const props = useSpring({
|
||||
opacity: 1,
|
||||
transform: 'translate3d(0,0px,0)',
|
||||
|
@ -24,3 +25,11 @@ export default function Toast({ message, timeout = 3000, onClose }) {
|
|||
document.getElementById('__modals'),
|
||||
);
|
||||
}
|
||||
|
||||
Toast.propTypes = {
|
||||
message: PropTypes.node,
|
||||
timeout: PropTypes.number,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Toast;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
||||
import classNames from 'classnames';
|
||||
|
@ -12,7 +13,7 @@ import { useRouter } from 'next/router';
|
|||
|
||||
const geoUrl = '/world-110m.json';
|
||||
|
||||
export default function WorldMap({ data, className }) {
|
||||
function WorldMap({ data, className }) {
|
||||
const { basePath } = useRouter();
|
||||
const [tooltip, setTooltip] = useState();
|
||||
const [theme] = useTheme();
|
||||
|
@ -89,3 +90,16 @@ export default function WorldMap({ data, className }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
WorldMap.propTypes = {
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
x: PropTypes.string,
|
||||
y: PropTypes.number,
|
||||
z: PropTypes.number,
|
||||
}),
|
||||
),
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default WorldMap;
|
||||
|
|
|
@ -11,14 +11,22 @@
|
|||
|
||||
.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 {
|
||||
|
|
|
@ -35,6 +35,6 @@
|
|||
.row > .col {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-left: 0;
|
||||
padding: 0;
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,4 +44,7 @@
|
|||
.header {
|
||||
padding: 0 15px;
|
||||
}
|
||||
.nav {
|
||||
font-size: var(--font-size-normal);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import React from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import Header from 'components/layout/Header';
|
||||
import Footer from 'components/layout/Footer';
|
||||
|
||||
export default function Layout({ title, children, header = true, footer = true }) {
|
||||
const { basePath } = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>umami{title && ` - ${title}`}</title>
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
<link rel="icon" href={`${basePath}/favicon.ico`} />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap"
|
||||
rel="stylesheet"
|
||||
|
|
|
@ -3,7 +3,7 @@ import classNames from 'classnames';
|
|||
import ChartJS from 'chart.js';
|
||||
import Legend from 'components/metrics/Legend';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { dateFormat } from 'lib/lang';
|
||||
import { dateFormat } from 'lib/date';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { DEFAUL_CHART_HEIGHT, DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
|
||||
|
@ -44,9 +44,9 @@ export default function BarChart({
|
|||
|
||||
switch (unit) {
|
||||
case 'minute':
|
||||
return index % 2 === 0 ? dateFormat(d, 'h:mm', locale) : '';
|
||||
return index % 2 === 0 ? dateFormat(d, 'H:mm', locale) : '';
|
||||
case 'hour':
|
||||
return dateFormat(d, 'ha', locale);
|
||||
return dateFormat(d, 'p', locale);
|
||||
case 'day':
|
||||
if (records > 31) {
|
||||
if (w <= 500) {
|
||||
|
@ -93,9 +93,9 @@ export default function BarChart({
|
|||
function getTooltipFormat(unit) {
|
||||
switch (unit) {
|
||||
case 'hour':
|
||||
return 'EEE ha — MMM d yyyy';
|
||||
return 'EEE p — PPP';
|
||||
default:
|
||||
return 'EEE MMMM d yyyy';
|
||||
return 'PPPP';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,6 +131,7 @@ export default function BarChart({
|
|||
minRotation: 0,
|
||||
maxRotation: 0,
|
||||
fontColor: colors.text,
|
||||
autoSkipPadding: 1,
|
||||
},
|
||||
gridLines: {
|
||||
display: false,
|
||||
|
|
|
@ -10,7 +10,11 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
|
|||
const countryNames = useCountryNames(locale);
|
||||
|
||||
function renderLabel({ x }) {
|
||||
return <div className={locale}>{countryNames[x]}</div>;
|
||||
return (
|
||||
<div className={locale}>
|
||||
{countryNames[x] ?? <FormattedMessage id="label.unknown" defaultMessage="Unknown" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||
import { FixedSizeList } from 'react-window';
|
||||
import { useSpring, animated, config } from 'react-spring';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import NoData from 'components/common/NoData';
|
||||
import { formatNumber, formatLongNumber } from 'lib/format';
|
||||
import styles from './DataTable.module.css';
|
||||
|
@ -27,7 +28,11 @@ export default function DataTable({
|
|||
return (
|
||||
<AnimatedRow
|
||||
key={label}
|
||||
label={renderLabel ? renderLabel(row) : label}
|
||||
label={
|
||||
renderLabel
|
||||
? renderLabel(row)
|
||||
: label ?? <FormattedMessage id="label.unknown" defaultMessage="Unknown" />
|
||||
}
|
||||
value={value}
|
||||
percent={percent}
|
||||
animate={animate && !virtualize}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
.table {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
font-size: var(--font-size-small);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-rows: fit-content(100%) auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import { deviceFilter } from 'lib/filters';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { getDeviceMessage } from 'components/messages';
|
||||
|
||||
|
@ -12,7 +11,6 @@ export default function DevicesTable({ websiteId, ...props }) {
|
|||
type="device"
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
websiteId={websiteId}
|
||||
dataFilter={deviceFilter}
|
||||
renderLabel={({ x }) => getDeviceMessage(x)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,18 +1,46 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import Tag from 'components/common/Tag';
|
||||
import DropDown from 'components/common/DropDown';
|
||||
import { eventTypeFilter } from 'lib/filters';
|
||||
import styles from './EventsTable.module.css';
|
||||
|
||||
const EVENT_FILTER_DEFAULT = {
|
||||
value: 'EVENT_FILTER_DEFAULT',
|
||||
label: <FormattedMessage id="label.all-events" defaultMessage="All events" />,
|
||||
};
|
||||
|
||||
export default function EventsTable({ websiteId, ...props }) {
|
||||
const [eventType, setEventType] = useState(EVENT_FILTER_DEFAULT.value);
|
||||
const [eventTypes, setEventTypes] = useState([]);
|
||||
|
||||
const dropDownOptions = [EVENT_FILTER_DEFAULT, ...eventTypes.map(t => ({ value: t, label: t }))];
|
||||
|
||||
function handleDataLoad(data) {
|
||||
setEventTypes([...new Set(data.map(({ x }) => x.split('\t')[0]))]);
|
||||
props.onDataLoad?.(data);
|
||||
}
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
|
||||
type="event"
|
||||
metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />}
|
||||
websiteId={websiteId}
|
||||
renderLabel={({ x }) => <Label value={x} />}
|
||||
/>
|
||||
<>
|
||||
{eventTypes?.length > 1 && (
|
||||
<div className={styles.filter}>
|
||||
<DropDown value={eventType} options={dropDownOptions} onChange={setEventType} />
|
||||
</div>
|
||||
)}
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
|
||||
type="event"
|
||||
metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />}
|
||||
websiteId={websiteId}
|
||||
dataFilter={eventTypeFilter}
|
||||
filterOptions={eventType === EVENT_FILTER_DEFAULT.value ? [] : [eventType]}
|
||||
renderLabel={({ x }) => <Label value={x} />}
|
||||
onDataLoad={handleDataLoad}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
.filter {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
|
@ -11,8 +11,6 @@
|
|||
@media only screen and (max-width: 992px) {
|
||||
.bar {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.bar > div:nth-child(n + 3) {
|
||||
display: none;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ import styles from './MetricsTable.module.css';
|
|||
|
||||
export default function MetricsTable({
|
||||
websiteId,
|
||||
websiteDomain,
|
||||
type,
|
||||
className,
|
||||
dataFilter,
|
||||
|
@ -42,7 +41,6 @@ export default function MetricsTable({
|
|||
type,
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
domain: websiteDomain,
|
||||
url,
|
||||
},
|
||||
onDataLoad,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
.container {
|
||||
position: relative;
|
||||
min-height: 430px;
|
||||
height: 100%;
|
||||
font-size: var(--font-size-small);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import { osFilter } from 'lib/filters';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default function OSTable({ websiteId, ...props }) {
|
||||
|
@ -11,7 +10,6 @@ export default function OSTable({ websiteId, ...props }) {
|
|||
type="os"
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
websiteId={websiteId}
|
||||
dataFilter={osFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@ import React, { useMemo, useState } from 'react';
|
|||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import firstBy from 'thenby';
|
||||
import { format } from 'date-fns';
|
||||
import Icon from 'components/common/Icon';
|
||||
import Tag from 'components/common/Tag';
|
||||
import Dot from 'components/common/Dot';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
import NoData from 'components/common/NoData';
|
||||
import { devices } from 'components/messages';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
|
@ -15,8 +15,8 @@ import Bolt from 'assets/bolt.svg';
|
|||
import Visitor from 'assets/visitor.svg';
|
||||
import Eye from 'assets/eye.svg';
|
||||
import { stringToColor } from 'lib/format';
|
||||
import { dateFormat } from 'lib/date';
|
||||
import styles from './RealtimeLog.module.css';
|
||||
import NoData from '../common/NoData';
|
||||
|
||||
const TYPE_ALL = 0;
|
||||
const TYPE_PAGEVIEW = 1;
|
||||
|
@ -129,7 +129,12 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
|||
id="message.log.visitor"
|
||||
defaultMessage="Visitor from {country} using {browser} on {os} {device}"
|
||||
values={{
|
||||
country: <b>{countryNames[country]}</b>,
|
||||
country: (
|
||||
<b>
|
||||
{countryNames[country] ||
|
||||
intl.formatMessage({ id: 'label.unknown', defaultMessage: 'Unknown' })}
|
||||
</b>
|
||||
),
|
||||
browser: <b>{BROWSERS[browser]}</b>,
|
||||
os: <b>{os}</b>,
|
||||
device: <b>{intl.formatMessage(devices[device])?.toLowerCase()}</b>,
|
||||
|
@ -140,7 +145,7 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
|||
}
|
||||
|
||||
function getTime({ created_at }) {
|
||||
return format(new Date(created_at), 'h:mm:ss');
|
||||
return dateFormat(new Date(created_at), 'pp', locale);
|
||||
}
|
||||
|
||||
function getColor(row) {
|
||||
|
@ -176,9 +181,11 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
|||
</div>
|
||||
<div className={styles.body}>
|
||||
{logs?.length === 0 && <NoData />}
|
||||
<FixedSizeList height={400} itemCount={logs.length} itemSize={40}>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
{logs?.length > 0 && (
|
||||
<FixedSizeList height={400} itemCount={logs.length} itemSize={40}>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
.table {
|
||||
font-size: var(--font-size-xsmall);
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: fit-content(100%) fit-content(100%) auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
@ -21,6 +24,7 @@
|
|||
|
||||
.body {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
|
|
@ -42,7 +42,6 @@ export default function ReferrersTable({ websiteId, websiteDomain, showFilters,
|
|||
type="referrer"
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
websiteId={websiteId}
|
||||
websiteDomain={websiteDomain}
|
||||
dataFilter={refFilter}
|
||||
filterOptions={{
|
||||
domain: websiteDomain,
|
||||
|
|
|
@ -6,13 +6,15 @@ import useDateRange from 'hooks/useDateRange';
|
|||
import { DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||
import { getDateRange } from 'lib/date';
|
||||
import styles from './DateRangeSetting.module.css';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export default function DateRangeSetting() {
|
||||
const [locale] = useLocale();
|
||||
const [dateRange, setDateRange] = useDateRange();
|
||||
const { startDate, endDate, value } = dateRange;
|
||||
|
||||
function handleReset() {
|
||||
setDateRange(getDateRange(DEFAULT_DATE_RANGE));
|
||||
setDateRange(getDateRange(DEFAULT_DATE_RANGE, locale));
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -16,12 +16,18 @@ export default function LanguageButton() {
|
|||
return (
|
||||
<>
|
||||
<Head>
|
||||
{(locale === 'zh-CN' || locale === 'zh-TW') && (
|
||||
{locale === 'zh-CN' && (
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
)}
|
||||
{locale === 'zh-TW' && (
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
)}
|
||||
{locale === 'ja-JP' && (
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap"
|
||||
|
|
|
@ -5,9 +5,11 @@ import { getItem, setItem } from 'lib/web';
|
|||
import { setDateRange } from '../redux/actions/websites';
|
||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||
import useForceUpdate from './useForceUpdate';
|
||||
import useLocale from './useLocale';
|
||||
|
||||
export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_RANGE) {
|
||||
const dispatch = useDispatch();
|
||||
const [locale] = useLocale();
|
||||
const dateRange = useSelector(state => state.websites[websiteId]?.dateRange);
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
|
@ -16,7 +18,7 @@ export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_
|
|||
|
||||
if (globalDefault) {
|
||||
if (typeof globalDefault === 'string') {
|
||||
globalDateRange = getDateRange(globalDefault);
|
||||
globalDateRange = getDateRange(globalDefault, locale);
|
||||
} else if (typeof globalDefault === 'object') {
|
||||
globalDateRange = {
|
||||
...globalDefault,
|
||||
|
@ -37,5 +39,5 @@ export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_
|
|||
}
|
||||
}
|
||||
|
||||
return [dateRange || globalDateRange || getDateRange(defaultDateRange), saveDateRange];
|
||||
return [dateRange || globalDateRange || getDateRange(defaultDateRange, locale), saveDateRange];
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"label.administrator": "Administrator",
|
||||
"label.all": "Alle",
|
||||
"label.all-websites": "Alle Webseiten",
|
||||
"label.all-events": "Alle Ereignisse",
|
||||
"label.back": "Zurück",
|
||||
"label.cancel": "Abbrechen",
|
||||
"label.change-password": "Passwort ändern",
|
||||
|
@ -38,7 +39,7 @@
|
|||
"label.passwords-dont-match": "Passwörter stimmen nicht überein",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Echtzeit",
|
||||
"label.realtime-logs": "Echtzeit Logs",
|
||||
"label.realtime-logs": "Echtzeit-Protokoll",
|
||||
"label.refresh": "Aktualisieren",
|
||||
"label.required": "Erforderlich",
|
||||
"label.reset": "Zurücksetzen",
|
||||
|
@ -57,23 +58,23 @@
|
|||
"label.view-details": "Details anzeigen",
|
||||
"label.websites": "Webseiten",
|
||||
"message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}",
|
||||
"message.confirm-delete": "Sind sie sich sicher {target} zu löschen?",
|
||||
"message.confirm-delete": "Sind Sie sich sicher {target} zu löschen?",
|
||||
"message.copied": "In Zwischenablage kopiert!",
|
||||
"message.delete-warning": "Alle zugehörigen Daten werden auch gelöscht.",
|
||||
"message.delete-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.",
|
||||
"message.failure": "Es ist ein Fehler aufgetreten.",
|
||||
"message.get-share-url": "Freigabe-URL abrufen",
|
||||
"message.get-tracking-code": "Erstelle Tracking Kennung",
|
||||
"message.go-to-settings": "Zu den Einstellungen",
|
||||
"message.incorrect-username-password": "Falsches Passwort oder Benutzername.",
|
||||
"message.log.visitor": "Besucher aus {country} benutzt {browser} auf {os} {device}",
|
||||
"message.new-version-available": "Eine neue Version umami {version} ist verfügbar!",
|
||||
"message.new-version-available": "Eine neue Version von umami {version} ist verfügbar!",
|
||||
"message.no-data-available": "Keine Daten vorhanden.",
|
||||
"message.no-websites-configured": "Es ist keine Webseite vorhanden.",
|
||||
"message.page-not-found": "Seite nicht gefunden.",
|
||||
"message.powered-by": "Ermöglicht durch {name}",
|
||||
"message.powered-by": "Betrieben durch {name}",
|
||||
"message.save-success": "Erfolgreich gespeichert.",
|
||||
"message.share-url": "Dies ist die öffentliche URL zum Teilen für {target}.",
|
||||
"message.track-stats": "Um die Statistiken für {target} zu übermitteln, platzieren Sie bitte den folgenden Quelltext im {head} ihrer Homepage.",
|
||||
"message.track-stats": "Um die Statistiken für {target} zu übermitteln, platzieren Sie bitte den folgenden Quelltext im {head} ihrer Webseite.",
|
||||
"message.type-delete": "Geben Sie {delete} in das Feld unten ein um zu bestätigen.",
|
||||
"metrics.actions": "Aktionen",
|
||||
"metrics.average-visit-time": "Durchschn. Besuchszeit",
|
||||
|
@ -92,7 +93,7 @@
|
|||
"metrics.operating-systems": "Betriebssysteme",
|
||||
"metrics.page-views": "Seitenaufrufe",
|
||||
"metrics.pages": "Seiten",
|
||||
"metrics.referrers": "Referrers",
|
||||
"metrics.referrers": "Referrer",
|
||||
"metrics.unique-visitors": "Eindeutige Besucher",
|
||||
"metrics.views": "Aufrufe",
|
||||
"metrics.visitors": "Besucher"
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"label.administrator": "Administrator",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.all-events": "All events",
|
||||
"label.back": "Back",
|
||||
"label.cancel": "Cancel",
|
||||
"label.change-password": "Change password",
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"label.administrator": "Administrador",
|
||||
"label.all": "Todos",
|
||||
"label.all-websites": "Todos los sitios",
|
||||
"label.all-events": "Todos los eventos",
|
||||
"label.back": "Atrás",
|
||||
"label.cancel": "Cancelar",
|
||||
"label.change-password": "Cambiar contraseña",
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"label.accounts": "حساب ها",
|
||||
"label.add-account": "افزودن حساب",
|
||||
"label.add-website": "افزودن وب سایت",
|
||||
"label.administrator": "مدیر",
|
||||
"label.all": "همه",
|
||||
"label.all-websites": "همه وب سایت ها",
|
||||
"label.all-events": "همه رویداد ها",
|
||||
"label.back": "برگشت",
|
||||
"label.cancel": "انصراف",
|
||||
"label.change-password": "تغییر رمز",
|
||||
"label.confirm-password": "تایید رمز",
|
||||
"label.copy-to-clipboard": "کپی به حافظه",
|
||||
"label.current-password": "رمز فعلی",
|
||||
"label.custom-range": "محدوده دلخواه",
|
||||
"label.dashboard": "داشبورد",
|
||||
"label.date-range": "محدوده تاریخ",
|
||||
"label.default-date-range": "محدوده پیشفرض تاریخ",
|
||||
"label.delete": "حذف",
|
||||
"label.delete-account": "حذف حساب",
|
||||
"label.delete-website": "حذف وب سایت",
|
||||
"label.dismiss": "رد کردن",
|
||||
"label.domain": "دامنه",
|
||||
"label.edit": "ویرایش",
|
||||
"label.edit-account": "ویرایش حساب",
|
||||
"label.edit-website": "ویرایش وب سایت",
|
||||
"label.enable-share-url": "فعال کردن اشتراک گذاری URL",
|
||||
"label.invalid": "نامعتبر",
|
||||
"label.invalid-domain": "دامنه نامعتبر",
|
||||
"label.last-days": "لیست {x} روز",
|
||||
"label.last-hours": "لیست {x} ساعت",
|
||||
"label.logged-in-as": "وارد شده به عنوان {username}",
|
||||
"label.login": "ورود",
|
||||
"label.logout": "خروج",
|
||||
"label.more": "بیشتر",
|
||||
"label.name": "نام",
|
||||
"label.new-password": "رمز جدید",
|
||||
"label.password": "رمز",
|
||||
"label.passwords-dont-match": "رمز ها یکسان نیستند",
|
||||
"label.profile": "پروفایل",
|
||||
"label.realtime": "آمار هم اکنون",
|
||||
"label.realtime-logs": "لاگ های هم اکنون",
|
||||
"label.refresh": "تازه کردن",
|
||||
"label.required": "لازم",
|
||||
"label.reset": "ریست",
|
||||
"label.save": "ذخیره",
|
||||
"label.settings": "تنظیمات",
|
||||
"label.share-url": "به اشتراک گذاری URL",
|
||||
"label.single-day": "یک روز",
|
||||
"label.this-month": "این ماه",
|
||||
"label.this-week": "این هفته",
|
||||
"label.this-year": "امسال",
|
||||
"label.timezone": "منطقه زمانی",
|
||||
"label.today": "امروز",
|
||||
"label.tracking-code": "کد رهگیری",
|
||||
"label.unknown": "ناشناخته",
|
||||
"label.username": "نام کاربری",
|
||||
"label.view-details": "مشاهده جزئیات",
|
||||
"label.websites": "وب سایت ها",
|
||||
"message.active-users": "{x} هم اکنون {x, plural, one {یک} other {از میان}}",
|
||||
"message.confirm-delete": "آیا مطمئن هستید می خواهید {target} را حذف کنید?",
|
||||
"message.copied": "کپی شد!",
|
||||
"message.delete-warning": "همه داده های مرتبط هم حذف خواهد شد.",
|
||||
"message.failure": "مشکلی پیش آمده است.",
|
||||
"message.get-share-url": "دریافت URL برای اشتراک گذاری",
|
||||
"message.get-tracking-code": "گرفتن کد رهگیری",
|
||||
"message.go-to-settings": "رفتن به تنظیمات",
|
||||
"message.incorrect-username-password": "نام کاربری / رمز نادرست است.",
|
||||
"message.log.visitor": "بازدید کننده از کشور {country} با مروگر {browser} در {os} {device}",
|
||||
"message.new-version-available": "نسخه جدید umami ({version}) وجود است!",
|
||||
"message.no-data-available": "اطلاعاتی موجود نیست.",
|
||||
"message.no-websites-configured": "شما هیچ وب سایتی را پیکر بندی نکرده اید.",
|
||||
"message.page-not-found": "صفحه یافت نشد.",
|
||||
"message.powered-by": "قدرت گرفته توسط {name}",
|
||||
"message.save-success": "با موفقیت ذخیره شد.",
|
||||
"message.share-url": "این URL به اشتراک گذاشته شده عمومی برای {target} است.",
|
||||
"message.track-stats": "برای ردیابی آمار {target}, کد روبرو را در قسمت {head} وب سایت قرار دهید.",
|
||||
"message.type-delete": "جهت اطمینان '{delete}' را در کادر زیر بنویسید.",
|
||||
"metrics.actions": "اقدامات",
|
||||
"metrics.average-visit-time": "میانگین زمان بازدید",
|
||||
"metrics.bounce-rate": "نرخ Bounce",
|
||||
"metrics.browsers": "مروگر ها",
|
||||
"metrics.countries": "کشور ها",
|
||||
"metrics.device.desktop": "دسکتاپ",
|
||||
"metrics.device.laptop": "لپ تاپ",
|
||||
"metrics.device.mobile": "موبایل",
|
||||
"metrics.device.tablet": "تبلت",
|
||||
"metrics.devices": "دستگاه ها",
|
||||
"metrics.events": "رویداد ها",
|
||||
"metrics.filter.combined": "ترکیب شده",
|
||||
"metrics.filter.domain-only": "فقط دامنه",
|
||||
"metrics.filter.raw": "خام",
|
||||
"metrics.operating-systems": "سیستم عامل ها",
|
||||
"metrics.page-views": "بازدید صفحه",
|
||||
"metrics.pages": "صفحه ها",
|
||||
"metrics.referrers": "ارجاع دهندگان",
|
||||
"metrics.unique-visitors": "بازدید کننده خالص",
|
||||
"metrics.views": "بازدید",
|
||||
"metrics.visitors": "بازدید کننده"
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
"label.administrator": "Administrateur",
|
||||
"label.all": "Tout",
|
||||
"label.all-websites": "Tous les sites web",
|
||||
"label.all-events": "Tous les événements",
|
||||
"label.back": "Retour",
|
||||
"label.cancel": "Annuler",
|
||||
"label.change-password": "Changer le mot de passe",
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
"label.current-password": "目前密码",
|
||||
"label.custom-range": "自定义时间段",
|
||||
"label.dashboard": "仪表板",
|
||||
"label.date-range": "多日",
|
||||
"label.default-date-range": "默认日期范围",
|
||||
"label.date-range": "时间段",
|
||||
"label.default-date-range": "默认时间段",
|
||||
"label.delete": "删除",
|
||||
"label.delete-account": "删除账户",
|
||||
"label.delete-website": "删除网站",
|
||||
|
@ -23,7 +23,7 @@
|
|||
"label.edit": "编辑",
|
||||
"label.edit-account": "编辑账户",
|
||||
"label.edit-website": "编辑网站",
|
||||
"label.enable-share-url": "激活共享链接",
|
||||
"label.enable-share-url": "启用共享链接",
|
||||
"label.invalid": "输入无效",
|
||||
"label.invalid-domain": "无效域名",
|
||||
"label.last-days": "最近 {x} 天",
|
||||
|
@ -57,30 +57,30 @@
|
|||
"label.view-details": "查看更多",
|
||||
"label.websites": "网站",
|
||||
"message.active-users": "当前在线 {x} 人",
|
||||
"message.confirm-delete": "你确定要删除{target}吗?",
|
||||
"message.copied": "复制成功!",
|
||||
"message.delete-warning": "所有相关数据将会被删除.",
|
||||
"message.failure": "出现错误.",
|
||||
"message.get-share-url": "获得共享链接",
|
||||
"message.get-tracking-code": "获得跟踪代码",
|
||||
"message.confirm-delete": "你确定要删除 {target} 吗?",
|
||||
"message.copied": "复制成功!",
|
||||
"message.delete-warning": "所有相关数据将会被删除。",
|
||||
"message.failure": "出现错误。",
|
||||
"message.get-share-url": "获取共享链接",
|
||||
"message.get-tracking-code": "获取跟踪代码",
|
||||
"message.go-to-settings": "去设置",
|
||||
"message.incorrect-username-password": "用户名密码不正确.",
|
||||
"message.log.visitor": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 进行访问.",
|
||||
"message.incorrect-username-password": "用户名或密码不正确。",
|
||||
"message.log.visitor": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 浏览器进行访问。",
|
||||
"message.new-version-available": "umami 有新版本 {version} 发布啦!",
|
||||
"message.no-data-available": "无可用数据.",
|
||||
"message.no-websites-configured": "你还没有设置任何网站.",
|
||||
"message.page-not-found": "网页未找到.",
|
||||
"message.powered-by": "运行 {name}",
|
||||
"message.save-success": "成功保存.",
|
||||
"message.share-url": "这是 {target} 的共享链接.",
|
||||
"message.track-stats": "把以下代码放到你的网站的{head}部分来收集{target}的数据.",
|
||||
"message.type-delete": "在下面空格输入{delete}确认",
|
||||
"message.no-data-available": "无可用数据。",
|
||||
"message.no-websites-configured": "你还没有设置任何网站。",
|
||||
"message.page-not-found": "网页未找到。",
|
||||
"message.powered-by": "由 {name} 提供支持",
|
||||
"message.save-success": "保存成功。",
|
||||
"message.share-url": "这是 {target} 的共享链接。",
|
||||
"message.track-stats": "把以下代码放到你的网站的 {head} 部分来收集 {target} 的数据。",
|
||||
"message.type-delete": "在下方输入框输入 {delete} 以确认删除。",
|
||||
"metrics.actions": "用户行为",
|
||||
"metrics.average-visit-time": "平均访问时间",
|
||||
"metrics.bounce-rate": "跳出率",
|
||||
"metrics.browsers": "浏览器",
|
||||
"metrics.countries": "国家",
|
||||
"metrics.device.desktop": "台式机",
|
||||
"metrics.device.desktop": "桌面电脑",
|
||||
"metrics.device.laptop": "笔记本",
|
||||
"metrics.device.mobile": "手机",
|
||||
"metrics.device.tablet": "平板",
|
||||
|
@ -90,10 +90,10 @@
|
|||
"metrics.filter.domain-only": "只看域名",
|
||||
"metrics.filter.raw": "原始",
|
||||
"metrics.operating-systems": "操作系统",
|
||||
"metrics.page-views": "页面流量",
|
||||
"metrics.page-views": "页面浏览量",
|
||||
"metrics.pages": "网页",
|
||||
"metrics.referrers": "指入域名",
|
||||
"metrics.referrers": "来源域名",
|
||||
"metrics.unique-visitors": "独立访客",
|
||||
"metrics.views": "页面流量",
|
||||
"metrics.visitors": "独立访客"
|
||||
"metrics.views": "浏览量",
|
||||
"metrics.visitors": "访客"
|
||||
}
|
||||
|
|
|
@ -80,12 +80,14 @@ export const POSTGRESQL_DATE_FORMATS = {
|
|||
year: 'YYYY-01-01',
|
||||
};
|
||||
|
||||
export const DOMAIN_REGEX = /localhost(:\d{1,5})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}/;
|
||||
export const DOMAIN_REGEX = /^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63})$/;
|
||||
|
||||
export const DESKTOP_SCREEN_WIDTH = 1920;
|
||||
export const LAPTOP_SCREEN_WIDTH = 1024;
|
||||
export const MOBILE_SCREEN_WIDTH = 479;
|
||||
|
||||
export const URL_LENGTH = 500;
|
||||
|
||||
export const DESKTOP_OS = [
|
||||
'Windows 3.11',
|
||||
'Windows 95',
|
||||
|
|
23
lib/date.js
23
lib/date.js
|
@ -23,7 +23,10 @@ import {
|
|||
differenceInCalendarDays,
|
||||
differenceInCalendarMonths,
|
||||
differenceInCalendarYears,
|
||||
format,
|
||||
} from 'date-fns';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import { dateLocales } from 'lib/lang';
|
||||
|
||||
export function getTimezone() {
|
||||
return moment.tz.guess();
|
||||
|
@ -33,8 +36,9 @@ export function getLocalTime(t) {
|
|||
return addMinutes(new Date(t), new Date().getTimezoneOffset());
|
||||
}
|
||||
|
||||
export function getDateRange(value) {
|
||||
export function getDateRange(value, locale = 'en-US') {
|
||||
const now = new Date();
|
||||
const localeOptions = dateLocales[locale];
|
||||
|
||||
const { num, unit } = value.match(/^(?<num>[0-9]+)(?<unit>hour|day|week|month|year)$/).groups;
|
||||
|
||||
|
@ -49,8 +53,8 @@ export function getDateRange(value) {
|
|||
};
|
||||
case 'week':
|
||||
return {
|
||||
startDate: startOfWeek(now),
|
||||
endDate: endOfWeek(now),
|
||||
startDate: startOfWeek(now, { locale: localeOptions }),
|
||||
endDate: endOfWeek(now, { locale: localeOptions }),
|
||||
unit: 'day',
|
||||
value,
|
||||
};
|
||||
|
@ -150,3 +154,16 @@ export function getDateLength(startDate, endDate, unit) {
|
|||
const [diff] = dateFuncs[unit];
|
||||
return diff(endDate, startDate) + 1;
|
||||
}
|
||||
|
||||
export const customFormats = {
|
||||
'en-US': {
|
||||
p: 'ha',
|
||||
pp: 'h:mm:ss',
|
||||
},
|
||||
};
|
||||
|
||||
export function dateFormat(date, str, locale = 'en-US') {
|
||||
return format(date, customFormats?.[locale]?.[str] || str, {
|
||||
locale: dateLocales[locale] || enUS,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -113,12 +113,18 @@ export const refFilter = (data, { domain, domainOnly, raw }) => {
|
|||
return Object.keys(map).map(key => ({ x: key, y: map[key], w: links[key] }));
|
||||
};
|
||||
|
||||
export const browserFilter = data =>
|
||||
data.map(({ x, y }) => ({ x: BROWSERS[x] || x, y })).filter(({ x }) => x);
|
||||
export const browserFilter = data => data.map(({ x, y }) => ({ x: BROWSERS[x] ?? x, y }));
|
||||
|
||||
export const osFilter = data => data.filter(({ x }) => x);
|
||||
export const eventTypeFilter = (data, types) => {
|
||||
if (!types || types.length === 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
export const deviceFilter = data => data.filter(({ x }) => x);
|
||||
return data.filter(({ x }) => {
|
||||
const [event] = x.split('\t');
|
||||
return types.some(type => type === event);
|
||||
});
|
||||
};
|
||||
|
||||
export const percentFilter = data => {
|
||||
const total = data.reduce((n, { y }) => n + y, 0);
|
||||
|
|
10
lib/lang.js
10
lib/lang.js
|
@ -1,4 +1,3 @@
|
|||
import { format } from 'date-fns';
|
||||
import {
|
||||
cs,
|
||||
da,
|
||||
|
@ -8,6 +7,7 @@ import {
|
|||
es,
|
||||
fi,
|
||||
fr,
|
||||
faIR,
|
||||
he,
|
||||
hi,
|
||||
id,
|
||||
|
@ -55,6 +55,7 @@ import taMessages from 'lang-compiled/ta-IN.json';
|
|||
import hiMessages from 'lang-compiled/hi-IN.json';
|
||||
import heMessages from 'lang-compiled/he-IL.json';
|
||||
import itMessages from 'lang-compiled/it-IT.json';
|
||||
import faIRMessages from 'lang-compiled/fa-IR.json';
|
||||
|
||||
export const messages = {
|
||||
'en-US': enMessages,
|
||||
|
@ -85,6 +86,7 @@ export const messages = {
|
|||
'hi-IN': hiMessages,
|
||||
'he-IL': heMessages,
|
||||
'it-IT': itMessages,
|
||||
'fa-IR': faIRMessages,
|
||||
};
|
||||
|
||||
export const dateLocales = {
|
||||
|
@ -116,6 +118,7 @@ export const dateLocales = {
|
|||
'hi-IN': hi,
|
||||
'he-IL': he,
|
||||
'it-IT': it,
|
||||
'fa-IR': faIR,
|
||||
};
|
||||
|
||||
export const menuOptions = [
|
||||
|
@ -126,6 +129,7 @@ export const menuOptions = [
|
|||
{ label: 'Deutsch', value: 'de-DE', display: 'de' },
|
||||
{ label: 'English', value: 'en-US', display: 'en' },
|
||||
{ label: 'Español', value: 'es-MX', display: 'es' },
|
||||
{ label: 'فارسی', value: 'fa-IR', display: 'fa' },
|
||||
{ label: 'Føroyskt', value: 'fo-FO', display: 'fo' },
|
||||
{ label: 'Français', value: 'fr-FR', display: 'fr' },
|
||||
{ label: 'Ελληνικά', value: 'el-GR', display: 'el' },
|
||||
|
@ -148,7 +152,3 @@ export const menuOptions = [
|
|||
{ label: 'Türkçe', value: 'tr-TR', display: 'tr' },
|
||||
{ label: 'українська', value: 'uk-UA', display: 'uk' },
|
||||
];
|
||||
|
||||
export function dateFormat(date, str, locale) {
|
||||
return format(date, str, { locale: dateLocales[locale] || enUS });
|
||||
}
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import moment from 'moment-timezone';
|
||||
import prisma from 'lib/db';
|
||||
import { subMinutes } from 'date-fns';
|
||||
import { MYSQL, POSTGRESQL, MYSQL_DATE_FORMATS, POSTGRESQL_DATE_FORMATS } from 'lib/constants';
|
||||
import {
|
||||
MYSQL,
|
||||
POSTGRESQL,
|
||||
MYSQL_DATE_FORMATS,
|
||||
POSTGRESQL_DATE_FORMATS,
|
||||
URL_LENGTH,
|
||||
} from 'lib/constants';
|
||||
|
||||
export function getDatabase() {
|
||||
const type =
|
||||
|
@ -190,8 +196,8 @@ export async function savePageView(website_id, session_id, url, referrer) {
|
|||
session_id,
|
||||
},
|
||||
},
|
||||
url,
|
||||
referrer,
|
||||
url: url.substr(0, URL_LENGTH),
|
||||
referrer: referrer.substr(0, URL_LENGTH),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
@ -211,7 +217,7 @@ export async function saveEvent(website_id, session_id, url, event_type, event_v
|
|||
session_id,
|
||||
},
|
||||
},
|
||||
url,
|
||||
url: url.substr(0, URL_LENGTH),
|
||||
event_type,
|
||||
event_value,
|
||||
},
|
||||
|
@ -428,7 +434,7 @@ export function getPageviewMetrics(website_id, start_at, end_at, field, table, f
|
|||
|
||||
if (domain) {
|
||||
domainFilter = `and referrer not like $${params.length + 1} and referrer not like '/%'`;
|
||||
params.push(`%${domain}%`);
|
||||
params.push(`%://${domain}/%`);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
|
|
23
package.json
23
package.json
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"name": "umami",
|
||||
"version": "1.12.0",
|
||||
"version": "1.15.0",
|
||||
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
||||
"author": "Mike Cao <mike@mikecao.com>",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/mikecao/umami",
|
||||
"homepage": "https://umami.is",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mikecao/umami.git"
|
||||
|
@ -56,7 +56,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "2.15.0",
|
||||
"@prisma/client": "2.18.0",
|
||||
"@reduxjs/toolkit": "^1.5.0",
|
||||
"bcrypt": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
|
@ -71,19 +71,20 @@
|
|||
"formik": "^2.2.6",
|
||||
"immer": "^8.0.1",
|
||||
"is-localhost-ip": "^1.4.0",
|
||||
"isbot-fast": "^1.2.0",
|
||||
"isbot": "^3.0.25",
|
||||
"jose": "2.0.3",
|
||||
"maxmind": "^4.3.1",
|
||||
"moment-timezone": "^0.5.32",
|
||||
"next": "^10.0.6",
|
||||
"next": "^10.0.8",
|
||||
"prompts": "2.4.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-intl": "^5.12.0",
|
||||
"react-intl": "^5.12.3",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-simple-maps": "^2.3.0",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-tooltip": "^4.2.11",
|
||||
"react-tooltip": "^4.2.14",
|
||||
"react-use-measure": "^2.0.3",
|
||||
"react-window": "^1.8.6",
|
||||
"redux": "^4.0.5",
|
||||
|
@ -97,7 +98,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^2.13.16",
|
||||
"@prisma/cli": "2.15.0",
|
||||
"@prisma/cli": "2.18.0",
|
||||
"@rollup/plugin-buble": "^0.21.3",
|
||||
"@rollup/plugin-node-resolve": "^11.1.1",
|
||||
"@rollup/plugin-replace": "^2.3.4",
|
||||
|
@ -105,14 +106,14 @@
|
|||
"cross-env": "^7.0.3",
|
||||
"del": "^6.0.0",
|
||||
"dotenv-cli": "^4.0.0",
|
||||
"eslint": "^7.19.0",
|
||||
"eslint": "^7.20.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"extract-react-intl-messages": "^4.1.1",
|
||||
"husky": "^4.3.8",
|
||||
"lint-staged": "^10.5.3",
|
||||
"lint-staged": "^10.5.4",
|
||||
"loadtest": "5.1.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss-flexbugs-fixes": "^5.0.2",
|
||||
|
@ -123,7 +124,7 @@
|
|||
"rollup": "^2.38.3",
|
||||
"rollup-plugin-hashbang": "^2.2.2",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"stylelint": "^13.9.0",
|
||||
"stylelint": "^13.10.0",
|
||||
"stylelint-config-css-modules": "^2.2.0",
|
||||
"stylelint-config-prettier": "^8.0.1",
|
||||
"stylelint-config-recommended": "^3.0.0",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { Provider } from 'react-redux';
|
||||
import { useStore } from 'redux/store';
|
||||
|
@ -25,17 +26,20 @@ const Intl = ({ children }) => {
|
|||
export default function App({ Component, pageProps }) {
|
||||
useForceSSL(process.env.FORCE_SSL);
|
||||
const store = useStore();
|
||||
const { basePath } = useRouter();
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Head>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" />
|
||||
<link rel="manifest" href="site.webmanifest" />
|
||||
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={`${basePath}/apple-touch-icon.png`} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={`${basePath}/favicon-32x32.png`} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={`${basePath}/favicon-16x16.png`} />
|
||||
<link rel="manifest" href={`${basePath}/site.webmanifest`} />
|
||||
<link rel="mask-icon" href={`${basePath}/safari-pinned-tab.svg`} color="#5bbad5" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</Head>
|
||||
<Intl>
|
||||
<Component {...pageProps} />
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import isBot from 'isbot-fast';
|
||||
import isbot from 'isbot';
|
||||
import { savePageView, saveEvent } from 'lib/queries';
|
||||
import { useCors, useSession } from 'lib/middleware';
|
||||
import { getIpAddress } from 'lib/request';
|
||||
import { ok, badRequest } from 'lib/response';
|
||||
import { createToken } from 'lib/crypto';
|
||||
import { getIpAddress } from '../../lib/request';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useCors(req, res);
|
||||
|
||||
if (isBot(req.headers['user-agent'])) {
|
||||
if (isbot(req.headers['user-agent'])) {
|
||||
return ok(res);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { getPageviewMetrics, getSessionMetrics } from 'lib/queries';
|
||||
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
|
||||
import { DOMAIN_REGEX } from 'lib/constants';
|
||||
import { getPageviewMetrics, getSessionMetrics, getWebsiteById } from 'lib/queries';
|
||||
import { ok, methodNotAllowed, unauthorized, badRequest } from 'lib/response';
|
||||
import { allowQuery } from 'lib/auth';
|
||||
|
||||
const sessionColumns = ['browser', 'os', 'device', 'country'];
|
||||
|
@ -31,11 +30,7 @@ export default async (req, res) => {
|
|||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { id, type, start_at, end_at, domain, url } = req.query;
|
||||
|
||||
if (domain && !DOMAIN_REGEX.test(domain)) {
|
||||
return badRequest(res);
|
||||
}
|
||||
const { id, type, start_at, end_at, url } = req.query;
|
||||
|
||||
const websiteId = +id;
|
||||
const startDate = new Date(+start_at);
|
||||
|
@ -47,7 +42,18 @@ export default async (req, res) => {
|
|||
return ok(res, data);
|
||||
}
|
||||
|
||||
if (type === 'event' || pageviewColumns.includes(type)) {
|
||||
if (pageviewColumns.includes(type) || type === 'event') {
|
||||
let domain;
|
||||
if (type === 'referrer') {
|
||||
const website = getWebsiteById(websiteId);
|
||||
|
||||
if (!website) {
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
domain = website.domain;
|
||||
}
|
||||
|
||||
const data = await getPageviewMetrics(
|
||||
websiteId,
|
||||
startDate,
|
||||
|
@ -55,7 +61,7 @@ export default async (req, res) => {
|
|||
getColumn(type),
|
||||
getTable(type),
|
||||
{
|
||||
domain: type !== 'event' && domain,
|
||||
domain,
|
||||
url: type !== 'url' && url,
|
||||
},
|
||||
);
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -14,6 +14,7 @@ body {
|
|||
font-size: var(--font-size-normal);
|
||||
color: var(--gray900);
|
||||
background: var(--gray75);
|
||||
overflow-y: overlay;
|
||||
}
|
||||
|
||||
.zh-CN {
|
||||
|
@ -21,7 +22,7 @@ body {
|
|||
}
|
||||
|
||||
.zh-TW {
|
||||
font-family: 'Noto Sans SC', sans-serif !important;
|
||||
font-family: 'Noto Sans TC', sans-serif !important;
|
||||
}
|
||||
|
||||
.ja-JP {
|
||||
|
|
Loading…
Reference in New Issue