Merge remote-tracking branch 'upstream/master'
commit
9069cb830e
|
@ -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);
|
||||
|
@ -29,7 +24,7 @@ export default function DropDown({
|
|||
}
|
||||
|
||||
useDocumentClick(e => {
|
||||
if (!ref.current.contains(e.target)) {
|
||||
if (!ref.current?.contains(e.target)) {
|
||||
setShowMenu(false);
|
||||
}
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -30,7 +31,7 @@ export default function MenuButton({
|
|||
}
|
||||
|
||||
useDocumentClick(e => {
|
||||
if (!ref.current.contains(e.target)) {
|
||||
if (!ref.current?.contains(e.target)) {
|
||||
setShowMenu(false);
|
||||
}
|
||||
});
|
||||
|
@ -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,17 +1,18 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
||||
import classNames from 'classnames';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { THEME_COLORS } from 'lib/constants';
|
||||
import { ISO_COUNTRIES, THEME_COLORS, MAP_FILE } from 'lib/constants';
|
||||
import styles from './WorldMap.module.css';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
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();
|
||||
const colors = useMemo(
|
||||
|
@ -57,10 +58,10 @@ export default function WorldMap({ data, className }) {
|
|||
>
|
||||
<ComposableMap projection="geoMercator">
|
||||
<ZoomableGroup zoom={0.8} minZoom={0.7} center={[0, 40]}>
|
||||
<Geographies geography={geoUrl}>
|
||||
<Geographies geography={`${basePath}${MAP_FILE}`}>
|
||||
{({ geographies }) => {
|
||||
return geographies.map(geo => {
|
||||
const code = geo.properties.ISO_A2;
|
||||
const code = ISO_COUNTRIES[geo.id];
|
||||
|
||||
return (
|
||||
<Geography
|
||||
|
@ -87,3 +88,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;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import { post } from 'lib/web';
|
||||
import Button from 'components/common/Button';
|
||||
import FormLayout, {
|
||||
FormButtons,
|
||||
|
@ -10,6 +8,7 @@ import FormLayout, {
|
|||
FormMessage,
|
||||
FormRow,
|
||||
} from 'components/layout/FormLayout';
|
||||
import usePost from 'hooks/usePost';
|
||||
|
||||
const initialValues = {
|
||||
username: '',
|
||||
|
@ -30,11 +29,11 @@ const validate = ({ user_id, username, password }) => {
|
|||
};
|
||||
|
||||
export default function AccountEditForm({ values, onSave, onClose }) {
|
||||
const { basePath } = useRouter();
|
||||
const post = usePost();
|
||||
const [message, setMessage] = useState();
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const { ok, data } = await post(`${basePath}/api/account`, values);
|
||||
const { ok, data } = await post('/api/account', values);
|
||||
|
||||
if (ok) {
|
||||
onSave();
|
||||
|
@ -58,15 +57,19 @@ export default function AccountEditForm({ values, onSave, onClose }) {
|
|||
<label htmlFor="username">
|
||||
<FormattedMessage id="label.username" defaultMessage="Username" />
|
||||
</label>
|
||||
<Field name="username" type="text" />
|
||||
<FormError name="username" />
|
||||
<div>
|
||||
<Field name="username" type="text" />
|
||||
<FormError name="username" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="password">
|
||||
<FormattedMessage id="label.password" defaultMessage="Password" />
|
||||
</label>
|
||||
<Field name="password" type="password" />
|
||||
<FormError name="password" />
|
||||
<div>
|
||||
<Field name="password" type="password" />
|
||||
<FormError name="password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import { post } from 'lib/web';
|
||||
import Button from 'components/common/Button';
|
||||
import FormLayout, {
|
||||
FormButtons,
|
||||
|
@ -10,6 +8,7 @@ import FormLayout, {
|
|||
FormMessage,
|
||||
FormRow,
|
||||
} from 'components/layout/FormLayout';
|
||||
import usePost from 'hooks/usePost';
|
||||
|
||||
const initialValues = {
|
||||
current_password: '',
|
||||
|
@ -38,11 +37,11 @@ const validate = ({ current_password, new_password, confirm_password }) => {
|
|||
};
|
||||
|
||||
export default function ChangePasswordForm({ values, onSave, onClose }) {
|
||||
const { basePath } = useRouter();
|
||||
const post = usePost();
|
||||
const [message, setMessage] = useState();
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const { ok, data } = await post(`${basePath}/api/account/password`, values);
|
||||
const { ok, data } = await post('/api/account/password', values);
|
||||
|
||||
if (ok) {
|
||||
onSave();
|
||||
|
@ -66,22 +65,28 @@ export default function ChangePasswordForm({ values, onSave, onClose }) {
|
|||
<label htmlFor="current_password">
|
||||
<FormattedMessage id="label.current-password" defaultMessage="Current password" />
|
||||
</label>
|
||||
<Field name="current_password" type="password" />
|
||||
<FormError name="current_password" />
|
||||
<div>
|
||||
<Field name="current_password" type="password" />
|
||||
<FormError name="current_password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="new_password">
|
||||
<FormattedMessage id="label.new-password" defaultMessage="New password" />
|
||||
</label>
|
||||
<Field name="new_password" type="password" />
|
||||
<FormError name="new_password" />
|
||||
<div>
|
||||
<Field name="new_password" type="password" />
|
||||
<FormError name="new_password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="confirm_password">
|
||||
<FormattedMessage id="label.confirm-password" defaultMessage="Confirm password" />
|
||||
</label>
|
||||
<Field name="confirm_password" type="password" />
|
||||
<FormError name="confirm_password" />
|
||||
<div>
|
||||
<Field name="confirm_password" type="password" />
|
||||
<FormError name="confirm_password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import { del } from 'lib/web';
|
||||
import Button from 'components/common/Button';
|
||||
import FormLayout, {
|
||||
FormButtons,
|
||||
|
@ -10,6 +8,7 @@ import FormLayout, {
|
|||
FormMessage,
|
||||
FormRow,
|
||||
} from 'components/layout/FormLayout';
|
||||
import useDelete from 'hooks/useDelete';
|
||||
|
||||
const CONFIRMATION_WORD = 'DELETE';
|
||||
|
||||
|
@ -28,11 +27,11 @@ const validate = ({ confirmation }) => {
|
|||
};
|
||||
|
||||
export default function DeleteForm({ values, onSave, onClose }) {
|
||||
const { basePath } = useRouter();
|
||||
const del = useDelete();
|
||||
const [message, setMessage] = useState();
|
||||
|
||||
const handleSubmit = async ({ type, id }) => {
|
||||
const { ok, data } = await del(`${basePath}/api/${type}/${id}`);
|
||||
const { ok, data } = await del(`/api/${type}/${id}`);
|
||||
|
||||
if (ok) {
|
||||
onSave();
|
||||
|
@ -73,8 +72,10 @@ export default function DeleteForm({ values, onSave, onClose }) {
|
|||
/>
|
||||
</p>
|
||||
<FormRow>
|
||||
<Field name="confirmation" type="text" />
|
||||
<FormError name="confirmation" />
|
||||
<div>
|
||||
<Field name="confirmation" type="text" />
|
||||
<FormError name="confirmation" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button
|
||||
|
|
|
@ -2,7 +2,6 @@ import React, { useState } from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import { post } from 'lib/web';
|
||||
import Button from 'components/common/Button';
|
||||
import FormLayout, {
|
||||
FormButtons,
|
||||
|
@ -13,6 +12,7 @@ import FormLayout, {
|
|||
import Icon from 'components/common/Icon';
|
||||
import Logo from 'assets/logo.svg';
|
||||
import styles from './LoginForm.module.css';
|
||||
import usePost from 'hooks/usePost';
|
||||
|
||||
const validate = ({ username, password }) => {
|
||||
const errors = {};
|
||||
|
@ -28,11 +28,12 @@ const validate = ({ username, password }) => {
|
|||
};
|
||||
|
||||
export default function LoginForm() {
|
||||
const post = usePost();
|
||||
const router = useRouter();
|
||||
const [message, setMessage] = useState();
|
||||
|
||||
const handleSubmit = async ({ username, password }) => {
|
||||
const { ok, status, data } = await post(`${router.basePath}/api/auth/login`, {
|
||||
const { ok, status, data } = await post('/api/auth/login', {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
@ -65,21 +66,27 @@ export default function LoginForm() {
|
|||
>
|
||||
{() => (
|
||||
<Form>
|
||||
<Icon icon={<Logo />} size="xlarge" className={styles.icon} />
|
||||
<h1 className="center">umami</h1>
|
||||
<div className={styles.header}>
|
||||
<Icon icon={<Logo />} size="xlarge" className={styles.icon} />
|
||||
<h1 className="center">umami</h1>
|
||||
</div>
|
||||
<FormRow>
|
||||
<label htmlFor="username">
|
||||
<FormattedMessage id="label.username" defaultMessage="Username" />
|
||||
</label>
|
||||
<Field name="username" type="text" />
|
||||
<FormError name="username" />
|
||||
<div>
|
||||
<Field name="username" type="text" />
|
||||
<FormError name="username" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="password">
|
||||
<FormattedMessage id="label.password" defaultMessage="Password" />
|
||||
</label>
|
||||
<Field name="password" type="password" />
|
||||
<FormError name="password" />
|
||||
<div>
|
||||
<Field name="password" type="password" />
|
||||
<FormError name="password" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
|
|
|
@ -13,3 +13,11 @@
|
|||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useRouter } from 'next/router';
|
||||
import Button from 'components/common/Button';
|
||||
import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout';
|
||||
import CopyButton from 'components/common/CopyButton';
|
||||
|
||||
export default function TrackingCodeForm({ values, onClose }) {
|
||||
const ref = useRef();
|
||||
const { basePath } = useRouter();
|
||||
const { name, share_id } = values;
|
||||
|
||||
return (
|
||||
|
@ -23,7 +25,9 @@ export default function TrackingCodeForm({ values, onClose }) {
|
|||
rows={3}
|
||||
cols={60}
|
||||
spellCheck={false}
|
||||
defaultValue={`${document.location.origin}/share/${share_id}/${encodeURIComponent(name)}`}
|
||||
defaultValue={`${
|
||||
document.location.origin
|
||||
}${basePath}/share/${share_id}/${encodeURIComponent(name)}`}
|
||||
readOnly
|
||||
/>
|
||||
</FormRow>
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import React, { useRef } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useRouter } from 'next/router';
|
||||
import Button from 'components/common/Button';
|
||||
import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout';
|
||||
import CopyButton from 'components/common/CopyButton';
|
||||
|
||||
export default function TrackingCodeForm({ values, onClose }) {
|
||||
const ref = useRef();
|
||||
const { basePath } = useRouter();
|
||||
|
||||
return (
|
||||
<FormLayout>
|
||||
|
@ -22,7 +24,7 @@ export default function TrackingCodeForm({ values, onClose }) {
|
|||
rows={3}
|
||||
cols={60}
|
||||
spellCheck={false}
|
||||
defaultValue={`<script async defer data-website-id="${values.website_uuid}" src="${document.location.origin}/umami.js"></script>`}
|
||||
defaultValue={`<script async defer data-website-id="${values.website_uuid}" src="${document.location.origin}${basePath}/umami.js"></script>`}
|
||||
readOnly
|
||||
/>
|
||||
</FormRow>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import { post } from 'lib/web';
|
||||
import Button from 'components/common/Button';
|
||||
import FormLayout, {
|
||||
FormButtons,
|
||||
|
@ -11,7 +10,7 @@ import FormLayout, {
|
|||
} from 'components/layout/FormLayout';
|
||||
import Checkbox from 'components/common/Checkbox';
|
||||
import { DOMAIN_REGEX } from 'lib/constants';
|
||||
import { useRouter } from 'next/router';
|
||||
import usePost from 'hooks/usePost';
|
||||
|
||||
const initialValues = {
|
||||
name: '',
|
||||
|
@ -35,11 +34,11 @@ const validate = ({ name, domain }) => {
|
|||
};
|
||||
|
||||
export default function WebsiteEditForm({ values, onSave, onClose }) {
|
||||
const { basePath } = useRouter();
|
||||
const post = usePost();
|
||||
const [message, setMessage] = useState();
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const { ok, data } = await post(`${basePath}/api/website`, values);
|
||||
const { ok, data } = await post('/api/website', values);
|
||||
|
||||
if (ok) {
|
||||
onSave();
|
||||
|
@ -63,15 +62,19 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
|||
<label htmlFor="name">
|
||||
<FormattedMessage id="label.name" defaultMessage="Name" />
|
||||
</label>
|
||||
<Field name="name" type="text" />
|
||||
<FormError name="name" />
|
||||
<div>
|
||||
<Field name="name" type="text" />
|
||||
<FormError name="name" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="domain">
|
||||
<FormattedMessage id="label.domain" defaultMessage="Domain" />
|
||||
</label>
|
||||
<Field name="domain" type="text" />
|
||||
<FormError name="domain" />
|
||||
<div>
|
||||
<Field name="domain" type="text" placeholder="example.com" />
|
||||
<FormError name="domain" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label></label>
|
||||
|
|
|
@ -11,10 +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 {
|
||||
|
@ -33,9 +45,9 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
left: calc(100% + 16px);
|
||||
bottom: 0;
|
||||
margin-left: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.msg {
|
||||
|
@ -68,3 +80,15 @@
|
|||
color: var(--gray50);
|
||||
background: var(--gray800);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
.error {
|
||||
align-items: flex-start;
|
||||
top: calc(100% + 7px);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.error:after {
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,6 @@
|
|||
.row > .col {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-left: 0;
|
||||
padding: 0;
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
@ -13,40 +13,66 @@ import styles from './Header.module.css';
|
|||
|
||||
export default function Header() {
|
||||
const user = useSelector(state => state.user);
|
||||
const [active, setActive] = useState(false);
|
||||
|
||||
function handleClick() {
|
||||
setActive(state => !state);
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="container">
|
||||
<nav className="container">
|
||||
{user?.is_admin && <UpdateNotice />}
|
||||
<div className={classNames(styles.header, 'row align-items-center')}>
|
||||
<div className="col-12 col-md-12 col-lg-3">
|
||||
<div className={styles.title}>
|
||||
<Icon icon={<Logo />} size="large" className={styles.logo} />
|
||||
<Link href={user ? '/' : 'https://umami.is'}>umami</Link>
|
||||
<div className={styles.nav}>
|
||||
<div className="">
|
||||
<div className={styles.title}>
|
||||
<Icon icon={<Logo />} size="large" className={styles.logo} />
|
||||
<Link href={user ? '/' : 'https://umami.is'}>umami</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-md-12 col-lg-6">
|
||||
<button
|
||||
onClick={handleClick}
|
||||
role="button"
|
||||
className={styles.burger}
|
||||
aria-label="menu"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{active ? (
|
||||
<div> X </div>
|
||||
) : (
|
||||
<>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{user && (
|
||||
<div className={styles.nav}>
|
||||
<Link href="/dashboard">
|
||||
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
|
||||
</Link>
|
||||
<Link href="/realtime">
|
||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||
</Link>
|
||||
<Link href="/settings">
|
||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||
</Link>
|
||||
<div className={styles.items}>
|
||||
<div className={active ? classNames(styles.active) : ''}>
|
||||
<Link href="/dashboard">
|
||||
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
|
||||
</Link>
|
||||
<Link href="/realtime">
|
||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||
</Link>
|
||||
<Link href="/settings">
|
||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-12 col-lg-3">
|
||||
<div className={styles.buttons}>
|
||||
<ThemeButton />
|
||||
<LanguageButton menuAlign="right" />
|
||||
{user && <UserButton />}
|
||||
<div className={styles.items}>
|
||||
<div className={active ? classNames(styles.active) : ''}>
|
||||
<div className={styles.buttons}>
|
||||
<ThemeButton />
|
||||
<LanguageButton menuAlign="right" />
|
||||
{user && <UserButton />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
.navbar {
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.burger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
min-height: 100px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
@ -15,6 +28,15 @@
|
|||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-normal);
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
@ -33,17 +55,77 @@
|
|||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.title {
|
||||
justify-content: center;
|
||||
.nav {
|
||||
font-size: var(--font-size-large);
|
||||
justify-content: space-between;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.items {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
font-size: var(--font-size-large);
|
||||
font-size: var(--font-size-normal);
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
justify-content: center;
|
||||
.items {
|
||||
display: flex;
|
||||
justify-content: unset;
|
||||
align-items: left;
|
||||
font-size: var(--font-size-normal);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.items > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header .active {
|
||||
display: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.items a {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.burger {
|
||||
display: block;
|
||||
/* color: #4a4a4a; */
|
||||
cursor: pointer;
|
||||
height: 3.25rem;
|
||||
width: 3.25rem;
|
||||
margin-left: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.burger span {
|
||||
transform: translateX(-50%);
|
||||
padding: 1px 0px;
|
||||
margin: 6px 0;
|
||||
width: 20px;
|
||||
display: block;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.burger div {
|
||||
height: 100%;
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
font-size: 1.5rem;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,6 @@ export default function Layout({ title, children, header = true, footer = true }
|
|||
<>
|
||||
<Head>
|
||||
<title>umami{title && ` - ${title}`}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
{header && <Header />}
|
||||
<main className="container">{children}</main>
|
||||
|
|
|
@ -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';
|
||||
|
@ -40,26 +40,35 @@ export default function BarChart({
|
|||
function renderXLabel(label, index, values) {
|
||||
if (loading) return '';
|
||||
const d = new Date(values[index].value);
|
||||
const w = canvas.current.width;
|
||||
const sw = canvas.current.width / window.devicePixelRatio;
|
||||
|
||||
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) {
|
||||
if (records > 25) {
|
||||
if (sw <= 275) {
|
||||
return index % 10 === 0 ? dateFormat(d, 'M/d', locale) : '';
|
||||
}
|
||||
return index % 5 === 0 ? dateFormat(d, 'M/d', locale) : '';
|
||||
if (sw <= 550) {
|
||||
return index % 5 === 0 ? dateFormat(d, 'M/d', locale) : '';
|
||||
}
|
||||
if (sw <= 700) {
|
||||
return index % 2 === 0 ? dateFormat(d, 'M/d', locale) : '';
|
||||
}
|
||||
return dateFormat(d, 'MMM d', locale);
|
||||
}
|
||||
if (w <= 500) {
|
||||
if (sw <= 375) {
|
||||
return index % 2 === 0 ? dateFormat(d, 'MMM d', locale) : '';
|
||||
}
|
||||
if (sw <= 425) {
|
||||
return dateFormat(d, 'MMM d', locale);
|
||||
}
|
||||
return dateFormat(d, 'EEE M/d', locale);
|
||||
case 'month':
|
||||
if (w <= 660) {
|
||||
if (sw <= 330) {
|
||||
return index % 2 === 0 ? dateFormat(d, 'MMM', locale) : '';
|
||||
}
|
||||
return dateFormat(d, 'MMM', locale);
|
||||
|
@ -93,9 +102,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 +140,7 @@ export default function BarChart({
|
|||
minRotation: 0,
|
||||
maxRotation: 0,
|
||||
fontColor: colors.text,
|
||||
autoSkipPadding: 1,
|
||||
},
|
||||
gridLines: {
|
||||
display: false,
|
||||
|
@ -175,6 +185,7 @@ export default function BarChart({
|
|||
options.scales.xAxes[0].ticks.callback = renderXLabel;
|
||||
options.scales.xAxes[0].ticks.fontColor = colors.text;
|
||||
options.scales.yAxes[0].ticks.fontColor = colors.text;
|
||||
options.scales.yAxes[0].ticks.precision = 0;
|
||||
options.scales.yAxes[0].gridLines.color = colors.line;
|
||||
options.scales.yAxes[0].gridLines.zeroLineColor = colors.zeroLine;
|
||||
options.animation.duration = animationDuration;
|
||||
|
|
|
@ -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,13 +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,23 +1,51 @@
|
|||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Label = ({ value }) => {
|
||||
const [event, label] = value.split(':');
|
||||
const [event, label] = value.split('\t');
|
||||
return (
|
||||
<>
|
||||
<Tag>{event}</Tag>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
.filter {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
|
@ -41,6 +41,7 @@ export default function MetricsBar({ websiteId, className }) {
|
|||
}
|
||||
|
||||
const { pageviews, uniques, bounces, totaltime } = data || {};
|
||||
const num = Math.min(uniques, bounces);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>
|
||||
|
@ -60,7 +61,7 @@ export default function MetricsBar({ websiteId, className }) {
|
|||
/>
|
||||
<MetricCard
|
||||
label={<FormattedMessage id="metrics.bounce-rate" defaultMessage="Bounce rate" />}
|
||||
value={pageviews ? (bounces / pageviews) * 100 : 0}
|
||||
value={uniques ? (num / uniques) * 100 : 0}
|
||||
format={n => Number(n).toFixed(0) + '%'}
|
||||
/>
|
||||
<MetricCard
|
||||
|
|
|
@ -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;
|
||||
|
@ -29,7 +29,7 @@ const TYPE_ICONS = {
|
|||
[TYPE_EVENT]: <Bolt />,
|
||||
};
|
||||
|
||||
export default function RealtimeLog({ data, websites }) {
|
||||
export default function RealtimeLog({ data, websites, websiteId }) {
|
||||
const intl = useIntl();
|
||||
const [locale] = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
|
@ -88,7 +88,7 @@ export default function RealtimeLog({ data, websites }) {
|
|||
}
|
||||
|
||||
function getWebsite({ website_id }) {
|
||||
return websites.find(n => n.website_id === website_id)?.name;
|
||||
return websites.find(n => n.website_id === website_id);
|
||||
}
|
||||
|
||||
function getDetail({
|
||||
|
@ -101,6 +101,7 @@ export default function RealtimeLog({ data, websites }) {
|
|||
os,
|
||||
country,
|
||||
device,
|
||||
website_id,
|
||||
}) {
|
||||
if (event_type) {
|
||||
return (
|
||||
|
@ -110,7 +111,17 @@ export default function RealtimeLog({ data, websites }) {
|
|||
);
|
||||
}
|
||||
if (view_id) {
|
||||
return url;
|
||||
const domain = getWebsite({ website_id })?.domain;
|
||||
return (
|
||||
<a
|
||||
className={styles.link}
|
||||
href={`//${domain}${url}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
if (session_id) {
|
||||
return (
|
||||
|
@ -118,7 +129,12 @@ export default function RealtimeLog({ data, websites }) {
|
|||
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>,
|
||||
|
@ -129,7 +145,7 @@ export default function RealtimeLog({ data, websites }) {
|
|||
}
|
||||
|
||||
function getTime({ created_at }) {
|
||||
return format(new Date(created_at), 'h:mm:ss');
|
||||
return dateFormat(new Date(created_at), 'pp', locale);
|
||||
}
|
||||
|
||||
function getColor(row) {
|
||||
|
@ -150,7 +166,9 @@ export default function RealtimeLog({ data, websites }) {
|
|||
<Icon className={styles.icon} icon={getIcon(row)} />
|
||||
{getDetail(row)}
|
||||
</div>
|
||||
<div className={styles.website}>{getWebsite(row)}</div>
|
||||
{!websiteId && websites.length > 1 && (
|
||||
<div className={styles.website}>{getWebsite(row)?.domain}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -163,9 +181,11 @@ export default function RealtimeLog({ data, websites }) {
|
|||
</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 {
|
||||
|
@ -44,3 +48,12 @@
|
|||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row .link {
|
||||
color: var(--gray900);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.row .link:hover {
|
||||
color: var(--primary400);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,10 @@ export default function RealtimeViews({ websiteId, data, websites }) {
|
|||
const [filter, setFilter] = useState(FILTER_REFERRERS);
|
||||
const domains = useMemo(() => websites.map(({ domain }) => domain), [websites]);
|
||||
const getDomain = useCallback(
|
||||
id => websites.find(({ website_id }) => website_id === id)?.domain,
|
||||
id =>
|
||||
websites.length === 1
|
||||
? websites[0]?.domain
|
||||
: websites.find(({ website_id }) => website_id === id)?.domain,
|
||||
[websites],
|
||||
);
|
||||
|
||||
|
@ -28,6 +31,15 @@ export default function RealtimeViews({ websiteId, data, websites }) {
|
|||
},
|
||||
];
|
||||
|
||||
const renderLink = ({ x }) => {
|
||||
const domain = x.startsWith('/') ? getDomain(websiteId) : '';
|
||||
return (
|
||||
<a href={`//${domain}${x}`} target="_blank" rel="noreferrer noopener">
|
||||
{x}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const [referrers, pages] = useMemo(() => {
|
||||
if (pageviews) {
|
||||
const referrers = percentFilter(
|
||||
|
@ -55,7 +67,7 @@ export default function RealtimeViews({ websiteId, data, websites }) {
|
|||
pageviews
|
||||
.reduce((arr, { url, website_id }) => {
|
||||
if (url?.startsWith('/')) {
|
||||
if (!websiteId) {
|
||||
if (!websiteId && websites.length > 1) {
|
||||
url = `${getDomain(website_id)}${url}`;
|
||||
}
|
||||
const row = arr.find(({ x }) => x === url);
|
||||
|
@ -91,6 +103,7 @@ export default function RealtimeViews({ websiteId, data, websites }) {
|
|||
<DataTable
|
||||
title={<FormattedMessage id="metrics.pages" defaultMessage="Pages" />}
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
renderLabel={renderLink}
|
||||
data={pages}
|
||||
height={400}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -10,12 +10,23 @@ import Arrow from 'assets/arrow-right.svg';
|
|||
import styles from './WebsiteHeader.module.css';
|
||||
|
||||
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 (
|
||||
<PageHeader>
|
||||
<div className={styles.title}>
|
||||
<Favicon domain={domain} />
|
||||
{title}
|
||||
</div>
|
||||
<div className={styles.title}>{header}</div>
|
||||
<ActiveUsers className={styles.active} websiteId={websiteId} />
|
||||
<ButtonLayout align="right">
|
||||
<RefreshButton websiteId={websiteId} />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.title {
|
||||
color: var(--gray-900);
|
||||
color: var(--gray900);
|
||||
font-size: var(--font-size-large);
|
||||
line-height: var(--font-size-large);
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@ export default function RealtimeDashboard() {
|
|||
<RealtimeViews websiteId={websiteId} data={realtimeData} websites={websites} />
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} lg={8}>
|
||||
<RealtimeLog data={realtimeData} websites={websites} />
|
||||
<RealtimeLog websiteId={websiteId} data={realtimeData} websites={websites} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
|
|
|
@ -43,19 +43,12 @@ export default function WebsiteDetails({ websiteId }) {
|
|||
const [eventsData, setEventsData] = useState();
|
||||
const {
|
||||
resolve,
|
||||
router,
|
||||
query: { view },
|
||||
} = usePageQuery();
|
||||
|
||||
const BackButton = () => (
|
||||
<div key="back-button" className={styles.backButton}>
|
||||
<Link
|
||||
key="back-button"
|
||||
href={router.pathname}
|
||||
as={resolve({ view: undefined })}
|
||||
icon={<Arrow />}
|
||||
size="small"
|
||||
>
|
||||
<Link key="back-button" href={resolve({ view: undefined })} icon={<Arrow />} size="small">
|
||||
<FormattedMessage id="label.back" defaultMessage="Back" />
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
@ -29,14 +29,13 @@ export default function AccountSettings() {
|
|||
|
||||
const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null);
|
||||
|
||||
const DashboardLink = row =>
|
||||
row.is_admin ? null : (
|
||||
<Link href={`/dashboard/${row.user_id}/${row.username}`}>
|
||||
<a>
|
||||
<Icon icon={<LinkIcon />} />
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
const DashboardLink = row => (
|
||||
<Link href={`/dashboard/${row.user_id}/${row.username}`}>
|
||||
<a>
|
||||
<Icon icon={<LinkIcon />} />
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const Buttons = row =>
|
||||
row.username !== 'admin' ? (
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -24,7 +24,7 @@ export default function LanguageButton() {
|
|||
)}
|
||||
{locale === 'zh-TW' && (
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap"
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.menu {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
min-width: 500px;
|
||||
min-width: 560px;
|
||||
max-width: 100vw;
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.button svg {
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { del } from 'lib/web';
|
||||
|
||||
export default function useDelete() {
|
||||
const { basePath } = useRouter();
|
||||
|
||||
return useCallback(async (url, params, headers) => {
|
||||
return del(`${basePath}${url}`, params, headers);
|
||||
}, []);
|
||||
}
|
|
@ -6,8 +6,7 @@ import { useRouter } from 'next/router';
|
|||
|
||||
export default function useFetch(url, options = {}, update = []) {
|
||||
const dispatch = useDispatch();
|
||||
const [data, setData] = useState();
|
||||
const [status, setStatus] = useState();
|
||||
const [response, setResponse] = useState();
|
||||
const [error, setError] = useState();
|
||||
const [loading, setLoadiing] = useState(false);
|
||||
const [count, setCount] = useState(0);
|
||||
|
@ -19,18 +18,17 @@ export default function useFetch(url, options = {}, update = []) {
|
|||
setLoadiing(true);
|
||||
setError(null);
|
||||
const time = performance.now();
|
||||
const { data, status } = await get(`${basePath}${url}`, params, headers);
|
||||
const { data, status, ok } = await get(`${basePath}${url}`, params, headers);
|
||||
|
||||
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
|
||||
|
||||
if (status >= 400) {
|
||||
setError(data);
|
||||
setData(null);
|
||||
setResponse({ data: null, status, ok });
|
||||
} else {
|
||||
setData(data);
|
||||
setResponse({ data, status, ok });
|
||||
}
|
||||
|
||||
setStatus(status);
|
||||
onDataLoad?.(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
@ -60,5 +58,5 @@ export default function useFetch(url, options = {}, update = []) {
|
|||
}
|
||||
}, [interval, !!disabled]);
|
||||
|
||||
return { data, status, error, loading };
|
||||
return { ...response, error, loading };
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getQueryString } from '../lib/url';
|
||||
import { getQueryString } from 'lib/url';
|
||||
|
||||
export default function usePageQuery() {
|
||||
const router = useRouter();
|
||||
|
@ -25,7 +25,9 @@ export default function usePageQuery() {
|
|||
function resolve(params) {
|
||||
const search = getQueryString({ ...query, ...params });
|
||||
|
||||
return `${pathname}${search}`;
|
||||
const { asPath } = router;
|
||||
|
||||
return `${asPath.split('?')[0]}${search}`;
|
||||
}
|
||||
|
||||
return { pathname, query, resolve, router };
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { post } from 'lib/web';
|
||||
|
||||
export default function usePost() {
|
||||
const { basePath } = useRouter();
|
||||
|
||||
return useCallback(async (url, params, headers) => {
|
||||
return post(`${basePath}${url}`, params, headers);
|
||||
}, []);
|
||||
}
|
|
@ -2,17 +2,7 @@ import { useState, useEffect } from 'react';
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateUser } from 'redux/actions/user';
|
||||
import { useRouter } from 'next/router';
|
||||
import { get } from '../lib/web';
|
||||
|
||||
export async function fetchUser() {
|
||||
const res = await fetch('/api/auth/verify');
|
||||
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
import { get } from 'lib/web';
|
||||
|
||||
export default function useRequireLogin() {
|
||||
const router = useRouter();
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useRouter } from 'next/router';
|
||||
import { get } from 'lib/web';
|
||||
import { setShareToken } from 'redux/actions/app';
|
||||
|
||||
export default function useShareToken(shareId) {
|
||||
const { basePath } = useRouter();
|
||||
const dispatch = useDispatch();
|
||||
const shareToken = useSelector(state => state.app.shareToken);
|
||||
|
||||
async function loadToken(id) {
|
||||
const { data } = await get(`/api/share/${id}`);
|
||||
const { data } = await get(`${basePath}/api/share/${id}`);
|
||||
|
||||
if (data) {
|
||||
dispatch(setShareToken(data));
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"cs-CZ": ["label.reset", "metrics.device.tablet"],
|
||||
"de-DE": [
|
||||
"label.administrator",
|
||||
"label.name",
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"label.accounts": "Účty",
|
||||
"label.add-account": "Přidat účet",
|
||||
"label.add-website": "Přidat web",
|
||||
"label.administrator": "Administrátor",
|
||||
"label.all": "Vše",
|
||||
"label.all-websites": "Všechny weby",
|
||||
"label.back": "Zpět",
|
||||
"label.cancel": "Zrušit",
|
||||
"label.change-password": "Změnit heslo",
|
||||
"label.confirm-password": "Potvrdit heslo",
|
||||
"label.copy-to-clipboard": "Kopírovat do schránky",
|
||||
"label.current-password": "Aktuální heslo",
|
||||
"label.custom-range": "Vlastní rozsah",
|
||||
"label.dashboard": "Přehled",
|
||||
"label.date-range": "Období",
|
||||
"label.default-date-range": "Výchozí období",
|
||||
"label.delete": "Smazat",
|
||||
"label.delete-account": "Smazat účet",
|
||||
"label.delete-website": "Smazat web",
|
||||
"label.dismiss": "Odejít",
|
||||
"label.domain": "Doména",
|
||||
"label.edit": "Upravit",
|
||||
"label.edit-account": "Upravit účet",
|
||||
"label.edit-website": "Upravit web",
|
||||
"label.enable-share-url": "Povolit sdílení URL",
|
||||
"label.invalid": "Neplatný",
|
||||
"label.invalid-domain": "Neplatná doména",
|
||||
"label.last-days": "Posledních {x} dnů",
|
||||
"label.last-hours": "Posledních {x} hodin",
|
||||
"label.logged-in-as": "Přihlášený jako {username}",
|
||||
"label.login": "Přihlásit",
|
||||
"label.logout": "Odhlásit",
|
||||
"label.more": "Více",
|
||||
"label.name": "Jméno",
|
||||
"label.new-password": "Nové heslo",
|
||||
"label.password": "Heslo",
|
||||
"label.passwords-dont-match": "Hesla se neschodují",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Aktuálně",
|
||||
"label.realtime-logs": "Aktuální záznamy",
|
||||
"label.refresh": "Obnovit",
|
||||
"label.required": "Vyžadováno",
|
||||
"label.reset": "Reset",
|
||||
"label.save": "Uložit",
|
||||
"label.settings": "Nastavení",
|
||||
"label.share-url": "Sdílet URL",
|
||||
"label.single-day": "Jeden den",
|
||||
"label.this-month": "Tento měsíc",
|
||||
"label.this-week": "Tento týden",
|
||||
"label.this-year": "Tento rok",
|
||||
"label.timezone": "Časová zóna",
|
||||
"label.today": "Dnes",
|
||||
"label.tracking-code": "Sledovací kód",
|
||||
"label.unknown": "Neznámý",
|
||||
"label.username": "Uživatelské jméno",
|
||||
"label.view-details": "Zobrazit detaily",
|
||||
"label.websites": "Weby",
|
||||
"message.active-users": "{x} aktuálně {x, plural, one {návštěvník} other {návštěvníci}}",
|
||||
"message.confirm-delete": "Opravdu smazat {target}?",
|
||||
"message.copied": "Zkopírováno!",
|
||||
"message.delete-warning": "Všechna související data budou také smazána.",
|
||||
"message.failure": "Něco se pokazilo.",
|
||||
"message.get-share-url": "Získat sdílené URL",
|
||||
"message.get-tracking-code": "Získat měřící kód",
|
||||
"message.go-to-settings": "Jít do nastavení",
|
||||
"message.incorrect-username-password": "Nesprávné jméno/heslo.",
|
||||
"message.log.visitor": "Návštěvník z {country} s prohlížečem {browser} na {os} {device}",
|
||||
"message.new-version-available": "Nová verze umami {version} je k dispozici!",
|
||||
"message.no-data-available": "Žádná data.",
|
||||
"message.no-websites-configured": "Nemáte nastavený žádný web.",
|
||||
"message.page-not-found": "Stránka nenalezena.",
|
||||
"message.powered-by": "Běží na {name}",
|
||||
"message.save-success": "Úspěšně uloženo.",
|
||||
"message.share-url": "Toto je sdílené URL pro {target}.",
|
||||
"message.track-stats": "Pro sledování návštěv na {target}, přidejte následující kód do {head} části vašeho webu.",
|
||||
"message.type-delete": "Napište {delete} pro potvrzení.",
|
||||
"metrics.actions": "Akce",
|
||||
"metrics.average-visit-time": "Průměrný čas návštěvy",
|
||||
"metrics.bounce-rate": "Okamžité opuštění",
|
||||
"metrics.browsers": "Prohlížeč",
|
||||
"metrics.countries": "Země",
|
||||
"metrics.device.desktop": "Stolní počítač",
|
||||
"metrics.device.laptop": "Přenosný počítač",
|
||||
"metrics.device.mobile": "Mobilní telefon",
|
||||
"metrics.device.tablet": "Tablet",
|
||||
"metrics.devices": "Zařízení",
|
||||
"metrics.events": "Události",
|
||||
"metrics.filter.combined": "Kombinace",
|
||||
"metrics.filter.domain-only": "Domény",
|
||||
"metrics.filter.raw": "Nezpracované",
|
||||
"metrics.operating-systems": "Operační systém",
|
||||
"metrics.page-views": "Zobrazení stránek",
|
||||
"metrics.pages": "Stránky",
|
||||
"metrics.referrers": "Odkazy",
|
||||
"metrics.unique-visitors": "Jedinečné návštěvy",
|
||||
"metrics.views": "Zobrazení",
|
||||
"metrics.visitors": "Návštěvy"
|
||||
}
|
|
@ -3,8 +3,8 @@
|
|||
"label.add-account": "Tilføj konto",
|
||||
"label.add-website": "Tilføj hjemmeside",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.all": "Alle",
|
||||
"label.all-websites": "Alle websites",
|
||||
"label.back": "Tilbage",
|
||||
"label.cancel": "Afvis",
|
||||
"label.change-password": "Skift adgangskode",
|
||||
|
@ -14,11 +14,11 @@
|
|||
"label.custom-range": "Tilpasset interval",
|
||||
"label.dashboard": "Betjeningspanel",
|
||||
"label.date-range": "Datointerval",
|
||||
"label.default-date-range": "Default date range",
|
||||
"label.default-date-range": "Standard datointerval",
|
||||
"label.delete": "Slet",
|
||||
"label.delete-account": "Slet konto",
|
||||
"label.delete-website": "Slet hjemmeside",
|
||||
"label.dismiss": "Dismiss",
|
||||
"label.dismiss": "Afvis",
|
||||
"label.domain": "Domæne",
|
||||
"label.edit": "Rediger",
|
||||
"label.edit-account": "Rediger konto",
|
||||
|
@ -37,8 +37,8 @@
|
|||
"label.password": "Adgangskode",
|
||||
"label.passwords-dont-match": "Adgangskoder matcher ikke",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.realtime": "Realtid",
|
||||
"label.realtime-logs": "Realtid logs",
|
||||
"label.refresh": "Opdater",
|
||||
"label.required": "Påkrævet",
|
||||
"label.reset": "Reset",
|
||||
|
@ -49,7 +49,7 @@
|
|||
"label.this-month": "Denne måned",
|
||||
"label.this-week": "Denne uge",
|
||||
"label.this-year": "Dette år",
|
||||
"label.timezone": "Timezone",
|
||||
"label.timezone": "Tidszone",
|
||||
"label.today": "Idag",
|
||||
"label.tracking-code": "Sporingskode",
|
||||
"label.unknown": "Ukendt",
|
||||
|
@ -65,8 +65,8 @@
|
|||
"message.get-tracking-code": "Få sporingskode",
|
||||
"message.go-to-settings": "Gå til betjeningspanel",
|
||||
"message.incorrect-username-password": "Ugyldigt brugernavn/adgangskode.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.log.visitor": "Besøgende fra {country} bruger {browser} på {os} {device}",
|
||||
"message.new-version-available": "Ny udgave af Umami {version} er tilgængelig!",
|
||||
"message.no-data-available": "Ingen data tilgængelig.",
|
||||
"message.no-websites-configured": "Du har ikke konfigureret nogen websteder.",
|
||||
"message.page-not-found": "Side ikke fundet.",
|
||||
|
|
|
@ -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.failure": "Es it ein Fehler aufgetreten.",
|
||||
"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 der ö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.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 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": "بازدید کننده"
|
||||
}
|
|
@ -3,8 +3,8 @@
|
|||
"label.add-account": "Lisää tili",
|
||||
"label.add-website": "Lisää verkkosivu",
|
||||
"label.administrator": "Järjestelmänvalvoja",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.all": "Kaikki",
|
||||
"label.all-websites": "Kaikki verkkosivut",
|
||||
"label.back": "Takaisin",
|
||||
"label.cancel": "Peruuta",
|
||||
"label.change-password": "Vaihda salasana",
|
||||
|
@ -12,7 +12,7 @@
|
|||
"label.copy-to-clipboard": "Kopioi leikepöydälle",
|
||||
"label.current-password": "Nykyinen salasana",
|
||||
"label.custom-range": "Mukautettu jakso",
|
||||
"label.dashboard": "Dashboard",
|
||||
"label.dashboard": "Ohjauspaneeli",
|
||||
"label.date-range": "Ajanjakso",
|
||||
"label.default-date-range": "Oletusajanjakso",
|
||||
"label.delete": "Poista",
|
||||
|
@ -37,8 +37,8 @@
|
|||
"label.password": "Salasana",
|
||||
"label.passwords-dont-match": "Salasanat eivät täsmää",
|
||||
"label.profile": "Profiili",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.realtime": "Reaaliaikainen",
|
||||
"label.realtime-logs": "Reaaliaikaiset lokit",
|
||||
"label.refresh": "Päivitä",
|
||||
"label.required": "Vaaditaan",
|
||||
"label.reset": "Nollaa",
|
||||
|
@ -65,7 +65,7 @@
|
|||
"message.get-tracking-code": "Hanki seurantakoodi",
|
||||
"message.go-to-settings": "Mene asetuksiin",
|
||||
"message.incorrect-username-password": "Väärä käyttäjänimi/salasana.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.log.visitor": "Vierailija maasta {country} käyttäen selainta {browser} {os}-laitteella: {device}",
|
||||
"message.new-version-available": "Uusi versio umamista {version} on käytettävissä!",
|
||||
"message.no-data-available": "Tietoja ei ole käytettävissä.",
|
||||
"message.no-websites-configured": "Sinulla ei ole määritettyjä verkkosivustoja.",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"label.accounts": "חשבונות",
|
||||
"label.add-account": "הוספה",
|
||||
"label.add-website": "הוספת אתר",
|
||||
"label.administrator": "מנהל",
|
||||
"label.all": "הכל",
|
||||
"label.all-websites": "כל האתרים",
|
||||
"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": "Powered by {name}",
|
||||
"message.save-success": "נשמר בהצלחה",
|
||||
"message.share-url": "זהו URL ציבורי עבור {target}",
|
||||
"message.track-stats": "יש להוסיף את הקוד הבא לאזור ה-{head} של האתר",
|
||||
"message.type-delete": "הקלידו {delete} בתיבה על מנת לאשר",
|
||||
"metrics.actions": "פעולות",
|
||||
"metrics.average-visit-time": "זמן ביקור ממוצע",
|
||||
"metrics.bounce-rate": "Bounce rate",
|
||||
"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": "מבקרים"
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"label.accounts": "खाता",
|
||||
"label.add-account": "खाता जोड़ें",
|
||||
"label.add-website": "वेबसाइट",
|
||||
"label.administrator": "प्रशासक",
|
||||
"label.all": "सब",
|
||||
"label.all-websites": "सभी वेबसाइटें",
|
||||
"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": "{x} के रूप में लॉग इन किया",
|
||||
"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": "यूआरएल साझा करें",
|
||||
"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} यन्त्र पर",
|
||||
"message.new-version-available": "उमामी का नया संस्करण {version} उपलब्ध है!",
|
||||
"message.no-data-available": "कोई डेटा उपलब्ध नहीं है।",
|
||||
"message.no-websites-configured": "आपके पास कोई वेबसाइट कॉन्फ़िगर नहीं है।",
|
||||
"message.page-not-found": "पृष्ठ नहीं मिला।",
|
||||
"message.powered-by": "{name} द्वारा संचालित",
|
||||
"message.save-success": "सफलतापूर्वक संचित कर लिया गया है।",
|
||||
"message.share-url": "यह {target} के लिए सार्वजनिक रूप से साझा किया गया URL है।",
|
||||
"message.track-stats": "{target} के आँकड़ों को ट्रैक करने के लिए, अपनी वेबसाइट के {head} अनुभाग में निम्नलिखित कोड रखें।",
|
||||
"message.type-delete": "पुष्टि करने के लिए नीचे दिए गए बॉक्स में {delete} टाइप करें।",
|
||||
"metrics.actions": "कार्य",
|
||||
"metrics.average-visit-time": "औसत दृश्य समय",
|
||||
"metrics.bounce-rate": "उछाल दर",
|
||||
"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": "आगंतुकों"
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"label.accounts": "Utenti",
|
||||
"label.add-account": "Aggiungi utente",
|
||||
"label.add-website": "Aggiungi sito",
|
||||
"label.administrator": "Amministratore",
|
||||
"label.all": "Tutto",
|
||||
"label.all-websites": "Tutti i siti web",
|
||||
"label.back": "Indietro",
|
||||
"label.cancel": "Annulla",
|
||||
"label.change-password": "Modifica password",
|
||||
"label.confirm-password": "Conferma password",
|
||||
"label.copy-to-clipboard": "Copia",
|
||||
"label.current-password": "Password corrente",
|
||||
"label.custom-range": "Personalizzato",
|
||||
"label.dashboard": "Dashboard",
|
||||
"label.date-range": "Periodo",
|
||||
"label.default-date-range": "Periodo standard",
|
||||
"label.delete": "Elimina",
|
||||
"label.delete-account": "Elimina account",
|
||||
"label.delete-website": "Elimina sito",
|
||||
"label.dismiss": "Scarta",
|
||||
"label.domain": "Dominio",
|
||||
"label.edit": "Modifica",
|
||||
"label.edit-account": "Modifica account",
|
||||
"label.edit-website": "Modifica sito",
|
||||
"label.enable-share-url": "Abilita URL di condivisione",
|
||||
"label.invalid": "Non valido",
|
||||
"label.invalid-domain": "Dominio non valido",
|
||||
"label.last-days": "Ultimi {x} giorni",
|
||||
"label.last-hours": "Ultime {x} ore",
|
||||
"label.logged-in-as": "Ciao {username}",
|
||||
"label.login": "Login",
|
||||
"label.logout": "Logout",
|
||||
"label.more": "Dettagli",
|
||||
"label.name": "Nome",
|
||||
"label.new-password": "Nuova password",
|
||||
"label.password": "Password",
|
||||
"label.passwords-dont-match": "Le password non corrispondono",
|
||||
"label.profile": "Profilo",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Log in realtime",
|
||||
"label.refresh": "Ricarica",
|
||||
"label.required": "Obbligatorio",
|
||||
"label.reset": "Reset",
|
||||
"label.save": "Salva",
|
||||
"label.settings": "Impostazioni",
|
||||
"label.share-url": "Share URL",
|
||||
"label.single-day": "Singolo giorno",
|
||||
"label.this-month": "Questo mese",
|
||||
"label.this-week": "Questa settimana",
|
||||
"label.this-year": "Quest'anno",
|
||||
"label.timezone": "Fuso orario",
|
||||
"label.today": "Oggi",
|
||||
"label.tracking-code": "Codice di tracking",
|
||||
"label.unknown": "Sconosciuto",
|
||||
"label.username": "Username",
|
||||
"label.view-details": "Vedi dettagli",
|
||||
"label.websites": "Siti web",
|
||||
"message.active-users": "{x} {x, plural, one {visitatore} other {visitatori}} online",
|
||||
"message.confirm-delete": "Sei sicuro di voler eliminare {target}?",
|
||||
"message.copied": "Copiato!",
|
||||
"message.delete-warning": "Saranno eliminati anche tutti i dati associati.",
|
||||
"message.failure": "Si è verificato un errore.",
|
||||
"message.get-share-url": "Ottieni l'URL di condivisione",
|
||||
"message.get-tracking-code": "Ottieni il codice di tracking",
|
||||
"message.go-to-settings": "Vai alle impostazioni",
|
||||
"message.incorrect-username-password": "Username o password non corretti.",
|
||||
"message.log.visitor": "Utenti da {country} tramite {browser} su {os} {device}",
|
||||
"message.new-version-available": "Una nuova versione umami {version} è disponibile!",
|
||||
"message.no-data-available": "Nessun dato disponibile.",
|
||||
"message.no-websites-configured": "Non hai ancora configurato alcun sito.",
|
||||
"message.page-not-found": "Pagina non trovata",
|
||||
"message.powered-by": "Powered by {name}",
|
||||
"message.save-success": "Salvato!",
|
||||
"message.share-url": "Questo è l'URL di condivisione per {target}.",
|
||||
"message.track-stats": "Per tracciare le statistiche di {target}, inserisci questo codice nella sezione {head} del tuo sito web.",
|
||||
"message.type-delete": "Digita {delete} nel box qui sotto per confermare.",
|
||||
"metrics.actions": "Azioni",
|
||||
"metrics.average-visit-time": "Tempo medio di visita",
|
||||
"metrics.bounce-rate": "Frequenza di rimbalzo",
|
||||
"metrics.browsers": "Browser",
|
||||
"metrics.countries": "Nazioni",
|
||||
"metrics.device.desktop": "Desktop",
|
||||
"metrics.device.laptop": "Laptop",
|
||||
"metrics.device.mobile": "Mobile",
|
||||
"metrics.device.tablet": "Tablet",
|
||||
"metrics.devices": "Dispositivi",
|
||||
"metrics.events": "Eventi",
|
||||
"metrics.filter.combined": "Aggregati",
|
||||
"metrics.filter.domain-only": "Solo dominio",
|
||||
"metrics.filter.raw": "Raw",
|
||||
"metrics.operating-systems": "Sistemi operativi",
|
||||
"metrics.page-views": "Visualizzazioni di pagina",
|
||||
"metrics.pages": "Pagine",
|
||||
"metrics.referrers": "Referr",
|
||||
"metrics.unique-visitors": "Visitatori unici",
|
||||
"metrics.views": "Visualizzazioni",
|
||||
"metrics.visitors": "Visitatori"
|
||||
}
|
|
@ -3,8 +3,8 @@
|
|||
"label.add-account": "アカウントの追加",
|
||||
"label.add-website": "Webサイトの追加",
|
||||
"label.administrator": "管理者",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.all": "すべて表示",
|
||||
"label.all-websites": "すべてのWebサイト",
|
||||
"label.back": "戻る",
|
||||
"label.cancel": "キャンセル",
|
||||
"label.change-password": "パスワード変更",
|
||||
|
@ -13,7 +13,7 @@
|
|||
"label.current-password": "現在のパスワード",
|
||||
"label.custom-range": "期間を指定する",
|
||||
"label.dashboard": "ダッシュボード",
|
||||
"label.date-range": "日付範囲",
|
||||
"label.date-range": "範囲指定",
|
||||
"label.default-date-range": "最初に表示する期間",
|
||||
"label.delete": "削除",
|
||||
"label.delete-account": "アカウントの削除",
|
||||
|
@ -37,8 +37,8 @@
|
|||
"label.password": "パスワード",
|
||||
"label.passwords-dont-match": "パスワードが一致しません",
|
||||
"label.profile": "プロファイル",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.realtime": "リアルタイム",
|
||||
"label.realtime-logs": "リアルタイムログ",
|
||||
"label.refresh": "更新",
|
||||
"label.required": "必須",
|
||||
"label.reset": "リセット",
|
||||
|
@ -65,12 +65,12 @@
|
|||
"message.get-tracking-code": "トラッキングコードを取得",
|
||||
"message.go-to-settings": "設定する",
|
||||
"message.incorrect-username-password": "ユーザー名/パスワードが正しくありません。",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.log.visitor": "{os}({device})で{browser}を使用している{country}からの訪問者",
|
||||
"message.new-version-available": "新しいバージョン({version})が利用可能です!",
|
||||
"message.no-data-available": "データがありません。",
|
||||
"message.no-websites-configured": "Webサイトが設定されていません。",
|
||||
"message.page-not-found": "ページが見つかりません。",
|
||||
"message.powered-by": "Powered by {name}",
|
||||
"message.powered-by": "このシステムは {name} で実行されています。",
|
||||
"message.save-success": "正常に保存されました。",
|
||||
"message.share-url": "これは {target} の共有リンクです。",
|
||||
"message.track-stats": "{target}のアクセス解析を開始するには、次のコードをWebサイトの{head}セクションへ追加してください。",
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
"metrics.page-views": "Хуудас үзсэн",
|
||||
"metrics.pages": "Хуудас",
|
||||
"metrics.referrers": "Чиглүүлэгч",
|
||||
"metrics.unique-visitors": "Зочид",
|
||||
"metrics.unique-visitors": "Зочин",
|
||||
"metrics.views": "Үзсэн",
|
||||
"metrics.visitors": "Зочид"
|
||||
"metrics.visitors": "Зочин"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"label.accounts": "Akaun",
|
||||
"label.add-account": "Tambah akaun",
|
||||
"label.add-website": "Tambah laman web",
|
||||
"label.administrator": "Pentadbir",
|
||||
"label.all": "Semua",
|
||||
"label.all-websites": "Semua laman web",
|
||||
"label.all-events": "Semua peristiwa",
|
||||
"label.back": "Kembali",
|
||||
"label.cancel": "Batal",
|
||||
"label.change-password": "Tukar kata laluan",
|
||||
"label.confirm-password": "Sahkan kata laluan",
|
||||
"label.copy-to-clipboard": "Salin ke papan keratan",
|
||||
"label.current-password": "Kata laluan semasa",
|
||||
"label.custom-range": "Julat khas",
|
||||
"label.dashboard": "Papan pemuka",
|
||||
"label.date-range": "Julat tarikh",
|
||||
"label.default-date-range": "Julat tarikh lalai",
|
||||
"label.delete": "Padam",
|
||||
"label.delete-account": "Padam akaun",
|
||||
"label.delete-website": "Padam laman web",
|
||||
"label.dismiss": "Ketepikan",
|
||||
"label.domain": "Domain",
|
||||
"label.edit": "Edit",
|
||||
"label.edit-account": "Edit akaun",
|
||||
"label.edit-website": "Edit laman web",
|
||||
"label.enable-share-url": "Aktifkan url berkongsi",
|
||||
"label.invalid": "Tidak sah",
|
||||
"label.invalid-domain": "Domain tidak sah",
|
||||
"label.last-days": "{x} hari lepas",
|
||||
"label.last-hours": "{x} jam lepas",
|
||||
"label.logged-in-as": "Log masuk sebagai {username}",
|
||||
"label.login": "Log masuk",
|
||||
"label.logout": "Log keluar",
|
||||
"label.more": "Lebih banyak lagi",
|
||||
"label.name": "Nama",
|
||||
"label.new-password": "Kata laluan baru",
|
||||
"label.password": "Kata laluan",
|
||||
"label.passwords-dont-match": "Kata laluan tidak sepadan",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Siaran langsung",
|
||||
"label.realtime-logs": "Log secara siaran langsung",
|
||||
"label.refresh": "Muat semula",
|
||||
"label.required": "Diperlukan",
|
||||
"label.reset": "Tetapkan semula",
|
||||
"label.save": "Simpan",
|
||||
"label.settings": "Tetapan",
|
||||
"label.share-url": "Kongsikan URL",
|
||||
"label.single-day": "Satu hari",
|
||||
"label.this-month": "Bulan ini",
|
||||
"label.this-week": "Minggu ini",
|
||||
"label.this-year": "Tahun ini",
|
||||
"label.timezone": "Zon masa",
|
||||
"label.today": "Hari ini",
|
||||
"label.tracking-code": "Kod penjejakan",
|
||||
"label.unknown": "Tidak diketahui",
|
||||
"label.username": "Nama pengguna",
|
||||
"label.view-details": "Lihat butiran",
|
||||
"label.websites": "Laman web",
|
||||
"message.active-users": "{x} semasa {x, plural, one {pelawat} other {pelawat}}",
|
||||
"message.confirm-delete": "Pastikah anda ingin memadam {target}?",
|
||||
"message.copied": "Disalin!",
|
||||
"message.delete-warning": "Semua data yang berkaitan juga akan dihapuskan.",
|
||||
"message.failure": "Ada yang tidak kena.",
|
||||
"message.get-share-url": "Dapatkan URL berkongsi",
|
||||
"message.get-tracking-code": "Dapatkan kod penjejakan",
|
||||
"message.go-to-settings": "Pergi ke tetapan",
|
||||
"message.incorrect-username-password": "Pengguna/kata laluan tidak betul.",
|
||||
"message.log.visitor": "Pelawat dari {country} mengguna {browser} pada {os} {device}",
|
||||
"message.new-version-available": "Versi baru umami {version} boleh didapati!",
|
||||
"message.no-data-available": "Tiada data yang boleh didapati.",
|
||||
"message.no-websites-configured": "Anda tidak ada sebarang laman web yang telah dikonfigurasikan.",
|
||||
"message.page-not-found": "Halaman tidak dijumpai.",
|
||||
"message.powered-by": "Disediakan oleh {name}",
|
||||
"message.save-success": "Berjaya disimpan.",
|
||||
"message.share-url": "Ini adalah URL berkongsi untuk {target}.",
|
||||
"message.track-stats": "Untuk menjejak statistik bagi {target}, letakkan kod berikut di bahagian {head} laman web anda.",
|
||||
"message.type-delete": "Taip {delete} di dalam kotak di bawah untuk pengesahan.",
|
||||
"metrics.actions": "Aksi",
|
||||
"metrics.average-visit-time": "Purata tempoh masa lawatan",
|
||||
"metrics.bounce-rate": "Kadar lantunan",
|
||||
"metrics.browsers": "Pelayar web",
|
||||
"metrics.countries": "Negara",
|
||||
"metrics.device.desktop": "Desktop",
|
||||
"metrics.device.laptop": "Laptop",
|
||||
"metrics.device.mobile": "Telefon bimbit",
|
||||
"metrics.device.tablet": "Tablet",
|
||||
"metrics.devices": "Peranti",
|
||||
"metrics.events": "Peristiwa",
|
||||
"metrics.filter.combined": "Digabungkan",
|
||||
"metrics.filter.domain-only": "Domain sahaja",
|
||||
"metrics.filter.raw": "Mentah",
|
||||
"metrics.operating-systems": "Sistem operasi",
|
||||
"metrics.page-views": "Paparan halaman",
|
||||
"metrics.pages": "Halaman",
|
||||
"metrics.referrers": "Perujuk",
|
||||
"metrics.unique-visitors": "Pelawat unik",
|
||||
"metrics.views": "Lawatan",
|
||||
"metrics.visitors": "Pelawat"
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"label.accounts": "Konta",
|
||||
"label.add-account": "Dodaj konto",
|
||||
"label.add-website": "Dodaj witrynę",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "Wszystkie",
|
||||
"label.all-websites": "Wszystkie witryny",
|
||||
"label.back": "Powrót",
|
||||
"label.cancel": "Anuluj",
|
||||
"label.change-password": "Zmień hasło",
|
||||
"label.confirm-password": "Potwierdź hasło",
|
||||
"label.copy-to-clipboard": "Skopiuj do schowka",
|
||||
"label.current-password": "Aktualne hasło",
|
||||
"label.custom-range": "Zakres niestandardowy",
|
||||
"label.dashboard": "Dashboard",
|
||||
"label.date-range": "Zakres dat",
|
||||
"label.default-date-range": "Domyślny zakres dat",
|
||||
"label.delete": "Usuń",
|
||||
"label.delete-account": "Usuń konto",
|
||||
"label.delete-website": "Usuń witrynę",
|
||||
"label.dismiss": "Odrzuć",
|
||||
"label.domain": "Domena",
|
||||
"label.edit": "Edytuj",
|
||||
"label.edit-account": "Edytuj konto",
|
||||
"label.edit-website": "Edytuj witrynę",
|
||||
"label.enable-share-url": "Włącz udostępnianie adresu URL",
|
||||
"label.invalid": "Nieprawidłowy",
|
||||
"label.invalid-domain": "Nieprawidłowa witryna",
|
||||
"label.last-days": "Ostatnie {x} dni",
|
||||
"label.last-hours": "Ostatnie {x} godzin",
|
||||
"label.logged-in-as": "Zalogowano jako {username}",
|
||||
"label.login": "Zaloguj się",
|
||||
"label.logout": "Wyloguj",
|
||||
"label.more": "Więcej",
|
||||
"label.name": "Nazwa",
|
||||
"label.new-password": "Nowe hasło",
|
||||
"label.password": "Hasło",
|
||||
"label.passwords-dont-match": "Hasła się nie zgadzają",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Czas rzeczywisty",
|
||||
"label.realtime-logs": "Logi w czasie rzeczywistym",
|
||||
"label.refresh": "Odśwież",
|
||||
"label.required": "Wymagany",
|
||||
"label.reset": "Zresetuj",
|
||||
"label.save": "Zapisz",
|
||||
"label.settings": "Ustawienia",
|
||||
"label.share-url": "Udostępnij adres URL",
|
||||
"label.single-day": "W tym dniu",
|
||||
"label.this-month": "W tym miesiącu",
|
||||
"label.this-week": "W tym tygodniu",
|
||||
"label.this-year": "W tym roku",
|
||||
"label.timezone": "Strefa czasowa",
|
||||
"label.today": "Dzisiaj",
|
||||
"label.tracking-code": "Kod śledzenia",
|
||||
"label.unknown": "Nieznany",
|
||||
"label.username": "Nazwa użytkownika",
|
||||
"label.view-details": "Pokaż szczegóły",
|
||||
"label.websites": "Witryny",
|
||||
"message.active-users": "{x} aktualnie {x, plural, one {odwiedzający} other {odwiedzających}}",
|
||||
"message.confirm-delete": "Czy na pewno chcesz usunąć {target}?",
|
||||
"message.copied": "Skopiowano!",
|
||||
"message.delete-warning": "Wszystkie powiązane dane również zostaną usunięte.",
|
||||
"message.failure": "Coś poszło nie tak.",
|
||||
"message.get-share-url": "Uzyskaj adres URL udostępniania",
|
||||
"message.get-tracking-code": "Pobierz kod śledzenia",
|
||||
"message.go-to-settings": "Przejdź do ustawień",
|
||||
"message.incorrect-username-password": "Nieprawidłowa nazwa użytkownika/hasło.",
|
||||
"message.log.visitor": "Odwiedzający z {country} używa {browser} na {os} {device}",
|
||||
"message.new-version-available": "Nowa wersja umami {version} jest dostępna!",
|
||||
"message.no-data-available": "Brak dostępnych danych.",
|
||||
"message.no-websites-configured": "Nie masz skonfigurowanych żadnych witryn internetowych.",
|
||||
"message.page-not-found": "Strona nie znaleziona.",
|
||||
"message.powered-by": "Obsługiwane przez {name}",
|
||||
"message.save-success": "Zapisano pomyślnie.",
|
||||
"message.share-url": "To jest publicznie udostępniany adres URL dla {target}.",
|
||||
"message.track-stats": "Aby śledzić statystyki dla {target}, umieść poniższy kod w sekcji {head} swojej witryny.",
|
||||
"message.type-delete": "Wpisz {delete} w polu poniżej, aby potwierdzić.",
|
||||
"metrics.actions": "Działania",
|
||||
"metrics.average-visit-time": "Średni czas wizyty",
|
||||
"metrics.bounce-rate": "Współczynnik odrzuceń",
|
||||
"metrics.browsers": "Przeglądarki",
|
||||
"metrics.countries": "Kraje",
|
||||
"metrics.device.desktop": "Komputer",
|
||||
"metrics.device.laptop": "Laptop",
|
||||
"metrics.device.mobile": "Smartfon",
|
||||
"metrics.device.tablet": "Tablet",
|
||||
"metrics.devices": "Urządzenia",
|
||||
"metrics.events": "Zdarzenia",
|
||||
"metrics.filter.combined": "Połączone",
|
||||
"metrics.filter.domain-only": "Tylko domena",
|
||||
"metrics.filter.raw": "Surowe dane",
|
||||
"metrics.operating-systems": "System operacyjny",
|
||||
"metrics.page-views": "Wyświetlenia strony",
|
||||
"metrics.pages": "Strony",
|
||||
"metrics.referrers": "Źródła odsyłające",
|
||||
"metrics.unique-visitors": "Unikalni odwiedzający",
|
||||
"metrics.views": "Wyświetlenia",
|
||||
"metrics.visitors": "Odwiedzający"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -3,8 +3,8 @@
|
|||
"label.add-account": "Adicionar conta",
|
||||
"label.add-website": "Adicionar website",
|
||||
"label.administrator": "Administrador",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.all": "Todos",
|
||||
"label.all-websites": "Todos os websites",
|
||||
"label.back": "Voltar",
|
||||
"label.cancel": "Cancelar",
|
||||
"label.change-password": "Alterar palavra-passe",
|
||||
|
@ -37,8 +37,8 @@
|
|||
"label.password": "Palavra-passe",
|
||||
"label.passwords-dont-match": "Palavra-passes não correspondem",
|
||||
"label.profile": "Perfil",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.realtime": "Tempo real",
|
||||
"label.realtime-logs": "Relatório em tempo real",
|
||||
"label.refresh": "Atualizar",
|
||||
"label.required": "Obrigatório",
|
||||
"label.reset": "Repor",
|
||||
|
@ -65,7 +65,7 @@
|
|||
"message.get-tracking-code": "Obter código de tracking",
|
||||
"message.go-to-settings": "Ir para as definições",
|
||||
"message.incorrect-username-password": "Nome de utilizador/palavra-passe incorretos.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.log.visitor": "Visitante de {country} a usar {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": "Não tens nenhum website configurado.",
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"label.accounts": "Účty",
|
||||
"label.add-account": "Pridať účet",
|
||||
"label.add-website": "Pridať web",
|
||||
"label.administrator": "Administrátor",
|
||||
"label.all": "Všetko",
|
||||
"label.all-websites": "Všetky weby",
|
||||
"label.back": "Späť",
|
||||
"label.cancel": "Zrušiť",
|
||||
"label.change-password": "Zmeniť heslo",
|
||||
"label.confirm-password": "Potvrdiť heslo",
|
||||
"label.copy-to-clipboard": "Kopírovať do schránky",
|
||||
"label.current-password": "Aktuálne heslo",
|
||||
"label.custom-range": "Vlastný rozsah",
|
||||
"label.dashboard": "Prehlad",
|
||||
"label.date-range": "Obdobie",
|
||||
"label.default-date-range": "Predvolené obdobie",
|
||||
"label.delete": "Zmazať",
|
||||
"label.delete-account": "Zmazať účet",
|
||||
"label.delete-website": "Zmazať web",
|
||||
"label.dismiss": "Odísť",
|
||||
"label.domain": "Doména",
|
||||
"label.edit": "Upraviť",
|
||||
"label.edit-account": "Upraviť účet",
|
||||
"label.edit-website": "Upraviť web",
|
||||
"label.enable-share-url": "Povoliť zdielanie URL",
|
||||
"label.invalid": "Neplatný",
|
||||
"label.invalid-domain": "Neplatná doména",
|
||||
"label.last-days": "Posledných {x} dní",
|
||||
"label.last-hours": "Posledných {x} hodín",
|
||||
"label.logged-in-as": "Prihlásený ako {username}",
|
||||
"label.login": "Prihlásiť",
|
||||
"label.logout": "Odhlásiť",
|
||||
"label.more": "Viac",
|
||||
"label.name": "Meno",
|
||||
"label.new-password": "Nové heslo",
|
||||
"label.password": "Heslo",
|
||||
"label.passwords-dont-match": "Hesla se nezhodujú",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Aktuálne",
|
||||
"label.realtime-logs": "Aktuálne záznamy",
|
||||
"label.refresh": "Obnoviť",
|
||||
"label.required": "Povinné",
|
||||
"label.reset": "Reset",
|
||||
"label.save": "Uložiť",
|
||||
"label.settings": "Nastavenia",
|
||||
"label.share-url": "Zdielanie URL",
|
||||
"label.single-day": "Jeden deň",
|
||||
"label.this-month": "Tento mesiac",
|
||||
"label.this-week": "Tento týždeň",
|
||||
"label.this-year": "Tento rok",
|
||||
"label.timezone": "Časová zóna",
|
||||
"label.today": "Dnes",
|
||||
"label.tracking-code": "Sledovací kód",
|
||||
"label.unknown": "Neznámý",
|
||||
"label.username": "Užívateľské meno",
|
||||
"label.view-details": "Zobraziť detaily",
|
||||
"label.websites": "Weby",
|
||||
"message.active-users": "{x} aktuálne {x, plural, one {návštevník} other {návštěvníci}}",
|
||||
"message.confirm-delete": "Naozaj zmazať {target}?",
|
||||
"message.copied": "Skopírované!",
|
||||
"message.delete-warning": "Všetky príbuzné data budu tiež zmazané.",
|
||||
"message.failure": "Niečo sa pokazilo.",
|
||||
"message.get-share-url": "Získať zdielané URL",
|
||||
"message.get-tracking-code": "Získať tracking kód",
|
||||
"message.go-to-settings": "Ísť do nastavení",
|
||||
"message.incorrect-username-password": "Nesprávné meno/heslo.",
|
||||
"message.log.visitor": "Návštevník z {country} s prehliadačom {browser} na {os} {device}",
|
||||
"message.new-version-available": "Nová verzia umami {version} je k dispozícii!",
|
||||
"message.no-data-available": "Žiadne data.",
|
||||
"message.no-websites-configured": "Nemáte nastavený žiadny web.",
|
||||
"message.page-not-found": "Stránka sa nenašla.",
|
||||
"message.powered-by": "Powered by {name}",
|
||||
"message.save-success": "Úspešne uložené.",
|
||||
"message.share-url": "Toto je zdielané URL pre {target}.",
|
||||
"message.track-stats": "Pre sledovanie návštev na {target}, pridajte následujúci kód do {head} časti vašeho webu.",
|
||||
"message.type-delete": "Napíšte {delete} pre potvrdenie.",
|
||||
"metrics.actions": "Akcie",
|
||||
"metrics.average-visit-time": "Priemerný čas návštevy",
|
||||
"metrics.bounce-rate": "Okamžité opustenie",
|
||||
"metrics.browsers": "Prehliadač",
|
||||
"metrics.countries": "Zem",
|
||||
"metrics.device.desktop": "Stolný počítač",
|
||||
"metrics.device.laptop": "Prenosný počítač",
|
||||
"metrics.device.mobile": "Mobilný telefon",
|
||||
"metrics.device.tablet": "Tablet",
|
||||
"metrics.devices": "Zariadenie",
|
||||
"metrics.events": "Udalosti",
|
||||
"metrics.filter.combined": "Kombinácie",
|
||||
"metrics.filter.domain-only": "Domény",
|
||||
"metrics.filter.raw": "Nezpracované",
|
||||
"metrics.operating-systems": "Operačný systém",
|
||||
"metrics.page-views": "Zobrazenie stánok",
|
||||
"metrics.pages": "Stránky",
|
||||
"metrics.referrers": "Odkazy",
|
||||
"metrics.unique-visitors": "Jedinečné návštevy",
|
||||
"metrics.views": "Zobrazení",
|
||||
"metrics.visitors": "Návštevy"
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"label.accounts": "கணக்குகள்",
|
||||
"label.add-account": "கணக்கு சேர்க்க",
|
||||
"label.add-website": "வலைத்தளத்தைச் சேர்க்க",
|
||||
"label.administrator": "நிர்வாகியைச் சேர்க்க",
|
||||
"label.all": "எல்லாம்",
|
||||
"label.all-events": "அனைத்து நிகழ்வுகளும்",
|
||||
"label.all-websites": "அனைத்து வலைத்தளங்களும்",
|
||||
"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": "கள முகவரியை பகிரலாம்",
|
||||
"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": "வலைத்தள களத்தைப் பகிரவும்",
|
||||
"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": "கள முகவரியை ஐப் பெறுக",
|
||||
"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": "{target} இது பொதுவில் பகிரும் வலைத்தள முகவரி.",
|
||||
"message.track-stats": "{target}க்கான புள்ளிவிவரங்களைக் கண்காணிக்க, {head}ல் பின்வரும் குறியீட்டை வைக்கவும்.",
|
||||
"message.type-delete": "உறுதிப்படுத்த கீழே உள்ள பெட்டியில் {delete} என தட்டச்சு செய்க.",
|
||||
"metrics.actions": "செயல்கள்",
|
||||
"metrics.average-visit-time": "சராசரி வருகை நேரம்",
|
||||
"metrics.bounce-rate": "துள்ளல் விகிதம்",
|
||||
"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": "பார்வையாளர்கள்"
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"label.accounts": "Облікові записи",
|
||||
"label.add-account": "Додати обліковий запис",
|
||||
"label.add-website": "Додати вебсайт",
|
||||
"label.add-website": "Додати сайт",
|
||||
"label.administrator": "Адміністратор",
|
||||
"label.all": "Всі",
|
||||
"label.all-websites": "Всі вебсайти",
|
||||
"label.all-websites": "Всі сайти",
|
||||
"label.back": "Назад",
|
||||
"label.cancel": "Відмінити",
|
||||
"label.change-password": "Змінити пароль",
|
||||
|
@ -14,15 +14,15 @@
|
|||
"label.custom-range": "Довільний період",
|
||||
"label.dashboard": "Інформаційна панель",
|
||||
"label.date-range": "Діапазон дат",
|
||||
"label.default-date-range": "Діапазон дат за умовчанням",
|
||||
"label.default-date-range": "Діапазон дат за замовчуванням",
|
||||
"label.delete": "Видалити",
|
||||
"label.delete-account": "Видалити обліковий запис",
|
||||
"label.delete-website": "Видалити вебсайт",
|
||||
"label.delete-website": "Видалити сайт",
|
||||
"label.dismiss": "Відхилити",
|
||||
"label.domain": "Домен",
|
||||
"label.edit": "Редагувати",
|
||||
"label.edit-account": "Редагувати обліковий запис",
|
||||
"label.edit-website": "Редагувати вебсайт",
|
||||
"label.edit-website": "Редагувати сайт",
|
||||
"label.enable-share-url": "Дозволити ділитися посиланням",
|
||||
"label.invalid": "Некоректний",
|
||||
"label.invalid-domain": "Некоректний домен",
|
||||
|
@ -46,16 +46,16 @@
|
|||
"label.settings": "Налаштування",
|
||||
"label.share-url": "Поділитися посилання",
|
||||
"label.single-day": "Один день",
|
||||
"label.this-month": "Поточний місяць",
|
||||
"label.this-week": "Поточний тиждень",
|
||||
"label.this-year": "Поточний рік",
|
||||
"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": "Вебсайти",
|
||||
"label.websites": "Сайти",
|
||||
"message.active-users": "{x} поточних відвідувачів",
|
||||
"message.confirm-delete": "Ви впевнені, що бажаєте видалити {target}?",
|
||||
"message.copied": "Скопійовано!",
|
||||
|
@ -68,19 +68,19 @@
|
|||
"message.log.visitor": "Відвідувач з {country} використовуючи {browser} на {os} {device}",
|
||||
"message.new-version-available": "Нова версія umami {version} доступна!",
|
||||
"message.no-data-available": "Немає даних.",
|
||||
"message.no-websites-configured": "У вас немає налаштованих вебсайтів.",
|
||||
"message.no-websites-configured": "У вас немає налаштованих сайтів.",
|
||||
"message.page-not-found": "Сторінку не знайдено.",
|
||||
"message.powered-by": "На базі {name}",
|
||||
"message.save-success": "Збережено успішно.",
|
||||
"message.share-url": "Це публічне посилання для {target}.",
|
||||
"message.track-stats": "Або відслідковувати статистику для {target}, розмістіть наступний код у {head} секції вашого вебсайту.",
|
||||
"message.track-stats": "Аби відслідковувати статистику для {target}, розмістіть наступний код у {head} секції вашого сайту.",
|
||||
"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": "Планшет",
|
||||
|
@ -89,7 +89,7 @@
|
|||
"metrics.filter.combined": "Об'єднані",
|
||||
"metrics.filter.domain-only": "Лише домен",
|
||||
"metrics.filter.raw": "Сирі дані",
|
||||
"metrics.operating-systems": "Операційна система",
|
||||
"metrics.operating-systems": "Операційні системи",
|
||||
"metrics.page-views": "Перегляди сторінок",
|
||||
"metrics.pages": "Сторінки",
|
||||
"metrics.referrers": "Джерела",
|
||||
|
|
|
@ -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": "访客"
|
||||
}
|
||||
|
|
259
lib/constants.js
259
lib/constants.js
|
@ -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',
|
||||
|
@ -116,7 +118,7 @@ export const BROWSERS = {
|
|||
edge: 'Edge',
|
||||
'edge-ios': 'Edge (iOS)',
|
||||
yandexbrowser: 'Yandex',
|
||||
kakaotalk: 'KKaoTalk',
|
||||
kakaotalk: 'KaKaoTalk',
|
||||
samsung: 'Samsung',
|
||||
silk: 'Silk',
|
||||
miui: 'MIUI',
|
||||
|
@ -140,3 +142,256 @@ export const BROWSERS = {
|
|||
'ios-webview': 'iOS (webview)',
|
||||
searchbot: 'Searchbot',
|
||||
};
|
||||
|
||||
export const MAP_FILE = '/datamaps.world.json';
|
||||
|
||||
export const ISO_COUNTRIES = {
|
||||
AFG: 'AF',
|
||||
ALA: 'AX',
|
||||
ALB: 'AL',
|
||||
DZA: 'DZ',
|
||||
ASM: 'AS',
|
||||
AND: 'AD',
|
||||
AGO: 'AO',
|
||||
AIA: 'AI',
|
||||
ATA: 'AQ',
|
||||
ATG: 'AG',
|
||||
ARG: 'AR',
|
||||
ARM: 'AM',
|
||||
ABW: 'AW',
|
||||
AUS: 'AU',
|
||||
AUT: 'AT',
|
||||
AZE: 'AZ',
|
||||
BHS: 'BS',
|
||||
BHR: 'BH',
|
||||
BGD: 'BD',
|
||||
BRB: 'BB',
|
||||
BLR: 'BY',
|
||||
BEL: 'BE',
|
||||
BLZ: 'BZ',
|
||||
BEN: 'BJ',
|
||||
BMU: 'BM',
|
||||
BTN: 'BT',
|
||||
BOL: 'BO',
|
||||
BIH: 'BA',
|
||||
BWA: 'BW',
|
||||
BVT: 'BV',
|
||||
BRA: 'BR',
|
||||
VGB: 'VG',
|
||||
IOT: 'IO',
|
||||
BRN: 'BN',
|
||||
BGR: 'BG',
|
||||
BFA: 'BF',
|
||||
BDI: 'BI',
|
||||
KHM: 'KH',
|
||||
CMR: 'CM',
|
||||
CAN: 'CA',
|
||||
CPV: 'CV',
|
||||
CYM: 'KY',
|
||||
CAF: 'CF',
|
||||
TCD: 'TD',
|
||||
CHL: 'CL',
|
||||
CHN: 'CN',
|
||||
HKG: 'HK',
|
||||
MAC: 'MO',
|
||||
CXR: 'CX',
|
||||
CCK: 'CC',
|
||||
COL: 'CO',
|
||||
COM: 'KM',
|
||||
COG: 'CG',
|
||||
COD: 'CD',
|
||||
COK: 'CK',
|
||||
CRI: 'CR',
|
||||
CIV: 'CI',
|
||||
HRV: 'HR',
|
||||
CUB: 'CU',
|
||||
CYP: 'CY',
|
||||
CZE: 'CZ',
|
||||
DNK: 'DK',
|
||||
DJI: 'DJ',
|
||||
DMA: 'DM',
|
||||
DOM: 'DO',
|
||||
ECU: 'EC',
|
||||
EGY: 'EG',
|
||||
SLV: 'SV',
|
||||
GNQ: 'GQ',
|
||||
ERI: 'ER',
|
||||
EST: 'EE',
|
||||
ETH: 'ET',
|
||||
FLK: 'FK',
|
||||
FRO: 'FO',
|
||||
FJI: 'FJ',
|
||||
FIN: 'FI',
|
||||
FRA: 'FR',
|
||||
GUF: 'GF',
|
||||
PYF: 'PF',
|
||||
ATF: 'TF',
|
||||
GAB: 'GA',
|
||||
GMB: 'GM',
|
||||
GEO: 'GE',
|
||||
DEU: 'DE',
|
||||
GHA: 'GH',
|
||||
GIB: 'GI',
|
||||
GRC: 'GR',
|
||||
GRL: 'GL',
|
||||
GRD: 'GD',
|
||||
GLP: 'GP',
|
||||
GUM: 'GU',
|
||||
GTM: 'GT',
|
||||
GGY: 'GG',
|
||||
GIN: 'GN',
|
||||
GNB: 'GW',
|
||||
GUY: 'GY',
|
||||
HTI: 'HT',
|
||||
HMD: 'HM',
|
||||
VAT: 'VA',
|
||||
HND: 'HN',
|
||||
HUN: 'HU',
|
||||
ISL: 'IS',
|
||||
IND: 'IN',
|
||||
IDN: 'ID',
|
||||
IRN: 'IR',
|
||||
IRQ: 'IQ',
|
||||
IRL: 'IE',
|
||||
IMN: 'IM',
|
||||
ISR: 'IL',
|
||||
ITA: 'IT',
|
||||
JAM: 'JM',
|
||||
JPN: 'JP',
|
||||
JEY: 'JE',
|
||||
JOR: 'JO',
|
||||
KAZ: 'KZ',
|
||||
KEN: 'KE',
|
||||
KIR: 'KI',
|
||||
PRK: 'KP',
|
||||
KOR: 'KR',
|
||||
KWT: 'KW',
|
||||
KGZ: 'KG',
|
||||
LAO: 'LA',
|
||||
LVA: 'LV',
|
||||
LBN: 'LB',
|
||||
LSO: 'LS',
|
||||
LBR: 'LR',
|
||||
LBY: 'LY',
|
||||
LIE: 'LI',
|
||||
LTU: 'LT',
|
||||
LUX: 'LU',
|
||||
MKD: 'MK',
|
||||
MDG: 'MG',
|
||||
MWI: 'MW',
|
||||
MYS: 'MY',
|
||||
MDV: 'MV',
|
||||
MLI: 'ML',
|
||||
MLT: 'MT',
|
||||
MHL: 'MH',
|
||||
MTQ: 'MQ',
|
||||
MRT: 'MR',
|
||||
MUS: 'MU',
|
||||
MYT: 'YT',
|
||||
MEX: 'MX',
|
||||
FSM: 'FM',
|
||||
MDA: 'MD',
|
||||
MCO: 'MC',
|
||||
MNG: 'MN',
|
||||
MNE: 'ME',
|
||||
MSR: 'MS',
|
||||
MAR: 'MA',
|
||||
MOZ: 'MZ',
|
||||
MMR: 'MM',
|
||||
NAM: 'NA',
|
||||
NRU: 'NR',
|
||||
NPL: 'NP',
|
||||
NLD: 'NL',
|
||||
ANT: 'AN',
|
||||
NCL: 'NC',
|
||||
NZL: 'NZ',
|
||||
NIC: 'NI',
|
||||
NER: 'NE',
|
||||
NGA: 'NG',
|
||||
NIU: 'NU',
|
||||
NFK: 'NF',
|
||||
MNP: 'MP',
|
||||
NOR: 'NO',
|
||||
OMN: 'OM',
|
||||
PAK: 'PK',
|
||||
PLW: 'PW',
|
||||
PSE: 'PS',
|
||||
PAN: 'PA',
|
||||
PNG: 'PG',
|
||||
PRY: 'PY',
|
||||
PER: 'PE',
|
||||
PHL: 'PH',
|
||||
PCN: 'PN',
|
||||
POL: 'PL',
|
||||
PRT: 'PT',
|
||||
PRI: 'PR',
|
||||
QAT: 'QA',
|
||||
REU: 'RE',
|
||||
ROU: 'RO',
|
||||
RUS: 'RU',
|
||||
RWA: 'RW',
|
||||
BLM: 'BL',
|
||||
SHN: 'SH',
|
||||
KNA: 'KN',
|
||||
LCA: 'LC',
|
||||
MAF: 'MF',
|
||||
SPM: 'PM',
|
||||
VCT: 'VC',
|
||||
WSM: 'WS',
|
||||
SMR: 'SM',
|
||||
STP: 'ST',
|
||||
SAU: 'SA',
|
||||
SEN: 'SN',
|
||||
SRB: 'RS',
|
||||
SYC: 'SC',
|
||||
SLE: 'SL',
|
||||
SGP: 'SG',
|
||||
SVK: 'SK',
|
||||
SVN: 'SI',
|
||||
SLB: 'SB',
|
||||
SOM: 'SO',
|
||||
ZAF: 'ZA',
|
||||
SGS: 'GS',
|
||||
SSD: 'SS',
|
||||
ESP: 'ES',
|
||||
LKA: 'LK',
|
||||
SDN: 'SD',
|
||||
SUR: 'SR',
|
||||
SJM: 'SJ',
|
||||
SWZ: 'SZ',
|
||||
SWE: 'SE',
|
||||
CHE: 'CH',
|
||||
SYR: 'SY',
|
||||
TWN: 'TW',
|
||||
TJK: 'TJ',
|
||||
TZA: 'TZ',
|
||||
THA: 'TH',
|
||||
TLS: 'TL',
|
||||
TGO: 'TG',
|
||||
TKL: 'TK',
|
||||
TON: 'TO',
|
||||
TTO: 'TT',
|
||||
TUN: 'TN',
|
||||
TUR: 'TR',
|
||||
TKM: 'TM',
|
||||
TCA: 'TC',
|
||||
TUV: 'TV',
|
||||
UGA: 'UG',
|
||||
UKR: 'UA',
|
||||
ARE: 'AE',
|
||||
GBR: 'GB',
|
||||
USA: 'US',
|
||||
UMI: 'UM',
|
||||
URY: 'UY',
|
||||
UZB: 'UZ',
|
||||
VUT: 'VU',
|
||||
VEN: 'VE',
|
||||
VNM: 'VN',
|
||||
VIR: 'VI',
|
||||
WLF: 'WF',
|
||||
ESH: 'EH',
|
||||
YEM: 'YE',
|
||||
ZMB: 'ZM',
|
||||
ZWE: 'ZW',
|
||||
XKX: 'XK',
|
||||
};
|
||||
|
|
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);
|
||||
|
|
85
lib/lang.js
85
lib/lang.js
|
@ -1,24 +1,33 @@
|
|||
import { format } from 'date-fns';
|
||||
import {
|
||||
cs,
|
||||
sk,
|
||||
da,
|
||||
de,
|
||||
el,
|
||||
enUS,
|
||||
es,
|
||||
fi,
|
||||
fr,
|
||||
faIR,
|
||||
he,
|
||||
hi,
|
||||
id,
|
||||
it,
|
||||
ja,
|
||||
ms,
|
||||
nb,
|
||||
nl,
|
||||
pl,
|
||||
pt,
|
||||
ptBR,
|
||||
ro,
|
||||
ru,
|
||||
sv,
|
||||
ta,
|
||||
tr,
|
||||
uk,
|
||||
zhCN,
|
||||
zhTW,
|
||||
tr,
|
||||
ru,
|
||||
de,
|
||||
ja,
|
||||
es,
|
||||
fr,
|
||||
da,
|
||||
sv,
|
||||
el,
|
||||
pt,
|
||||
ro,
|
||||
nb,
|
||||
id,
|
||||
uk,
|
||||
fi,
|
||||
} from 'date-fns/locale';
|
||||
import enMessages from 'lang-compiled/en-US.json';
|
||||
import nlMessages from 'lang-compiled/nl-NL.json';
|
||||
|
@ -36,11 +45,21 @@ import svMessages from 'lang-compiled/sv-SE.json';
|
|||
import grMessages from 'lang-compiled/el-GR.json';
|
||||
import foMessages from 'lang-compiled/fo-FO.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 nbNOMessages from 'lang-compiled/nb-NO.json';
|
||||
import idMessages from 'lang-compiled/id-ID.json';
|
||||
import ukMessages from 'lang-compiled/uk-UA.json';
|
||||
import fiMessages from 'lang-compiled/fi-FI.json';
|
||||
import csMessages from 'lang-compiled/cs-CZ.json';
|
||||
import skMessages from 'lang-compiled/sk-SK.json';
|
||||
import plMessages from 'lang-compiled/pl-PL.json';
|
||||
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';
|
||||
import msMYMessages from 'lang-compiled/ms-MY.json';
|
||||
|
||||
export const messages = {
|
||||
'en-US': enMessages,
|
||||
|
@ -59,11 +78,21 @@ export const messages = {
|
|||
'el-GR': grMessages,
|
||||
'fo-FO': foMessages,
|
||||
'pt-PT': ptMessages,
|
||||
'pt-BR': ptBRMessages,
|
||||
'ro-RO': roMessages,
|
||||
'nb-NO': nbNOMessages,
|
||||
'id-ID': idMessages,
|
||||
'uk-UA': ukMessages,
|
||||
'fi-FI': fiMessages,
|
||||
'cs-CZ': csMessages,
|
||||
'sk-SK': skMessages,
|
||||
'pl-PL': plMessages,
|
||||
'ta-IN': taMessages,
|
||||
'hi-IN': hiMessages,
|
||||
'he-IL': heMessages,
|
||||
'it-IT': itMessages,
|
||||
'fa-IR': faIRMessages,
|
||||
'ms-MY': msMYMessages,
|
||||
};
|
||||
|
||||
export const dateLocales = {
|
||||
|
@ -83,37 +112,53 @@ export const dateLocales = {
|
|||
'el-GR': el,
|
||||
'fo-FO': da,
|
||||
'pt-PT': pt,
|
||||
'pt-BR': ptBR,
|
||||
'ro-RO': ro,
|
||||
'nb-NO': nb,
|
||||
'id-ID': id,
|
||||
'uk-UA': uk,
|
||||
'fi-FI': fi,
|
||||
'cs-CZ': cs,
|
||||
'sk-SK': sk,
|
||||
'pl-PL': pl,
|
||||
'ta-In': ta,
|
||||
'hi-IN': hi,
|
||||
'he-IL': he,
|
||||
'it-IT': it,
|
||||
'fa-IR': faIR,
|
||||
'ms-MY': ms,
|
||||
};
|
||||
|
||||
export const menuOptions = [
|
||||
{ label: '中文', value: 'zh-CN', display: 'cn' },
|
||||
{ label: '中文(繁體)', value: 'zh-TW', display: 'tw' },
|
||||
{ label: 'Čeština', value: 'cs-CZ', display: 'cs' },
|
||||
{ label: 'Dansk', value: 'da-DK', display: 'da' },
|
||||
{ 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' },
|
||||
{ label: 'עברית', value: 'he-IL', display: 'he' },
|
||||
{ label: 'हिन्दी', value: 'hi-IN', display: 'hi' },
|
||||
{ label: 'Italiano', value: 'it-IT', display: 'it' },
|
||||
{ label: 'Bahasa Indonesia', value: 'id-ID', display: 'id' },
|
||||
{ label: '日本語', value: 'ja-JP', display: 'ja' },
|
||||
{ label: 'Malay', value: 'ms-MY', display: 'ms' },
|
||||
{ label: 'Монгол', value: 'mn-MN', display: 'mn' },
|
||||
{ label: 'Nederlands', value: 'nl-NL', display: 'nl' },
|
||||
{ label: 'Norsk Bokmål', value: 'nb-NO', display: 'nb' },
|
||||
{ label: 'Polski', value: 'pl-PL', display: 'pl' },
|
||||
{ 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: 'Română', value: 'ro-RO', display: 'ro' },
|
||||
{ label: 'Slovenčina', value: 'sk-SK', display: 'sk' },
|
||||
{ label: 'Suomi', value: 'fi-FI', display: 'fi' },
|
||||
{ label: 'Svenska', value: 'sv-SE', display: 'sv' },
|
||||
{ label: 'தமிழ்', value: 'ta-IN', display: 'ta' },
|
||||
{ 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 =
|
||||
|
@ -68,7 +74,7 @@ export function getTimestampInterval(field) {
|
|||
|
||||
export async function getWebsiteById(website_id) {
|
||||
return runQuery(
|
||||
prisma.website.findOne({
|
||||
prisma.website.findUnique({
|
||||
where: {
|
||||
website_id,
|
||||
},
|
||||
|
@ -78,7 +84,7 @@ export async function getWebsiteById(website_id) {
|
|||
|
||||
export async function getWebsiteByUuid(website_uuid) {
|
||||
return runQuery(
|
||||
prisma.website.findOne({
|
||||
prisma.website.findUnique({
|
||||
where: {
|
||||
website_uuid,
|
||||
},
|
||||
|
@ -88,7 +94,7 @@ export async function getWebsiteByUuid(website_uuid) {
|
|||
|
||||
export async function getWebsiteByShareId(share_id) {
|
||||
return runQuery(
|
||||
prisma.website.findOne({
|
||||
prisma.website.findUnique({
|
||||
where: {
|
||||
share_id,
|
||||
},
|
||||
|
@ -152,11 +158,7 @@ export async function createSession(website_id, data) {
|
|||
return runQuery(
|
||||
prisma.session.create({
|
||||
data: {
|
||||
website: {
|
||||
connect: {
|
||||
website_id,
|
||||
},
|
||||
},
|
||||
website_id,
|
||||
...data,
|
||||
},
|
||||
select: {
|
||||
|
@ -168,7 +170,7 @@ export async function createSession(website_id, data) {
|
|||
|
||||
export async function getSessionByUuid(session_uuid) {
|
||||
return runQuery(
|
||||
prisma.session.findOne({
|
||||
prisma.session.findUnique({
|
||||
where: {
|
||||
session_uuid,
|
||||
},
|
||||
|
@ -180,18 +182,10 @@ export async function savePageView(website_id, session_id, url, referrer) {
|
|||
return runQuery(
|
||||
prisma.pageview.create({
|
||||
data: {
|
||||
website: {
|
||||
connect: {
|
||||
website_id,
|
||||
},
|
||||
},
|
||||
session: {
|
||||
connect: {
|
||||
session_id,
|
||||
},
|
||||
},
|
||||
url,
|
||||
referrer,
|
||||
website_id,
|
||||
session_id,
|
||||
url: url?.substr(0, URL_LENGTH),
|
||||
referrer: referrer?.substr(0, URL_LENGTH),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
@ -201,19 +195,11 @@ export async function saveEvent(website_id, session_id, url, event_type, event_v
|
|||
return runQuery(
|
||||
prisma.event.create({
|
||||
data: {
|
||||
website: {
|
||||
connect: {
|
||||
website_id,
|
||||
},
|
||||
},
|
||||
session: {
|
||||
connect: {
|
||||
session_id,
|
||||
},
|
||||
},
|
||||
url,
|
||||
event_type,
|
||||
event_value,
|
||||
website_id,
|
||||
session_id,
|
||||
url: url?.substr(0, URL_LENGTH),
|
||||
event_type: event_type?.substr(0, 50),
|
||||
event_value: event_value?.substr(0, 50),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
@ -225,7 +211,7 @@ export async function getAccounts() {
|
|||
|
||||
export async function getAccountById(user_id) {
|
||||
return runQuery(
|
||||
prisma.account.findOne({
|
||||
prisma.account.findUnique({
|
||||
where: {
|
||||
user_id,
|
||||
},
|
||||
|
@ -235,7 +221,7 @@ export async function getAccountById(user_id) {
|
|||
|
||||
export async function getAccountByUsername(username) {
|
||||
return runQuery(
|
||||
prisma.account.findOne({
|
||||
prisma.account.findUnique({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
|
@ -428,7 +414,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) {
|
||||
|
|
|
@ -6,6 +6,7 @@ module.exports = {
|
|||
VERSION: pkg.version,
|
||||
FORCE_SSL: !!process.env.FORCE_SSL,
|
||||
},
|
||||
basePath: process.env.BASE_PATH,
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
|
@ -28,6 +29,6 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
];
|
||||
},
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue