mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-18 15:23:38 +01:00
commit
bb0f762217
@ -1,10 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import ReactTooltip from 'react-tooltip';
|
import ReactTooltip from 'react-tooltip';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from './Icon';
|
import Icon from './Icon';
|
||||||
import styles from './Button.module.css';
|
import styles from './Button.module.css';
|
||||||
|
|
||||||
export default function Button({
|
function Button({
|
||||||
type = 'button',
|
type = 'button',
|
||||||
icon,
|
icon,
|
||||||
size,
|
size,
|
||||||
@ -43,3 +44,19 @@ export default function Button({
|
|||||||
</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 React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import styles from './ButtonGroup.module.css';
|
import styles from './ButtonGroup.module.css';
|
||||||
|
|
||||||
export default function ButtonGroup({
|
function ButtonGroup({ items = [], selectedItem, className, size, icon, onClick = () => {} }) {
|
||||||
items = [],
|
|
||||||
selectedItem,
|
|
||||||
className,
|
|
||||||
size,
|
|
||||||
icon,
|
|
||||||
onClick = () => {},
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.group, className)}>
|
<div className={classNames(styles.group, className)}>
|
||||||
{items.map(item => {
|
{items.map(item => {
|
||||||
@ -30,3 +24,19 @@ export default function ButtonGroup({
|
|||||||
</div>
|
</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;
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import Icon from 'components/common/Icon';
|
import Icon from 'components/common/Icon';
|
||||||
import Check from 'assets/check.svg';
|
import Check from 'assets/check.svg';
|
||||||
import styles from './Checkbox.module.css';
|
import styles from './Checkbox.module.css';
|
||||||
|
|
||||||
export default function Checkbox({ name, value, label, onChange }) {
|
function Checkbox({ name, value, label, onChange }) {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -25,3 +26,12 @@ export default function Checkbox({ name, value, label, onChange }) {
|
|||||||
</div>
|
</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 React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
@ -6,7 +7,7 @@ const defaultText = (
|
|||||||
<FormattedMessage id="label.copy-to-clipboard" defaultMessage="Copy to clipboard" />
|
<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);
|
const [text, setText] = useState(defaultText);
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
@ -24,3 +25,13 @@ export default function CopyButton({ element, ...props }) {
|
|||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CopyButton.propTypes = {
|
||||||
|
element: PropTypes.shape({
|
||||||
|
current: PropTypes.shape({
|
||||||
|
select: PropTypes.func.isRequired,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CopyButton;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { endOfYear, isSameDay } from 'date-fns';
|
import { endOfYear, isSameDay } from 'date-fns';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
@ -54,7 +55,7 @@ const filterOptions = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function DateFilter({ value, startDate, endDate, onChange, className }) {
|
function DateFilter({ value, startDate, endDate, onChange, className }) {
|
||||||
const [showPicker, setShowPicker] = useState(false);
|
const [showPicker, setShowPicker] = useState(false);
|
||||||
const displayValue =
|
const displayValue =
|
||||||
value === 'custom' ? (
|
value === 'custom' ? (
|
||||||
@ -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 React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import styles from './Dot.module.css';
|
import styles from './Dot.module.css';
|
||||||
|
|
||||||
export default function Dot({ color, size, className }) {
|
function Dot({ color, size, className }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<div
|
<div
|
||||||
@ -15,3 +16,11 @@ export default function Dot({ color, size, className }) {
|
|||||||
</div>
|
</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 React, { useState, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Menu from './Menu';
|
import Menu from './Menu';
|
||||||
import useDocumentClick from 'hooks/useDocumentClick';
|
import useDocumentClick from 'hooks/useDocumentClick';
|
||||||
@ -6,13 +7,7 @@ import Chevron from 'assets/chevron-down.svg';
|
|||||||
import styles from './Dropdown.module.css';
|
import styles from './Dropdown.module.css';
|
||||||
import Icon from './Icon';
|
import Icon from './Icon';
|
||||||
|
|
||||||
export default function DropDown({
|
function DropDown({ value, className, menuClassName, options = [], onChange = () => {} }) {
|
||||||
value,
|
|
||||||
className,
|
|
||||||
menuClassName,
|
|
||||||
options = [],
|
|
||||||
onChange = () => {},
|
|
||||||
}) {
|
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
const selectedOption = options.find(e => e.value === value);
|
const selectedOption = options.find(e => e.value === value);
|
||||||
@ -29,7 +24,7 @@ export default function DropDown({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useDocumentClick(e => {
|
useDocumentClick(e => {
|
||||||
if (!ref.current.contains(e.target)) {
|
if (!ref.current?.contains(e.target)) {
|
||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -52,3 +47,18 @@ export default function DropDown({
|
|||||||
</div>
|
</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,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import Icon from 'components/common/Icon';
|
import Icon from 'components/common/Icon';
|
||||||
import Logo from 'assets/logo.svg';
|
import Logo from 'assets/logo.svg';
|
||||||
import styles from './EmptyPlaceholder.module.css';
|
import styles from './EmptyPlaceholder.module.css';
|
||||||
|
|
||||||
export default function EmptyPlaceholder({ msg, children }) {
|
function EmptyPlaceholder({ msg, children }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
<Icon className={styles.icon} icon={<Logo />} size="xlarge" />
|
<Icon className={styles.icon} icon={<Logo />} size="xlarge" />
|
||||||
@ -12,3 +13,10 @@ export default function EmptyPlaceholder({ msg, children }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EmptyPlaceholder.propTypes = {
|
||||||
|
msg: PropTypes.node,
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmptyPlaceholder;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import styles from './Favicon.module.css';
|
import styles from './Favicon.module.css';
|
||||||
|
|
||||||
function getHostName(url) {
|
function getHostName(url) {
|
||||||
@ -6,7 +7,7 @@ function getHostName(url) {
|
|||||||
return match && match.length > 1 ? match[1] : null;
|
return match && match.length > 1 ? match[1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Favicon({ domain, ...props }) {
|
function Favicon({ domain, ...props }) {
|
||||||
const hostName = domain ? getHostName(domain) : null;
|
const hostName = domain ? getHostName(domain) : null;
|
||||||
|
|
||||||
return hostName ? (
|
return hostName ? (
|
||||||
@ -19,3 +20,9 @@ export default function Favicon({ domain, ...props }) {
|
|||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Favicon.propTypes = {
|
||||||
|
domain: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Favicon;
|
||||||
|
@ -1,11 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||||
import ButtonGroup from './ButtonGroup';
|
import ButtonGroup from './ButtonGroup';
|
||||||
|
|
||||||
export default function FilterButtons({ buttons, selected, onClick }) {
|
function FilterButtons({ buttons, selected, onClick }) {
|
||||||
return (
|
return (
|
||||||
<ButtonLayout>
|
<ButtonLayout>
|
||||||
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
||||||
</ButtonLayout>
|
</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 React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import styles from './Icon.module.css';
|
import styles from './Icon.module.css';
|
||||||
|
|
||||||
export default function Icon({ icon, className, size = 'medium', ...props }) {
|
function Icon({ icon, className, size = 'medium', ...props }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(styles.icon, className, {
|
className={classNames(styles.icon, className, {
|
||||||
@ -18,3 +19,11 @@ export default function Icon({ icon, className, size = 'medium', ...props }) {
|
|||||||
</div>
|
</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 React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import NextLink from 'next/link';
|
import NextLink from 'next/link';
|
||||||
import Icon from './Icon';
|
import Icon from './Icon';
|
||||||
import styles from './Link.module.css';
|
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 (
|
return (
|
||||||
<NextLink {...props}>
|
<NextLink {...props}>
|
||||||
<a
|
<a
|
||||||
@ -21,3 +22,13 @@ export default function Link({ className, icon, children, size, iconRight, ...pr
|
|||||||
</NextLink>
|
</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 React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import styles from './Loading.module.css';
|
import styles from './Loading.module.css';
|
||||||
|
|
||||||
export default function Loading({ className }) {
|
function Loading({ className }) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.loading, className)}>
|
<div className={classNames(styles.loading, className)}>
|
||||||
<div />
|
<div />
|
||||||
@ -11,3 +12,9 @@ export default function Loading({ className }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import styles from './Menu.module.css';
|
import styles from './Menu.module.css';
|
||||||
|
|
||||||
export default function Menu({
|
function Menu({
|
||||||
options = [],
|
options = [],
|
||||||
selectedOption,
|
selectedOption,
|
||||||
className,
|
className,
|
||||||
@ -46,3 +47,24 @@ export default function Menu({
|
|||||||
</div>
|
</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 React, { useState, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Menu from 'components/common/Menu';
|
import Menu from 'components/common/Menu';
|
||||||
import Button from 'components/common/Button';
|
import Button from 'components/common/Button';
|
||||||
import useDocumentClick from 'hooks/useDocumentClick';
|
import useDocumentClick from 'hooks/useDocumentClick';
|
||||||
import styles from './MenuButton.module.css';
|
import styles from './MenuButton.module.css';
|
||||||
|
|
||||||
export default function MenuButton({
|
function MenuButton({
|
||||||
icon,
|
icon,
|
||||||
value,
|
value,
|
||||||
options,
|
options,
|
||||||
@ -30,7 +31,7 @@ export default function MenuButton({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useDocumentClick(e => {
|
useDocumentClick(e => {
|
||||||
if (!ref.current.contains(e.target)) {
|
if (!ref.current?.contains(e.target)) {
|
||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -58,3 +59,25 @@ export default function MenuButton({
|
|||||||
</div>
|
</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 React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { useSpring, animated } from 'react-spring';
|
import { useSpring, animated } from 'react-spring';
|
||||||
import styles from './Modal.module.css';
|
import styles from './Modal.module.css';
|
||||||
|
|
||||||
export default function Modal({ title, children }) {
|
function Modal({ title, children }) {
|
||||||
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
|
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
|
||||||
|
|
||||||
return ReactDOM.createPortal(
|
return ReactDOM.createPortal(
|
||||||
@ -16,3 +17,10 @@ export default function Modal({ title, children }) {
|
|||||||
document.getElementById('__modals'),
|
document.getElementById('__modals'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Modal.propTypes = {
|
||||||
|
title: PropTypes.node,
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import styles from './NavMenu.module.css';
|
import styles from './NavMenu.module.css';
|
||||||
|
|
||||||
export default function NavMenu({ options = [], className, onSelect = () => {} }) {
|
function NavMenu({ options = [], className, onSelect = () => {} }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -30,3 +31,17 @@ export default function NavMenu({ options = [], className, onSelect = () => {} }
|
|||||||
</div>
|
</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 React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import styles from './NoData.module.css';
|
import styles from './NoData.module.css';
|
||||||
|
|
||||||
export default function NoData({ className }) {
|
function NoData({ className }) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.container, className)}>
|
<div className={classNames(styles.container, className)}>
|
||||||
<FormattedMessage id="message.no-data-available" defaultMessage="No data available." />
|
<FormattedMessage id="message.no-data-available" defaultMessage="No data available." />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NoData.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoData;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { setDateRange } from 'redux/actions/websites';
|
import { setDateRange } from 'redux/actions/websites';
|
||||||
@ -8,7 +9,7 @@ import Dots from 'assets/ellipsis-h.svg';
|
|||||||
import useDateRange from 'hooks/useDateRange';
|
import useDateRange from 'hooks/useDateRange';
|
||||||
import { getDateRange } from '../../lib/date';
|
import { getDateRange } from '../../lib/date';
|
||||||
|
|
||||||
export default function RefreshButton({ websiteId }) {
|
function RefreshButton({ websiteId }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [dateRange] = useDateRange(websiteId);
|
const [dateRange] = useDateRange(websiteId);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -35,3 +36,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 React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import NoData from 'components/common/NoData';
|
import NoData from 'components/common/NoData';
|
||||||
import styles from './Table.module.css';
|
import styles from './Table.module.css';
|
||||||
|
|
||||||
export default function Table({
|
function Table({
|
||||||
columns,
|
columns,
|
||||||
rows,
|
rows,
|
||||||
empty,
|
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 }) => (
|
export const TableRow = ({ columns, row }) => (
|
||||||
<div className={classNames(styles.row, 'row')}>
|
<div className={classNames(styles.row, 'row')}>
|
||||||
{columns.map(({ key, render, className, style, cell }, index) => (
|
{columns.map(({ key, render, className, style, cell }, index) => (
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import styles from './Tag.module.css';
|
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>;
|
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 React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { useSpring, animated } from 'react-spring';
|
import { useSpring, animated } from 'react-spring';
|
||||||
import styles from './Toast.module.css';
|
import styles from './Toast.module.css';
|
||||||
import Icon from 'components/common/Icon';
|
import Icon from 'components/common/Icon';
|
||||||
import Close from 'assets/times.svg';
|
import Close from 'assets/times.svg';
|
||||||
|
|
||||||
export default function Toast({ message, timeout = 3000, onClose }) {
|
function Toast({ message, timeout = 3000, onClose }) {
|
||||||
const props = useSpring({
|
const props = useSpring({
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transform: 'translate3d(0,0px,0)',
|
transform: 'translate3d(0,0px,0)',
|
||||||
@ -24,3 +25,11 @@ export default function Toast({ message, timeout = 3000, onClose }) {
|
|||||||
document.getElementById('__modals'),
|
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 React, { useState, useMemo } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import ReactTooltip from 'react-tooltip';
|
import ReactTooltip from 'react-tooltip';
|
||||||
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@ -12,7 +13,7 @@ import { useRouter } from 'next/router';
|
|||||||
|
|
||||||
const geoUrl = '/world-110m.json';
|
const geoUrl = '/world-110m.json';
|
||||||
|
|
||||||
export default function WorldMap({ data, className }) {
|
function WorldMap({ data, className }) {
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
const [tooltip, setTooltip] = useState();
|
const [tooltip, setTooltip] = useState();
|
||||||
const [theme] = useTheme();
|
const [theme] = useTheme();
|
||||||
@ -89,3 +90,16 @@ export default function WorldMap({ data, className }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WorldMap.propTypes = {
|
||||||
|
data: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
x: PropTypes.string,
|
||||||
|
y: PropTypes.number,
|
||||||
|
z: PropTypes.number,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorldMap;
|
||||||
|
@ -44,4 +44,7 @@
|
|||||||
.header {
|
.header {
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
}
|
}
|
||||||
|
.nav {
|
||||||
|
font-size: var(--font-size-normal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import classNames from 'classnames';
|
|||||||
import ChartJS from 'chart.js';
|
import ChartJS from 'chart.js';
|
||||||
import Legend from 'components/metrics/Legend';
|
import Legend from 'components/metrics/Legend';
|
||||||
import { formatLongNumber } from 'lib/format';
|
import { formatLongNumber } from 'lib/format';
|
||||||
import { dateFormat } from 'lib/lang';
|
import { dateFormat, timeFormat } from 'lib/lang';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import useTheme from 'hooks/useTheme';
|
import useTheme from 'hooks/useTheme';
|
||||||
import { DEFAUL_CHART_HEIGHT, DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
|
import { DEFAUL_CHART_HEIGHT, DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
|
||||||
@ -44,9 +44,9 @@ export default function BarChart({
|
|||||||
|
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case 'minute':
|
case 'minute':
|
||||||
return index % 2 === 0 ? dateFormat(d, 'h:mm', locale) : '';
|
return index % 2 === 0 ? dateFormat(d, 'H:mm', locale) : '';
|
||||||
case 'hour':
|
case 'hour':
|
||||||
return dateFormat(d, 'ha', locale);
|
return timeFormat(d, locale);
|
||||||
case 'day':
|
case 'day':
|
||||||
if (records > 31) {
|
if (records > 31) {
|
||||||
if (w <= 500) {
|
if (w <= 500) {
|
||||||
@ -131,6 +131,7 @@ export default function BarChart({
|
|||||||
minRotation: 0,
|
minRotation: 0,
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
fontColor: colors.text,
|
fontColor: colors.text,
|
||||||
|
autoSkipPadding: 1,
|
||||||
},
|
},
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: false,
|
display: false,
|
||||||
|
@ -10,7 +10,11 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
|
|||||||
const countryNames = useCountryNames(locale);
|
const countryNames = useCountryNames(locale);
|
||||||
|
|
||||||
function renderLabel({ x }) {
|
function renderLabel({ x }) {
|
||||||
return <div className={locale}>{countryNames[x]}</div>;
|
return (
|
||||||
|
<div className={locale}>
|
||||||
|
{countryNames[x] ?? <FormattedMessage id="label.unknown" defaultMessage="Unknown" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { FixedSizeList } from 'react-window';
|
import { FixedSizeList } from 'react-window';
|
||||||
import { useSpring, animated, config } from 'react-spring';
|
import { useSpring, animated, config } from 'react-spring';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import NoData from 'components/common/NoData';
|
import NoData from 'components/common/NoData';
|
||||||
import { formatNumber, formatLongNumber } from 'lib/format';
|
import { formatNumber, formatLongNumber } from 'lib/format';
|
||||||
import styles from './DataTable.module.css';
|
import styles from './DataTable.module.css';
|
||||||
@ -27,7 +28,11 @@ export default function DataTable({
|
|||||||
return (
|
return (
|
||||||
<AnimatedRow
|
<AnimatedRow
|
||||||
key={label}
|
key={label}
|
||||||
label={renderLabel ? renderLabel(row) : label}
|
label={
|
||||||
|
renderLabel
|
||||||
|
? renderLabel(row)
|
||||||
|
: label ?? <FormattedMessage id="label.unknown" defaultMessage="Unknown" />
|
||||||
|
}
|
||||||
value={value}
|
value={value}
|
||||||
percent={percent}
|
percent={percent}
|
||||||
animate={animate && !virtualize}
|
animate={animate && !virtualize}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MetricsTable from './MetricsTable';
|
import MetricsTable from './MetricsTable';
|
||||||
import { deviceFilter } from 'lib/filters';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { getDeviceMessage } from 'components/messages';
|
import { getDeviceMessage } from 'components/messages';
|
||||||
|
|
||||||
@ -12,7 +11,6 @@ export default function DevicesTable({ websiteId, ...props }) {
|
|||||||
type="device"
|
type="device"
|
||||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
dataFilter={deviceFilter}
|
|
||||||
renderLabel={({ x }) => getDeviceMessage(x)}
|
renderLabel={({ x }) => getDeviceMessage(x)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,18 +1,46 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import MetricsTable from './MetricsTable';
|
import MetricsTable from './MetricsTable';
|
||||||
import Tag from 'components/common/Tag';
|
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 }) {
|
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 (
|
return (
|
||||||
<MetricsTable
|
<>
|
||||||
{...props}
|
{eventTypes?.length > 1 && (
|
||||||
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
|
<div className={styles.filter}>
|
||||||
type="event"
|
<DropDown value={eventType} options={dropDownOptions} onChange={setEventType} />
|
||||||
metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />}
|
</div>
|
||||||
websiteId={websiteId}
|
)}
|
||||||
renderLabel={({ x }) => <Label value={x} />}
|
<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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
components/metrics/EventsTable.module.css
Normal file
6
components/metrics/EventsTable.module.css
Normal file
@ -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) {
|
@media only screen and (max-width: 992px) {
|
||||||
.bar {
|
.bar {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
overflow: auto;
|
||||||
.bar > div:nth-child(n + 3) {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MetricsTable from './MetricsTable';
|
import MetricsTable from './MetricsTable';
|
||||||
import { osFilter } from 'lib/filters';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
export default function OSTable({ websiteId, ...props }) {
|
export default function OSTable({ websiteId, ...props }) {
|
||||||
@ -11,7 +10,6 @@ export default function OSTable({ websiteId, ...props }) {
|
|||||||
type="os"
|
type="os"
|
||||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
dataFilter={osFilter}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,12 +10,23 @@ import Arrow from 'assets/arrow-right.svg';
|
|||||||
import styles from './WebsiteHeader.module.css';
|
import styles from './WebsiteHeader.module.css';
|
||||||
|
|
||||||
export default function WebsiteHeader({ websiteId, title, domain, showLink = false }) {
|
export default function WebsiteHeader({ websiteId, title, domain, showLink = false }) {
|
||||||
|
const header = showLink ? (
|
||||||
|
<>
|
||||||
|
<Favicon domain={domain} />
|
||||||
|
<Link href="/website/[...id]" as={`/website/${websiteId}/${title}`}>
|
||||||
|
{title}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<Favicon domain={domain} />
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>{header}</div>
|
||||||
<Favicon domain={domain} />
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
<ActiveUsers className={styles.active} websiteId={websiteId} />
|
<ActiveUsers className={styles.active} websiteId={websiteId} />
|
||||||
<ButtonLayout align="right">
|
<ButtonLayout align="right">
|
||||||
<RefreshButton websiteId={websiteId} />
|
<RefreshButton websiteId={websiteId} />
|
||||||
|
@ -29,14 +29,13 @@ export default function AccountSettings() {
|
|||||||
|
|
||||||
const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null);
|
const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null);
|
||||||
|
|
||||||
const DashboardLink = row =>
|
const DashboardLink = row => (
|
||||||
row.is_admin ? null : (
|
<Link href={`/dashboard/${row.user_id}/${row.username}`}>
|
||||||
<Link href={`/dashboard/${row.user_id}/${row.username}`}>
|
<a>
|
||||||
<a>
|
<Icon icon={<LinkIcon />} />
|
||||||
<Icon icon={<LinkIcon />} />
|
</a>
|
||||||
</a>
|
</Link>
|
||||||
</Link>
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const Buttons = row =>
|
const Buttons = row =>
|
||||||
row.username !== 'admin' ? (
|
row.username !== 'admin' ? (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.menu {
|
.menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
min-width: 500px;
|
min-width: 560px;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"label.administrator": "Administrator",
|
"label.administrator": "Administrator",
|
||||||
"label.all": "Alle",
|
"label.all": "Alle",
|
||||||
"label.all-websites": "Alle Webseiten",
|
"label.all-websites": "Alle Webseiten",
|
||||||
|
"label.all-events": "Alle Ereignisse",
|
||||||
"label.back": "Zurück",
|
"label.back": "Zurück",
|
||||||
"label.cancel": "Abbrechen",
|
"label.cancel": "Abbrechen",
|
||||||
"label.change-password": "Passwort ändern",
|
"label.change-password": "Passwort ändern",
|
||||||
@ -38,7 +39,7 @@
|
|||||||
"label.passwords-dont-match": "Passwörter stimmen nicht überein",
|
"label.passwords-dont-match": "Passwörter stimmen nicht überein",
|
||||||
"label.profile": "Profil",
|
"label.profile": "Profil",
|
||||||
"label.realtime": "Echtzeit",
|
"label.realtime": "Echtzeit",
|
||||||
"label.realtime-logs": "Echtzeit Logs",
|
"label.realtime-logs": "Echtzeit-Protokoll",
|
||||||
"label.refresh": "Aktualisieren",
|
"label.refresh": "Aktualisieren",
|
||||||
"label.required": "Erforderlich",
|
"label.required": "Erforderlich",
|
||||||
"label.reset": "Zurücksetzen",
|
"label.reset": "Zurücksetzen",
|
||||||
@ -57,23 +58,23 @@
|
|||||||
"label.view-details": "Details anzeigen",
|
"label.view-details": "Details anzeigen",
|
||||||
"label.websites": "Webseiten",
|
"label.websites": "Webseiten",
|
||||||
"message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}",
|
"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.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.failure": "Es ist ein Fehler aufgetreten.",
|
||||||
"message.get-share-url": "Freigabe-URL abrufen",
|
"message.get-share-url": "Freigabe-URL abrufen",
|
||||||
"message.get-tracking-code": "Erstelle Tracking Kennung",
|
"message.get-tracking-code": "Erstelle Tracking Kennung",
|
||||||
"message.go-to-settings": "Zu den Einstellungen",
|
"message.go-to-settings": "Zu den Einstellungen",
|
||||||
"message.incorrect-username-password": "Falsches Passwort oder Benutzername.",
|
"message.incorrect-username-password": "Falsches Passwort oder Benutzername.",
|
||||||
"message.log.visitor": "Besucher aus {country} benutzt {browser} auf {os} {device}",
|
"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-data-available": "Keine Daten vorhanden.",
|
||||||
"message.no-websites-configured": "Es ist keine Webseite vorhanden.",
|
"message.no-websites-configured": "Es ist keine Webseite vorhanden.",
|
||||||
"message.page-not-found": "Seite nicht gefunden.",
|
"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.save-success": "Erfolgreich gespeichert.",
|
||||||
"message.share-url": "Dies ist die öffentliche URL zum Teilen für {target}.",
|
"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.",
|
"message.type-delete": "Geben Sie {delete} in das Feld unten ein um zu bestätigen.",
|
||||||
"metrics.actions": "Aktionen",
|
"metrics.actions": "Aktionen",
|
||||||
"metrics.average-visit-time": "Durchschn. Besuchszeit",
|
"metrics.average-visit-time": "Durchschn. Besuchszeit",
|
||||||
@ -92,7 +93,7 @@
|
|||||||
"metrics.operating-systems": "Betriebssysteme",
|
"metrics.operating-systems": "Betriebssysteme",
|
||||||
"metrics.page-views": "Seitenaufrufe",
|
"metrics.page-views": "Seitenaufrufe",
|
||||||
"metrics.pages": "Seiten",
|
"metrics.pages": "Seiten",
|
||||||
"metrics.referrers": "Referrers",
|
"metrics.referrers": "Referrer",
|
||||||
"metrics.unique-visitors": "Eindeutige Besucher",
|
"metrics.unique-visitors": "Eindeutige Besucher",
|
||||||
"metrics.views": "Aufrufe",
|
"metrics.views": "Aufrufe",
|
||||||
"metrics.visitors": "Besucher"
|
"metrics.visitors": "Besucher"
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"label.administrator": "Administrator",
|
"label.administrator": "Administrator",
|
||||||
"label.all": "All",
|
"label.all": "All",
|
||||||
"label.all-websites": "All websites",
|
"label.all-websites": "All websites",
|
||||||
|
"label.all-events": "All events",
|
||||||
"label.back": "Back",
|
"label.back": "Back",
|
||||||
"label.cancel": "Cancel",
|
"label.cancel": "Cancel",
|
||||||
"label.change-password": "Change password",
|
"label.change-password": "Change password",
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"label.administrator": "Administrador",
|
"label.administrator": "Administrador",
|
||||||
"label.all": "Todos",
|
"label.all": "Todos",
|
||||||
"label.all-websites": "Todos los sitios",
|
"label.all-websites": "Todos los sitios",
|
||||||
|
"label.all-events": "Todos los eventos",
|
||||||
"label.back": "Atrás",
|
"label.back": "Atrás",
|
||||||
"label.cancel": "Cancelar",
|
"label.cancel": "Cancelar",
|
||||||
"label.change-password": "Cambiar contraseña",
|
"label.change-password": "Cambiar contraseña",
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"label.administrator": "Administrateur",
|
"label.administrator": "Administrateur",
|
||||||
"label.all": "Tout",
|
"label.all": "Tout",
|
||||||
"label.all-websites": "Tous les sites web",
|
"label.all-websites": "Tous les sites web",
|
||||||
|
"label.all-events": "Tous les événements",
|
||||||
"label.back": "Retour",
|
"label.back": "Retour",
|
||||||
"label.cancel": "Annuler",
|
"label.cancel": "Annuler",
|
||||||
"label.change-password": "Changer le mot de passe",
|
"label.change-password": "Changer le mot de passe",
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"label.login": "Zaloguj sie",
|
"label.login": "Zaloguj sie",
|
||||||
"label.logout": "Wyloguj",
|
"label.logout": "Wyloguj",
|
||||||
"label.more": "Więcej",
|
"label.more": "Więcej",
|
||||||
"label.name": "Name",
|
"label.name": "Nazwa",
|
||||||
"label.new-password": "Nowe hasło",
|
"label.new-password": "Nowe hasło",
|
||||||
"label.password": "Hasło",
|
"label.password": "Hasło",
|
||||||
"label.passwords-dont-match": "Hasła się nie zgadzają",
|
"label.passwords-dont-match": "Hasła się nie zgadzają",
|
||||||
|
99
lang/pt-BR.json
Normal file
99
lang/pt-BR.json
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
{
|
||||||
|
"label.accounts": "Contas",
|
||||||
|
"label.add-account": "Adicionar conta",
|
||||||
|
"label.add-website": "Adicionar site",
|
||||||
|
"label.administrator": "Administrador",
|
||||||
|
"label.all": "Todos",
|
||||||
|
"label.all-websites": "Todos os sites",
|
||||||
|
"label.back": "Voltar",
|
||||||
|
"label.cancel": "Cancelar",
|
||||||
|
"label.change-password": "Alterar a senha",
|
||||||
|
"label.confirm-password": "Confirme a nova senha",
|
||||||
|
"label.copy-to-clipboard": "Copiar para a área de transferência",
|
||||||
|
"label.current-password": "Senha atual",
|
||||||
|
"label.custom-range": "Intervalo personalizado",
|
||||||
|
"label.dashboard": "Painel",
|
||||||
|
"label.date-range": "Intervalo de datas",
|
||||||
|
"label.default-date-range": "Intervalo de datas predefinido",
|
||||||
|
"label.delete": "Remover",
|
||||||
|
"label.delete-account": "Remover conta",
|
||||||
|
"label.delete-website": "Remover site",
|
||||||
|
"label.dismiss": "Dispensar",
|
||||||
|
"label.domain": "Domínio",
|
||||||
|
"label.edit": "Editar",
|
||||||
|
"label.edit-account": "Editar conta",
|
||||||
|
"label.edit-website": "Editar site",
|
||||||
|
"label.enable-share-url": "Ativar link de compartilhamento",
|
||||||
|
"label.invalid": "Inválido",
|
||||||
|
"label.invalid-domain": "Domínio inválido",
|
||||||
|
"label.last-days": "Últimos {x} dias",
|
||||||
|
"label.last-hours": "Últimas {x} horas",
|
||||||
|
"label.logged-in-as": "Sessão iniciada como {username}",
|
||||||
|
"label.login": "Iniciar sessão",
|
||||||
|
"label.logout": "Sair",
|
||||||
|
"label.more": "Mais",
|
||||||
|
"label.name": "Nome",
|
||||||
|
"label.new-password": "Nova senha",
|
||||||
|
"label.password": "Senha",
|
||||||
|
"label.passwords-dont-match": "As senhas não correspondem",
|
||||||
|
"label.profile": "Perfil",
|
||||||
|
"label.realtime": "Tempo real",
|
||||||
|
"label.realtime-logs": "Relatório em tempo real",
|
||||||
|
"label.refresh": "Atualizar",
|
||||||
|
"label.required": "Obrigatório",
|
||||||
|
"label.reset": "Redefinir",
|
||||||
|
"label.save": "Salvar",
|
||||||
|
"label.settings": "Configurações",
|
||||||
|
"label.share-url": "Link de compartilhamento",
|
||||||
|
"label.single-day": "Dia específico",
|
||||||
|
"label.this-month": "Este mês",
|
||||||
|
"label.this-week": "Esta semana",
|
||||||
|
"label.this-year": "Este ano",
|
||||||
|
"label.timezone": "Fuso horário",
|
||||||
|
"label.today": "Hoje",
|
||||||
|
"label.tracking-code": "Código de rastreamento",
|
||||||
|
"label.unknown": "Desconhecido",
|
||||||
|
"label.username": "Nome de usuário",
|
||||||
|
"label.view-details": "Ver detalhes",
|
||||||
|
"label.websites": "Sites",
|
||||||
|
"message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento",
|
||||||
|
"message.confirm-delete": "Deseja realmente remover {target}?",
|
||||||
|
"message.copied": "Copiado!",
|
||||||
|
"message.delete-warning": "Todos os dados associados também serão eliminados.",
|
||||||
|
"message.failure": "Ocorreu um erro.",
|
||||||
|
"message.get-share-url": "Obter link de compartilhamento",
|
||||||
|
"message.get-tracking-code": "Obter código de rastreamento",
|
||||||
|
"message.go-to-settings": "Ir para as configurações",
|
||||||
|
"message.incorrect-username-password": "O nome de usuário e/ou senha está incorreto.",
|
||||||
|
"message.log.visitor": "Visitante de {country} usando {browser} no {device} {os}",
|
||||||
|
"message.new-version-available": "Uma nova versão de umami {version} está disponível!",
|
||||||
|
"message.no-data-available": "Sem dados disponíveis.",
|
||||||
|
"message.no-websites-configured": "Nenhum site foi configurado ainda.",
|
||||||
|
"message.page-not-found": "Página não encontrada.",
|
||||||
|
"message.powered-by": "Distribuído por {name}",
|
||||||
|
"message.save-success": "Salvo com sucesso.",
|
||||||
|
"message.share-url": "Este é o link público de compartilhamento para {target}.",
|
||||||
|
"message.track-stats": "Para gerar estatística para {target}, coloque o seguinte código no {head} do html do seu site.",
|
||||||
|
"message.type-delete": "Escreva {delete} abaixo para continuar.",
|
||||||
|
"metrics.actions": "Ações",
|
||||||
|
"metrics.average-visit-time": "Tempo médio da visita",
|
||||||
|
"metrics.bounce-rate": "Taxa de rejeição",
|
||||||
|
"metrics.browsers": "Navegadores",
|
||||||
|
"metrics.countries": "Países",
|
||||||
|
"metrics.device.desktop": "Computador",
|
||||||
|
"metrics.device.laptop": "Notebook",
|
||||||
|
"metrics.device.mobile": "Celular",
|
||||||
|
"metrics.device.tablet": "Tablet",
|
||||||
|
"metrics.devices": "Dispositivos",
|
||||||
|
"metrics.events": "Eventos",
|
||||||
|
"metrics.filter.combined": "Combinado",
|
||||||
|
"metrics.filter.domain-only": "Apenas domínio",
|
||||||
|
"metrics.filter.raw": "Dados brutos",
|
||||||
|
"metrics.operating-systems": "Sistemas operacionais",
|
||||||
|
"metrics.page-views": "Visualizações de página",
|
||||||
|
"metrics.pages": "Páginas",
|
||||||
|
"metrics.referrers": "Referências",
|
||||||
|
"metrics.unique-visitors": "Visitantes únicos",
|
||||||
|
"metrics.views": "Visualizações",
|
||||||
|
"metrics.visitors": "Visitantes"
|
||||||
|
}
|
@ -13,8 +13,8 @@
|
|||||||
"label.current-password": "目前密码",
|
"label.current-password": "目前密码",
|
||||||
"label.custom-range": "自定义时间段",
|
"label.custom-range": "自定义时间段",
|
||||||
"label.dashboard": "仪表板",
|
"label.dashboard": "仪表板",
|
||||||
"label.date-range": "多日",
|
"label.date-range": "时间段",
|
||||||
"label.default-date-range": "默认日期范围",
|
"label.default-date-range": "默认时间段",
|
||||||
"label.delete": "删除",
|
"label.delete": "删除",
|
||||||
"label.delete-account": "删除账户",
|
"label.delete-account": "删除账户",
|
||||||
"label.delete-website": "删除网站",
|
"label.delete-website": "删除网站",
|
||||||
@ -23,7 +23,7 @@
|
|||||||
"label.edit": "编辑",
|
"label.edit": "编辑",
|
||||||
"label.edit-account": "编辑账户",
|
"label.edit-account": "编辑账户",
|
||||||
"label.edit-website": "编辑网站",
|
"label.edit-website": "编辑网站",
|
||||||
"label.enable-share-url": "激活共享链接",
|
"label.enable-share-url": "启用共享链接",
|
||||||
"label.invalid": "输入无效",
|
"label.invalid": "输入无效",
|
||||||
"label.invalid-domain": "无效域名",
|
"label.invalid-domain": "无效域名",
|
||||||
"label.last-days": "最近 {x} 天",
|
"label.last-days": "最近 {x} 天",
|
||||||
@ -57,30 +57,30 @@
|
|||||||
"label.view-details": "查看更多",
|
"label.view-details": "查看更多",
|
||||||
"label.websites": "网站",
|
"label.websites": "网站",
|
||||||
"message.active-users": "当前在线 {x} 人",
|
"message.active-users": "当前在线 {x} 人",
|
||||||
"message.confirm-delete": "你确定要删除{target}吗?",
|
"message.confirm-delete": "你确定要删除 {target} 吗?",
|
||||||
"message.copied": "复制成功!",
|
"message.copied": "复制成功!",
|
||||||
"message.delete-warning": "所有相关数据将会被删除.",
|
"message.delete-warning": "所有相关数据将会被删除。",
|
||||||
"message.failure": "出现错误.",
|
"message.failure": "出现错误。",
|
||||||
"message.get-share-url": "获得共享链接",
|
"message.get-share-url": "获取共享链接",
|
||||||
"message.get-tracking-code": "获得跟踪代码",
|
"message.get-tracking-code": "获取跟踪代码",
|
||||||
"message.go-to-settings": "去设置",
|
"message.go-to-settings": "去设置",
|
||||||
"message.incorrect-username-password": "用户名密码不正确.",
|
"message.incorrect-username-password": "用户名或密码不正确。",
|
||||||
"message.log.visitor": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 进行访问.",
|
"message.log.visitor": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 浏览器进行访问。",
|
||||||
"message.new-version-available": "umami 有新版本 {version} 发布啦!",
|
"message.new-version-available": "umami 有新版本 {version} 发布啦!",
|
||||||
"message.no-data-available": "无可用数据.",
|
"message.no-data-available": "无可用数据。",
|
||||||
"message.no-websites-configured": "你还没有设置任何网站.",
|
"message.no-websites-configured": "你还没有设置任何网站。",
|
||||||
"message.page-not-found": "网页未找到.",
|
"message.page-not-found": "网页未找到。",
|
||||||
"message.powered-by": "运行 {name}",
|
"message.powered-by": "由 {name} 提供支持",
|
||||||
"message.save-success": "成功保存.",
|
"message.save-success": "保存成功。",
|
||||||
"message.share-url": "这是 {target} 的共享链接.",
|
"message.share-url": "这是 {target} 的共享链接。",
|
||||||
"message.track-stats": "把以下代码放到你的网站的{head}部分来收集{target}的数据.",
|
"message.track-stats": "把以下代码放到你的网站的 {head} 部分来收集 {target} 的数据。",
|
||||||
"message.type-delete": "在下面空格输入{delete}确认",
|
"message.type-delete": "在下方输入框输入 {delete} 以确认删除。",
|
||||||
"metrics.actions": "用户行为",
|
"metrics.actions": "用户行为",
|
||||||
"metrics.average-visit-time": "平均访问时间",
|
"metrics.average-visit-time": "平均访问时间",
|
||||||
"metrics.bounce-rate": "跳出率",
|
"metrics.bounce-rate": "跳出率",
|
||||||
"metrics.browsers": "浏览器",
|
"metrics.browsers": "浏览器",
|
||||||
"metrics.countries": "国家",
|
"metrics.countries": "国家",
|
||||||
"metrics.device.desktop": "台式机",
|
"metrics.device.desktop": "桌面电脑",
|
||||||
"metrics.device.laptop": "笔记本",
|
"metrics.device.laptop": "笔记本",
|
||||||
"metrics.device.mobile": "手机",
|
"metrics.device.mobile": "手机",
|
||||||
"metrics.device.tablet": "平板",
|
"metrics.device.tablet": "平板",
|
||||||
@ -90,10 +90,10 @@
|
|||||||
"metrics.filter.domain-only": "只看域名",
|
"metrics.filter.domain-only": "只看域名",
|
||||||
"metrics.filter.raw": "原始",
|
"metrics.filter.raw": "原始",
|
||||||
"metrics.operating-systems": "操作系统",
|
"metrics.operating-systems": "操作系统",
|
||||||
"metrics.page-views": "页面流量",
|
"metrics.page-views": "页面浏览量",
|
||||||
"metrics.pages": "网页",
|
"metrics.pages": "网页",
|
||||||
"metrics.referrers": "指入域名",
|
"metrics.referrers": "来源域名",
|
||||||
"metrics.unique-visitors": "独立访客",
|
"metrics.unique-visitors": "独立访客",
|
||||||
"metrics.views": "页面流量",
|
"metrics.views": "浏览量",
|
||||||
"metrics.visitors": "独立访客"
|
"metrics.visitors": "访客"
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ export const POSTGRESQL_DATE_FORMATS = {
|
|||||||
year: 'YYYY-01-01',
|
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(:\d{1,5})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}$/;
|
||||||
|
|
||||||
export const DESKTOP_SCREEN_WIDTH = 1920;
|
export const DESKTOP_SCREEN_WIDTH = 1920;
|
||||||
export const LAPTOP_SCREEN_WIDTH = 1024;
|
export const LAPTOP_SCREEN_WIDTH = 1024;
|
||||||
@ -116,7 +116,7 @@ export const BROWSERS = {
|
|||||||
edge: 'Edge',
|
edge: 'Edge',
|
||||||
'edge-ios': 'Edge (iOS)',
|
'edge-ios': 'Edge (iOS)',
|
||||||
yandexbrowser: 'Yandex',
|
yandexbrowser: 'Yandex',
|
||||||
kakaotalk: 'KKaoTalk',
|
kakaotalk: 'KaKaoTalk',
|
||||||
samsung: 'Samsung',
|
samsung: 'Samsung',
|
||||||
silk: 'Silk',
|
silk: 'Silk',
|
||||||
miui: 'MIUI',
|
miui: 'MIUI',
|
||||||
|
@ -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] }));
|
return Object.keys(map).map(key => ({ x: key, y: map[key], w: links[key] }));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const browserFilter = data =>
|
export const browserFilter = data => data.map(({ x, y }) => ({ x: BROWSERS[x] ?? x, y }));
|
||||||
data.map(({ x, y }) => ({ x: BROWSERS[x] || x, y })).filter(({ x }) => x);
|
|
||||||
|
|
||||||
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 => {
|
export const percentFilter = data => {
|
||||||
const total = data.reduce((n, { y }) => n + y, 0);
|
const total = data.reduce((n, { y }) => n + y, 0);
|
||||||
|
14
lib/lang.js
14
lib/lang.js
@ -17,6 +17,7 @@ import {
|
|||||||
nl,
|
nl,
|
||||||
pl,
|
pl,
|
||||||
pt,
|
pt,
|
||||||
|
ptBR,
|
||||||
ro,
|
ro,
|
||||||
ru,
|
ru,
|
||||||
sv,
|
sv,
|
||||||
@ -42,6 +43,7 @@ import svMessages from 'lang-compiled/sv-SE.json';
|
|||||||
import grMessages from 'lang-compiled/el-GR.json';
|
import grMessages from 'lang-compiled/el-GR.json';
|
||||||
import foMessages from 'lang-compiled/fo-FO.json';
|
import foMessages from 'lang-compiled/fo-FO.json';
|
||||||
import ptMessages from 'lang-compiled/pt-PT.json';
|
import ptMessages from 'lang-compiled/pt-PT.json';
|
||||||
|
import ptBRMessages from 'lang-compiled/pt-BR.json';
|
||||||
import roMessages from 'lang-compiled/ro-RO.json';
|
import roMessages from 'lang-compiled/ro-RO.json';
|
||||||
import nbNOMessages from 'lang-compiled/nb-NO.json';
|
import nbNOMessages from 'lang-compiled/nb-NO.json';
|
||||||
import idMessages from 'lang-compiled/id-ID.json';
|
import idMessages from 'lang-compiled/id-ID.json';
|
||||||
@ -71,6 +73,7 @@ export const messages = {
|
|||||||
'el-GR': grMessages,
|
'el-GR': grMessages,
|
||||||
'fo-FO': foMessages,
|
'fo-FO': foMessages,
|
||||||
'pt-PT': ptMessages,
|
'pt-PT': ptMessages,
|
||||||
|
'pt-BR': ptBRMessages,
|
||||||
'ro-RO': roMessages,
|
'ro-RO': roMessages,
|
||||||
'nb-NO': nbNOMessages,
|
'nb-NO': nbNOMessages,
|
||||||
'id-ID': idMessages,
|
'id-ID': idMessages,
|
||||||
@ -101,6 +104,7 @@ export const dateLocales = {
|
|||||||
'el-GR': el,
|
'el-GR': el,
|
||||||
'fo-FO': da,
|
'fo-FO': da,
|
||||||
'pt-PT': pt,
|
'pt-PT': pt,
|
||||||
|
'pt-BR': ptBR,
|
||||||
'ro-RO': ro,
|
'ro-RO': ro,
|
||||||
'nb-NO': nb,
|
'nb-NO': nb,
|
||||||
'id-ID': id,
|
'id-ID': id,
|
||||||
@ -114,6 +118,11 @@ export const dateLocales = {
|
|||||||
'it-IT': it,
|
'it-IT': it,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const timeFormats = {
|
||||||
|
// https://date-fns.org/v2.17.0/docs/format
|
||||||
|
'en-US': 'ha',
|
||||||
|
};
|
||||||
|
|
||||||
export const menuOptions = [
|
export const menuOptions = [
|
||||||
{ label: '中文', value: 'zh-CN', display: 'cn' },
|
{ label: '中文', value: 'zh-CN', display: 'cn' },
|
||||||
{ label: '中文(繁體)', value: 'zh-TW', display: 'tw' },
|
{ label: '中文(繁體)', value: 'zh-TW', display: 'tw' },
|
||||||
@ -135,6 +144,7 @@ export const menuOptions = [
|
|||||||
{ label: 'Norsk Bokmål', value: 'nb-NO', display: 'nb' },
|
{ label: 'Norsk Bokmål', value: 'nb-NO', display: 'nb' },
|
||||||
{ label: 'Polski', value: 'pl-PL', display: 'pl' },
|
{ label: 'Polski', value: 'pl-PL', display: 'pl' },
|
||||||
{ label: 'Português', value: 'pt-PT', display: 'pt' },
|
{ label: 'Português', value: 'pt-PT', display: 'pt' },
|
||||||
|
{ label: 'Português do Brasil', value: 'pt-BR', display: 'pt-BR' },
|
||||||
{ label: 'Русский', value: 'ru-RU', display: 'ru' },
|
{ label: 'Русский', value: 'ru-RU', display: 'ru' },
|
||||||
{ label: 'Română', value: 'ro-RO', display: 'ro' },
|
{ label: 'Română', value: 'ro-RO', display: 'ro' },
|
||||||
{ label: 'Suomi', value: 'fi-FI', display: 'fi' },
|
{ label: 'Suomi', value: 'fi-FI', display: 'fi' },
|
||||||
@ -147,3 +157,7 @@ export const menuOptions = [
|
|||||||
export function dateFormat(date, str, locale) {
|
export function dateFormat(date, str, locale) {
|
||||||
return format(date, str, { locale: dateLocales[locale] || enUS });
|
return format(date, str, { locale: dateLocales[locale] || enUS });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function timeFormat(date, locale = 'en-US') {
|
||||||
|
return format(date, timeFormats[locale] || 'p', { locale: dateLocales[locale] });
|
||||||
|
}
|
||||||
|
28
package.json
28
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "1.11.0",
|
"version": "1.13.0",
|
||||||
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
||||||
"author": "Mike Cao <mike@mikecao.com>",
|
"author": "Mike Cao <mike@mikecao.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -56,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "2.14.0",
|
"@prisma/client": "2.17.0",
|
||||||
"@reduxjs/toolkit": "^1.5.0",
|
"@reduxjs/toolkit": "^1.5.0",
|
||||||
"bcrypt": "^5.0.0",
|
"bcrypt": "^5.0.0",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
@ -69,21 +69,21 @@
|
|||||||
"detect-browser": "^5.2.0",
|
"detect-browser": "^5.2.0",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"formik": "^2.2.6",
|
"formik": "^2.2.6",
|
||||||
"immer": "^8.0.0",
|
"immer": "^8.0.1",
|
||||||
"is-localhost-ip": "^1.4.0",
|
"is-localhost-ip": "^1.4.0",
|
||||||
"isbot-fast": "^1.2.0",
|
"isbot-fast": "^1.2.0",
|
||||||
"jose": "2.0.3",
|
"jose": "2.0.3",
|
||||||
"maxmind": "^4.3.1",
|
"maxmind": "^4.3.1",
|
||||||
"moment-timezone": "^0.5.32",
|
"moment-timezone": "^0.5.32",
|
||||||
"next": "^10.0.5",
|
"next": "^10.0.7",
|
||||||
"prompts": "2.4.0",
|
"prompts": "2.4.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-intl": "^5.10.16",
|
"react-intl": "^5.12.3",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.2",
|
||||||
"react-simple-maps": "^2.3.0",
|
"react-simple-maps": "^2.3.0",
|
||||||
"react-spring": "^8.0.27",
|
"react-spring": "^8.0.27",
|
||||||
"react-tooltip": "^4.2.11",
|
"react-tooltip": "^4.2.14",
|
||||||
"react-use-measure": "^2.0.3",
|
"react-use-measure": "^2.0.3",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
@ -97,22 +97,22 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formatjs/cli": "^2.13.16",
|
"@formatjs/cli": "^2.13.16",
|
||||||
"@prisma/cli": "2.14.0",
|
"@prisma/cli": "2.17.0",
|
||||||
"@rollup/plugin-buble": "^0.21.3",
|
"@rollup/plugin-buble": "^0.21.3",
|
||||||
"@rollup/plugin-node-resolve": "^11.0.1",
|
"@rollup/plugin-node-resolve": "^11.1.1",
|
||||||
"@rollup/plugin-replace": "^2.3.4",
|
"@rollup/plugin-replace": "^2.3.4",
|
||||||
"@svgr/webpack": "^5.5.0",
|
"@svgr/webpack": "^5.5.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"del": "^6.0.0",
|
"del": "^6.0.0",
|
||||||
"dotenv-cli": "^4.0.0",
|
"dotenv-cli": "^4.0.0",
|
||||||
"eslint": "^7.17.0",
|
"eslint": "^7.20.0",
|
||||||
"eslint-config-prettier": "^7.1.0",
|
"eslint-config-prettier": "^7.2.0",
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
"eslint-plugin-react": "^7.22.0",
|
"eslint-plugin-react": "^7.22.0",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"extract-react-intl-messages": "^4.1.1",
|
"extract-react-intl-messages": "^4.1.1",
|
||||||
"husky": "^4.3.7",
|
"husky": "^4.3.8",
|
||||||
"lint-staged": "^10.5.3",
|
"lint-staged": "^10.5.4",
|
||||||
"loadtest": "5.1.2",
|
"loadtest": "5.1.2",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss-flexbugs-fixes": "^5.0.2",
|
"postcss-flexbugs-fixes": "^5.0.2",
|
||||||
@ -120,10 +120,10 @@
|
|||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-preset-env": "^6.7.0",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"prettier-eslint": "^12.0.0",
|
"prettier-eslint": "^12.0.0",
|
||||||
"rollup": "^2.36.1",
|
"rollup": "^2.38.3",
|
||||||
"rollup-plugin-hashbang": "^2.2.2",
|
"rollup-plugin-hashbang": "^2.2.2",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"stylelint": "^13.8.0",
|
"stylelint": "^13.10.0",
|
||||||
"stylelint-config-css-modules": "^2.2.0",
|
"stylelint-config-css-modules": "^2.2.0",
|
||||||
"stylelint-config-prettier": "^8.0.1",
|
"stylelint-config-prettier": "^8.0.1",
|
||||||
"stylelint-config-recommended": "^3.0.0",
|
"stylelint-config-recommended": "^3.0.0",
|
||||||
|
@ -36,6 +36,7 @@ export default function App({ Component, pageProps }) {
|
|||||||
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#5bbad5" />
|
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#5bbad5" />
|
||||||
<meta name="msapplication-TileColor" content="#da532c" />
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
</Head>
|
</Head>
|
||||||
<Intl>
|
<Intl>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
1
public/country/pt-BR.json
Normal file
1
public/country/pt-BR.json
Normal file
File diff suppressed because one or more lines are too long
@ -25,7 +25,7 @@ const commandlineOptions = {
|
|||||||
maxRequests: 1,
|
maxRequests: 1,
|
||||||
},
|
},
|
||||||
// Heavy can saturate CPU which leads to requests stalling depending on machine
|
// Heavy can saturate CPU which leads to requests stalling depending on machine
|
||||||
// Keep an eye if --verbose logs pause, or if node CPU in top is > 100.
|
// Keep an eye if --verbose logs pause, or if node CPU in top is > 100.
|
||||||
// https://github.com/alexfernandez/loadtest#usage-donts
|
// https://github.com/alexfernandez/loadtest#usage-donts
|
||||||
heavy: {
|
heavy: {
|
||||||
concurrency: 10,
|
concurrency: 10,
|
||||||
@ -51,7 +51,6 @@ const options = {
|
|||||||
const message = JSON.stringify(mockPageView());
|
const message = JSON.stringify(mockPageView());
|
||||||
options.headers['Content-Length'] = message.length;
|
options.headers['Content-Length'] = message.length;
|
||||||
options.headers['Content-Type'] = 'application/json';
|
options.headers['Content-Type'] = 'application/json';
|
||||||
options.headers['user-agent'] = 'User-Agent: Mozilla/5.0 LoadTest';
|
|
||||||
options.body = message;
|
options.body = message;
|
||||||
options.path = '/api/collect';
|
options.path = '/api/collect';
|
||||||
const request = client(options, callback);
|
const request = client(options, callback);
|
||||||
@ -95,15 +94,14 @@ loadtest.loadTest(options, (error, results) => {
|
|||||||
if (results.errorCodes && Object.keys(results.errorCodes).length) {
|
if (results.errorCodes && Object.keys(results.errorCodes).length) {
|
||||||
console.log(chalk.redBright('*'), chalk.red('Error Codes:'), results.errorCodes);
|
console.log(chalk.redBright('*'), chalk.red('Error Codes:'), results.errorCodes);
|
||||||
}
|
}
|
||||||
// console.log(results);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new object for each request. Note, we could randomize values here if desired.
|
* Create a new object for each request. Note, we could randomize values here if desired.
|
||||||
*
|
*
|
||||||
* TODO: Need a better way of passing in websiteId, hostname, URL.
|
* TODO: Need a better way of passing in websiteId, hostname, URL.
|
||||||
*
|
*
|
||||||
* @param {object} payload pageview payload same as sent via tracker
|
* @param {object} payload pageview payload same as sent via tracker
|
||||||
*/
|
*/
|
||||||
function mockPageView(
|
function mockPageView(
|
||||||
payload = {
|
payload = {
|
||||||
@ -121,6 +119,9 @@ function mockPageView(
|
|||||||
|
|
||||||
// If you pass in --verbose, this function is called
|
// If you pass in --verbose, this function is called
|
||||||
function statusCallback(error, result, latency) {
|
function statusCallback(error, result, latency) {
|
||||||
|
if (error) {
|
||||||
|
return console.error(chalk.redBright(error));
|
||||||
|
}
|
||||||
console.log(
|
console.log(
|
||||||
chalk.yellowBright(`\n## req #${result.requestIndex + 1} of ${latency.totalRequests}`),
|
chalk.yellowBright(`\n## req #${result.requestIndex + 1} of ${latency.totalRequests}`),
|
||||||
);
|
);
|
||||||
|
@ -133,6 +133,8 @@ import { removeTrailingSlash } from '../lib/url';
|
|||||||
/* Handle history changes */
|
/* Handle history changes */
|
||||||
|
|
||||||
const handlePush = (state, title, url) => {
|
const handlePush = (state, title, url) => {
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
removeEvents();
|
removeEvents();
|
||||||
|
|
||||||
currentRef = currentUrl;
|
currentRef = currentUrl;
|
||||||
@ -144,7 +146,9 @@ import { removeTrailingSlash } from '../lib/url';
|
|||||||
currentUrl = newUrl;
|
currentUrl = newUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
trackView(currentUrl, currentRef);
|
if (currentUrl !== currentRef) {
|
||||||
|
trackView(currentUrl, currentRef);
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(addEvents, 300);
|
setTimeout(addEvents, 300);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user