mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-18 15:23:38 +01:00
commit
f13d7cfed5
1
assets/bolt.svg
Normal file
1
assets/bolt.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M296 160H180.6l42.6-129.8C227.2 15 215.7 0 200 0H56C44 0 33.8 8.9 32.2 20.8l-32 240C-1.7 275.2 9.5 288 24 288h118.7L96.6 482.5c-3.6 15.2 8 29.5 23.3 29.5 8.4 0 16.4-4.4 20.8-12l176-304c9.3-15.9-2.2-36-20.7-36z"/></svg>
|
After Width: | Height: | Size: 289 B |
1
assets/eye.svg
Normal file
1
assets/eye.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z"/></svg>
|
After Width: | Height: | Size: 509 B |
1
assets/visitor.svg
Normal file
1
assets/visitor.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z"/></svg>
|
After Width: | Height: | Size: 336 B |
@ -3,7 +3,7 @@ import Button from './Button';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const defaultText = (
|
||||
<FormattedMessage id="button.copy-to-clipboard" defaultMessage="Copy to clipboard" />
|
||||
<FormattedMessage id="label.copy-to-clipboard" defaultMessage="Copy to clipboard" />
|
||||
);
|
||||
|
||||
export default function CopyButton({ element, ...props }) {
|
||||
|
15
components/common/Dot.js
Normal file
15
components/common/Dot.js
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Dot.module.css';
|
||||
|
||||
export default function Dot({ color, size, className }) {
|
||||
return (
|
||||
<div
|
||||
style={{ background: color }}
|
||||
className={classNames(styles.dot, className, {
|
||||
[styles.small]: size === 'small',
|
||||
[styles.large]: size === 'large',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
17
components/common/Dot.module.css
Normal file
17
components/common/Dot.module.css
Normal file
@ -0,0 +1,17 @@
|
||||
.dot {
|
||||
background: var(--green400);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.dot.small {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.dot.large {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
@ -6,7 +6,7 @@ import styles from './EmptyPlaceholder.module.css';
|
||||
export default function EmptyPlaceholder({ msg, children }) {
|
||||
return (
|
||||
<div className={styles.placeholder}>
|
||||
<Icon icon={<Logo />} size="xlarge" />
|
||||
<Icon className={styles.icon} icon={<Logo />} size="xlarge" />
|
||||
<h2>{msg}</h2>
|
||||
{children}
|
||||
</div>
|
||||
|
@ -5,3 +5,7 @@
|
||||
align-items: center;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
11
components/common/FilterButtons.js
Normal file
11
components/common/FilterButtons.js
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import ButtonGroup from './ButtonGroup';
|
||||
|
||||
export default function FilterButtons({ buttons, selected, onClick }) {
|
||||
return (
|
||||
<ButtonLayout>
|
||||
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
||||
</ButtonLayout>
|
||||
);
|
||||
}
|
@ -12,7 +12,7 @@ export default function RefreshButton({ websiteId }) {
|
||||
const dispatch = useDispatch();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const completed = useSelector(state => state.queries[`/api/website/${websiteId}/metrics`]);
|
||||
const completed = useSelector(state => state.queries[`/api/website/${websiteId}/stats`]);
|
||||
|
||||
function handleClick() {
|
||||
if (dateRange) {
|
||||
@ -28,7 +28,7 @@ export default function RefreshButton({ websiteId }) {
|
||||
return (
|
||||
<Button
|
||||
icon={loading ? <Dots /> : <Refresh />}
|
||||
tooltip={<FormattedMessage id="button.refresh" defaultMessage="Refresh" />}
|
||||
tooltip={<FormattedMessage id="label.refresh" defaultMessage="Refresh" />}
|
||||
tooltipId="button-refresh"
|
||||
size="small"
|
||||
onClick={handleClick}
|
||||
|
@ -3,40 +3,58 @@ import classNames from 'classnames';
|
||||
import NoData from 'components/common/NoData';
|
||||
import styles from './Table.module.css';
|
||||
|
||||
export default function Table({ columns, rows, empty }) {
|
||||
export default function Table({
|
||||
columns,
|
||||
rows,
|
||||
empty,
|
||||
className,
|
||||
bodyClassName,
|
||||
rowKey,
|
||||
showHeader = true,
|
||||
children,
|
||||
}) {
|
||||
if (empty && rows.length === 0) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.table}>
|
||||
<div className={classNames(styles.header, 'row')}>
|
||||
{columns.map(({ key, label, className, style, header }) => (
|
||||
<div
|
||||
key={key}
|
||||
className={classNames(styles.head, className, header?.className)}
|
||||
style={{ ...style, ...header?.style }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
<div className={classNames(styles.table, className)}>
|
||||
{showHeader && (
|
||||
<div className={classNames(styles.header, 'row')}>
|
||||
{columns.map(({ key, label, className, style, header }) => (
|
||||
<div
|
||||
key={key}
|
||||
className={classNames(styles.head, className, header?.className)}
|
||||
style={{ ...style, ...header?.style }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className={classNames(styles.body, bodyClassName)}>
|
||||
{rows.length === 0 && <NoData />}
|
||||
{rows.map((row, rowIndex) => (
|
||||
<div className={classNames(styles.row, 'row')} key={rowIndex}>
|
||||
{columns.map(({ key, render, className, style, cell }) => (
|
||||
<div
|
||||
key={`${rowIndex}${key}`}
|
||||
className={classNames(styles.cell, className, cell?.className)}
|
||||
style={{ ...style, ...cell?.style }}
|
||||
>
|
||||
{render ? render(row) : row[key]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
{!children &&
|
||||
rows.map((row, index) => {
|
||||
const id = rowKey ? rowKey(row) : index;
|
||||
return <TableRow key={id} columns={columns} row={row} />;
|
||||
})}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const TableRow = ({ columns, row }) => (
|
||||
<div className={classNames(styles.row, 'row')}>
|
||||
{columns.map(({ key, render, className, style, cell }, index) => (
|
||||
<div
|
||||
key={`${key}-${index}`}
|
||||
className={classNames(styles.cell, className, cell?.className)}
|
||||
style={{ ...style, ...cell?.style }}
|
||||
>
|
||||
{render ? render(row) : row[key]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
7
components/common/Tag.js
Normal file
7
components/common/Tag.js
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Tag.module.css';
|
||||
|
||||
export default function Tag({ className, children }) {
|
||||
return <span className={classNames(styles.tag, className)}>{children}</span>;
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
.type {
|
||||
font-size: var(--font-size-small);
|
||||
.tag {
|
||||
padding: 2px 4px;
|
||||
border: 1px solid var(--gray300);
|
||||
border-radius: 4px;
|
@ -36,10 +36,10 @@ export default function UpdateNotice() {
|
||||
</div>
|
||||
<ButtonLayout>
|
||||
<Button size="xsmall" variant="action" onClick={handleViewClick}>
|
||||
<FormattedMessage id="button.view-details" defaultMessage="View details" />
|
||||
<FormattedMessage id="label.view-details" defaultMessage="View details" />
|
||||
</Button>
|
||||
<Button size="xsmall" onClick={handleDismissClick}>
|
||||
<FormattedMessage id="button.dismiss" defaultMessage="Dismiss" />
|
||||
<FormattedMessage id="label.dismiss" defaultMessage="Dismiss" />
|
||||
</Button>
|
||||
</ButtonLayout>
|
||||
</div>
|
||||
|
@ -70,10 +70,10 @@ export default function AccountEditForm({ values, onSave, onClose }) {
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
<FormattedMessage id="button.save" defaultMessage="Save" />
|
||||
<FormattedMessage id="label.save" defaultMessage="Save" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
|
@ -85,10 +85,10 @@ export default function ChangePasswordForm({ values, onSave, onClose }) {
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
<FormattedMessage id="button.save" defaultMessage="Save" />
|
||||
<FormattedMessage id="label.save" defaultMessage="Save" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
|
@ -33,11 +33,11 @@ export default function DatePickerForm({
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: <FormattedMessage id="button.single-day" defaultMessage="Single day" />,
|
||||
label: <FormattedMessage id="label.single-day" defaultMessage="Single day" />,
|
||||
value: FILTER_DAY,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="button.date-range" defaultMessage="Date range" />,
|
||||
label: <FormattedMessage id="label.date-range" defaultMessage="Date range" />,
|
||||
value: FILTER_RANGE,
|
||||
},
|
||||
];
|
||||
@ -72,10 +72,10 @@ export default function DatePickerForm({
|
||||
</div>
|
||||
<FormButtons>
|
||||
<Button variant="action" onClick={handleSave} disabled={disabled}>
|
||||
<FormattedMessage id="button.save" defaultMessage="Save" />
|
||||
<FormattedMessage id="label.save" defaultMessage="Save" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</div>
|
||||
|
@ -82,10 +82,10 @@ export default function DeleteForm({ values, onSave, onClose }) {
|
||||
variant="danger"
|
||||
disabled={props.values.confirmation !== CONFIRMATION_WORD}
|
||||
>
|
||||
<FormattedMessage id="button.delete" defaultMessage="Delete" />
|
||||
<FormattedMessage id="label.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
|
@ -83,7 +83,7 @@ export default function LoginForm() {
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
<FormattedMessage id="button.login" defaultMessage="Login" />
|
||||
<FormattedMessage id="label.login" defaultMessage="Login" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
|
@ -30,7 +30,7 @@ export default function TrackingCodeForm({ values, onClose }) {
|
||||
<FormButtons>
|
||||
<CopyButton type="submit" variant="action" element={ref} />
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</FormLayout>
|
||||
|
@ -29,7 +29,7 @@ export default function TrackingCodeForm({ values, onClose }) {
|
||||
<FormButtons>
|
||||
<CopyButton type="submit" variant="action" element={ref} />
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</FormLayout>
|
||||
|
@ -91,10 +91,10 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="action">
|
||||
<FormattedMessage id="button.save" defaultMessage="Save" />
|
||||
<FormattedMessage id="label.save" defaultMessage="Save" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="button.cancel" defaultMessage="Cancel" />
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
|
@ -24,7 +24,9 @@ export default function Footer() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={classNames(styles.version, 'col-12 col-md-4')}>{`v${current}`}</div>
|
||||
<div className={classNames(styles.version, 'col-12 col-md-4')}>
|
||||
<Link href={`https://github.com/mikecao/umami/releases`}>{`v${current}`}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
31
components/layout/GridLayout.js
Normal file
31
components/layout/GridLayout.js
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './GridLayout.module.css';
|
||||
|
||||
export default function GridLayout({ className, children }) {
|
||||
return <div className={classNames(styles.grid, className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export const GridRow = ({ className, children }) => {
|
||||
return <div className={classNames(styles.row, className, 'row')}>{children}</div>;
|
||||
};
|
||||
|
||||
export const GridColumn = ({ xs, sm, md, lg, xl, className, children }) => {
|
||||
const classes = [];
|
||||
|
||||
classes.push(xs ? `col-${xs}` : 'col-12');
|
||||
|
||||
if (sm) {
|
||||
classes.push(`col-sm-${sm}`);
|
||||
}
|
||||
if (md) {
|
||||
classes.push(`col-md-${md}`);
|
||||
}
|
||||
if (lg) {
|
||||
classes.push(`col-lg-${lg}`);
|
||||
}
|
||||
if (xl) {
|
||||
classes.push(`col-lg-${xl}`);
|
||||
}
|
||||
return <div className={classNames(styles.col, classes, className)}>{children}</div>;
|
||||
};
|
40
components/layout/GridLayout.module.css
Normal file
40
components/layout/GridLayout.module.css
Normal file
@ -0,0 +1,40 @@
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
border-top: 1px solid var(--gray300);
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.row > .col {
|
||||
border-left: 1px solid var(--gray300);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.row > .col:first-child {
|
||||
border-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.row > .col:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.row {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.row > .col {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-left: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
@ -30,6 +30,9 @@ export default function Header() {
|
||||
<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>
|
||||
|
@ -1,11 +1,18 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import Dot from 'components/common/Dot';
|
||||
import { TOKEN_HEADER } from 'lib/constants';
|
||||
import useShareToken from 'hooks/useShareToken';
|
||||
import styles from './ActiveUsers.module.css';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default function ActiveUsers({ websiteId, token, className }) {
|
||||
const { data } = useFetch(`/api/website/${websiteId}/active`, { token }, { interval: 60000 });
|
||||
export default function ActiveUsers({ websiteId, className }) {
|
||||
const shareToken = useShareToken();
|
||||
const { data } = useFetch(`/api/website/${websiteId}/active`, {
|
||||
interval: 60000,
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
});
|
||||
const count = useMemo(() => {
|
||||
return data?.[0]?.x || 0;
|
||||
}, [data]);
|
||||
@ -16,7 +23,7 @@ export default function ActiveUsers({ websiteId, token, className }) {
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<div className={styles.dot} />
|
||||
<Dot />
|
||||
<div className={styles.text}>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
|
@ -12,11 +12,3 @@
|
||||
font-weight: 600;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
background: var(--green400);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 100%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
@ -5,17 +5,17 @@ import ChartJS from 'chart.js';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { dateFormat } from 'lib/lang';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import styles from './BarChart.module.css';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { THEME_COLORS } from 'lib/constants';
|
||||
import { DEFAUL_CHART_HEIGHT, DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
|
||||
import styles from './BarChart.module.css';
|
||||
|
||||
export default function BarChart({
|
||||
chartId,
|
||||
datasets,
|
||||
unit,
|
||||
records,
|
||||
height = 400,
|
||||
animationDuration = 300,
|
||||
height = DEFAUL_CHART_HEIGHT,
|
||||
animationDuration = DEFAULT_ANIMATION_DURATION,
|
||||
className,
|
||||
stacked = false,
|
||||
loading = false,
|
||||
@ -39,6 +39,8 @@ export default function BarChart({
|
||||
const w = canvas.current.width;
|
||||
|
||||
switch (unit) {
|
||||
case 'minute':
|
||||
return index % 2 === 0 ? dateFormat(d, 'h:mm', locale) : '';
|
||||
case 'hour':
|
||||
return dateFormat(d, 'ha', locale);
|
||||
case 'day':
|
||||
@ -63,7 +65,7 @@ export default function BarChart({
|
||||
}
|
||||
|
||||
function renderYLabel(label) {
|
||||
return +label > 1 ? formatLongNumber(label) : label;
|
||||
return +label > 1000 ? formatLongNumber(label) : label;
|
||||
}
|
||||
|
||||
function renderTooltip(model) {
|
||||
|
@ -3,15 +3,14 @@ import { FormattedMessage } from 'react-intl';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import { browserFilter } from 'lib/filters';
|
||||
|
||||
export default function BrowsersTable({ websiteId, token, limit }) {
|
||||
export default function BrowsersTable({ websiteId, ...props }) {
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />}
|
||||
type="browser"
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
limit={limit}
|
||||
dataFilter={browserFilter}
|
||||
/>
|
||||
);
|
||||
|
@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export default function CountriesTable({ websiteId, token, limit, onDataLoad = () => {} }) {
|
||||
export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
|
||||
const [locale] = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
|
||||
@ -15,13 +15,12 @@ export default function CountriesTable({ websiteId, token, limit, onDataLoad = (
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
|
||||
type="country"
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
limit={limit}
|
||||
onDataLoad={data => onDataLoad(percentFilter(data))}
|
||||
onDataLoad={data => onDataLoad?.(percentFilter(data))}
|
||||
renderLabel={renderLabel}
|
||||
/>
|
||||
);
|
||||
|
91
components/metrics/DataTable.js
Normal file
91
components/metrics/DataTable.js
Normal file
@ -0,0 +1,91 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { useSpring, animated, config } from 'react-spring';
|
||||
import classNames from 'classnames';
|
||||
import NoData from 'components/common/NoData';
|
||||
import { formatNumber, formatLongNumber } from 'lib/format';
|
||||
import styles from './DataTable.module.css';
|
||||
|
||||
export default function DataTable({
|
||||
data,
|
||||
title,
|
||||
metric,
|
||||
className,
|
||||
renderLabel,
|
||||
height,
|
||||
animate = true,
|
||||
virtualize = false,
|
||||
}) {
|
||||
const [format, setFormat] = useState(true);
|
||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||
|
||||
const handleSetFormat = () => setFormat(state => !state);
|
||||
|
||||
const getRow = row => {
|
||||
const { x: label, y: value, z: percent } = row;
|
||||
|
||||
return (
|
||||
<AnimatedRow
|
||||
key={label}
|
||||
label={renderLabel ? renderLabel(row) : label}
|
||||
value={value}
|
||||
percent={percent}
|
||||
animate={animate && !virtualize}
|
||||
format={formatFunc}
|
||||
onClick={handleSetFormat}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Row = ({ index, style }) => {
|
||||
return <div style={style}>{getRow(data[index])}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.table, className)}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.metric} onClick={handleSetFormat}>
|
||||
{metric}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.body} style={{ height }}>
|
||||
{data?.length === 0 && <NoData />}
|
||||
{virtualize && data.length > 0 ? (
|
||||
<FixedSizeList height={height} itemCount={data.length} itemSize={30}>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
) : (
|
||||
data.map(row => getRow(row))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => {
|
||||
const props = useSpring({
|
||||
width: percent,
|
||||
y: value,
|
||||
from: { width: 0, y: 0 },
|
||||
config: animate ? config.default : { duration: 0 },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
<div className={styles.value} onClick={onClick}>
|
||||
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
|
||||
</div>
|
||||
<div className={styles.percent}>
|
||||
<animated.div
|
||||
className={styles.bar}
|
||||
style={{ width: props.width.interpolate(n => `${n}%`) }}
|
||||
/>
|
||||
<animated.span className={styles.percentValue}>
|
||||
{props.width.interpolate(n => `${n.toFixed(0)}%`)}
|
||||
</animated.span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
95
components/metrics/DataTable.module.css
Normal file
95
components/metrics/DataTable.module.css
Normal file
@ -0,0 +1,95 @@
|
||||
.table {
|
||||
position: relative;
|
||||
font-size: var(--font-size-small);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-normal);
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.row {
|
||||
position: relative;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.label a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.label a:hover {
|
||||
color: var(--primary400);
|
||||
}
|
||||
|
||||
.label:empty {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
.label:empty:before {
|
||||
content: 'Unknown';
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.percent {
|
||||
position: relative;
|
||||
width: 50px;
|
||||
color: var(--gray600);
|
||||
border-left: 1px solid var(--gray600);
|
||||
padding-left: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 30px;
|
||||
opacity: 0.1;
|
||||
background: var(--primary400);
|
||||
z-index: -1;
|
||||
}
|
@ -4,15 +4,14 @@ import { deviceFilter } from 'lib/filters';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { getDeviceMessage } from 'components/messages';
|
||||
|
||||
export default function DevicesTable({ websiteId, token, limit }) {
|
||||
export default function DevicesTable({ websiteId, ...props }) {
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.devices" defaultMessage="Devices" />}
|
||||
type="device"
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
limit={limit}
|
||||
dataFilter={deviceFilter}
|
||||
renderLabel={({ x }) => getDeviceMessage(x)}
|
||||
/>
|
||||
|
@ -5,26 +5,31 @@ import { getDateArray, getDateLength } from 'lib/date';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import useTimezone from 'hooks/useTimezone';
|
||||
import { EVENT_COLORS } from 'lib/constants';
|
||||
import usePageQuery from '../../hooks/usePageQuery';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import useShareToken from 'hooks/useShareToken';
|
||||
import { EVENT_COLORS, TOKEN_HEADER } from 'lib/constants';
|
||||
|
||||
export default function EventsChart({ websiteId, token }) {
|
||||
export default function EventsChart({ websiteId, className, token }) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit, modified } = dateRange;
|
||||
const [timezone] = useTimezone();
|
||||
const { query } = usePageQuery();
|
||||
const shareToken = useShareToken();
|
||||
|
||||
const { data } = useFetch(
|
||||
`/api/website/${websiteId}/events`,
|
||||
{
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
unit,
|
||||
tz: timezone,
|
||||
url: query.url,
|
||||
token,
|
||||
params: {
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
unit,
|
||||
tz: timezone,
|
||||
url: query.url,
|
||||
token,
|
||||
},
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
},
|
||||
{ update: [modified] },
|
||||
[modified],
|
||||
);
|
||||
const datasets = useMemo(() => {
|
||||
if (!data) return [];
|
||||
@ -44,7 +49,7 @@ export default function EventsChart({ websiteId, token }) {
|
||||
});
|
||||
|
||||
return Object.keys(map).map((key, index) => {
|
||||
const color = tinycolor(EVENT_COLORS[index]);
|
||||
const color = tinycolor(EVENT_COLORS[index % EVENT_COLORS.length]);
|
||||
return {
|
||||
label: key,
|
||||
data: map[key],
|
||||
@ -77,6 +82,7 @@ export default function EventsChart({ websiteId, token }) {
|
||||
return (
|
||||
<BarChart
|
||||
chartId={`events-${websiteId}`}
|
||||
className={className}
|
||||
datasets={datasets}
|
||||
unit={unit}
|
||||
records={getDateLength(startDate, endDate, unit)}
|
||||
|
@ -1,19 +1,17 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import styles from './EventsTable.module.css';
|
||||
import Tag from 'components/common/Tag';
|
||||
|
||||
export default function EventsTable({ websiteId, token, limit, onDataLoad }) {
|
||||
export default function EventsTable({ websiteId, ...props }) {
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
|
||||
type="event"
|
||||
metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />}
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
limit={limit}
|
||||
renderLabel={({ x }) => <Label value={x} />}
|
||||
onDataLoad={onDataLoad}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -22,7 +20,7 @@ const Label = ({ value }) => {
|
||||
const [event, label] = value.split(':');
|
||||
return (
|
||||
<>
|
||||
<span className={styles.type}>{event}</span>
|
||||
<Tag>{event}</Tag>
|
||||
{label}
|
||||
</>
|
||||
);
|
||||
|
@ -5,12 +5,15 @@ import Loading from 'components/common/Loading';
|
||||
import ErrorMessage from 'components/common/ErrorMessage';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import useShareToken from 'hooks/useShareToken';
|
||||
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
||||
import { TOKEN_HEADER } from 'lib/constants';
|
||||
import MetricCard from './MetricCard';
|
||||
import styles from './MetricsBar.module.css';
|
||||
|
||||
export default function MetricsBar({ websiteId, token, className }) {
|
||||
export default function MetricsBar({ websiteId, className }) {
|
||||
const shareToken = useShareToken();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, modified } = dateRange;
|
||||
const [format, setFormat] = useState(true);
|
||||
@ -19,16 +22,16 @@ export default function MetricsBar({ websiteId, token, className }) {
|
||||
} = usePageQuery();
|
||||
|
||||
const { data, error, loading } = useFetch(
|
||||
`/api/website/${websiteId}/metrics`,
|
||||
`/api/website/${websiteId}/stats`,
|
||||
{
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
url,
|
||||
token,
|
||||
},
|
||||
{
|
||||
update: [modified],
|
||||
params: {
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
url,
|
||||
},
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
},
|
||||
[modified],
|
||||
);
|
||||
|
||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||
|
@ -1,34 +1,31 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { useSpring, animated, config } from 'react-spring';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'components/common/Link';
|
||||
import Loading from 'components/common/Loading';
|
||||
import NoData from 'components/common/NoData';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import Arrow from 'assets/arrow-right.svg';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import { formatNumber, formatLongNumber } from 'lib/format';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import useShareToken from 'hooks/useShareToken';
|
||||
import ErrorMessage from 'components/common/ErrorMessage';
|
||||
import DataTable from './DataTable';
|
||||
import { DEFAULT_ANIMATION_DURATION, TOKEN_HEADER } from 'lib/constants';
|
||||
import styles from './MetricsTable.module.css';
|
||||
import ErrorMessage from '../common/ErrorMessage';
|
||||
|
||||
export default function MetricsTable({
|
||||
websiteId,
|
||||
websiteDomain,
|
||||
token,
|
||||
title,
|
||||
metric,
|
||||
type,
|
||||
className,
|
||||
dataFilter,
|
||||
filterOptions,
|
||||
limit,
|
||||
renderLabel,
|
||||
onDataLoad = () => {},
|
||||
onDataLoad,
|
||||
...props
|
||||
}) {
|
||||
const shareToken = useShareToken();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, modified } = dateRange;
|
||||
const {
|
||||
@ -38,22 +35,23 @@ export default function MetricsTable({
|
||||
} = usePageQuery();
|
||||
|
||||
const { data, loading, error } = useFetch(
|
||||
`/api/website/${websiteId}/rankings`,
|
||||
`/api/website/${websiteId}/metrics`,
|
||||
{
|
||||
type,
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
domain: websiteDomain,
|
||||
url,
|
||||
token,
|
||||
params: {
|
||||
type,
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
domain: websiteDomain,
|
||||
url,
|
||||
},
|
||||
onDataLoad,
|
||||
delay: DEFAULT_ANIMATION_DURATION,
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
},
|
||||
{ onDataLoad, delay: 300, update: [modified] },
|
||||
[modified],
|
||||
);
|
||||
const [format, setFormat] = useState(true);
|
||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||
const shouldAnimate = limit > 0;
|
||||
|
||||
const rankings = useMemo(() => {
|
||||
const filteredData = useMemo(() => {
|
||||
if (data) {
|
||||
const items = percentFilter(dataFilter ? dataFilter(data, filterOptions) : data);
|
||||
if (limit) {
|
||||
@ -64,91 +62,24 @@ export default function MetricsTable({
|
||||
return [];
|
||||
}, [data, error, dataFilter, filterOptions]);
|
||||
|
||||
const handleSetFormat = () => setFormat(state => !state);
|
||||
|
||||
const getRow = row => {
|
||||
const { x: label, y: value, z: percent } = row;
|
||||
return (
|
||||
<AnimatedRow
|
||||
key={label}
|
||||
label={renderLabel ? renderLabel(row) : label}
|
||||
value={value}
|
||||
percent={percent}
|
||||
animate={shouldAnimate}
|
||||
format={formatFunc}
|
||||
onClick={handleSetFormat}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Row = ({ index, style }) => {
|
||||
return <div style={style}>{getRow(rankings[index])}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
{!data && loading && <Loading />}
|
||||
{error && <ErrorMessage />}
|
||||
{data && !error && (
|
||||
<>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.metric} onClick={handleSetFormat}>
|
||||
{metric}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{rankings?.length === 0 && <NoData />}
|
||||
{limit
|
||||
? rankings.map(row => getRow(row))
|
||||
: rankings.length > 0 && (
|
||||
<FixedSizeList height={500} itemCount={rankings.length} itemSize={30}>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
{limit && (
|
||||
<Link
|
||||
icon={<Arrow />}
|
||||
href={router.pathname}
|
||||
as={resolve({ view: type })}
|
||||
size="small"
|
||||
iconRight
|
||||
>
|
||||
<FormattedMessage id="button.more" defaultMessage="More" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{data && !error && <DataTable {...props} data={filteredData} className={className} />}
|
||||
<div className={styles.footer}>
|
||||
{data && !error && limit && (
|
||||
<Link
|
||||
icon={<Arrow />}
|
||||
href={router.pathname}
|
||||
as={resolve({ view: type })}
|
||||
size="small"
|
||||
iconRight
|
||||
>
|
||||
<FormattedMessage id="label.more" defaultMessage="More" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => {
|
||||
const props = useSpring({
|
||||
width: percent,
|
||||
y: value,
|
||||
from: { width: 0, y: 0 },
|
||||
config: animate ? config.default : { duration: 0 },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
<div className={styles.value} onClick={onClick}>
|
||||
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
|
||||
</div>
|
||||
<div className={styles.percent}>
|
||||
<animated.div
|
||||
className={styles.bar}
|
||||
style={{ width: props.width.interpolate(n => `${n}%`) }}
|
||||
/>
|
||||
<animated.span className={styles.percentValue}>
|
||||
{props.width.interpolate(n => `${n.toFixed(0)}%`)}
|
||||
</animated.span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -6,95 +6,6 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-normal);
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.row {
|
||||
position: relative;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.label {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.label a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.label a:hover {
|
||||
color: var(--primary400);
|
||||
}
|
||||
|
||||
.label:empty {
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
.label:empty:before {
|
||||
content: 'Unknown';
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.percent {
|
||||
position: relative;
|
||||
width: 50px;
|
||||
color: var(--gray600);
|
||||
border-left: 1px solid var(--gray600);
|
||||
padding-left: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 30px;
|
||||
opacity: 0.1;
|
||||
background: var(--primary400);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -3,15 +3,14 @@ import MetricsTable from './MetricsTable';
|
||||
import { osFilter } from 'lib/filters';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default function OSTable({ websiteId, token, limit }) {
|
||||
export default function OSTable({ websiteId, ...props }) {
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />}
|
||||
type="os"
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
limit={limit}
|
||||
dataFilter={osFilter}
|
||||
/>
|
||||
);
|
||||
|
@ -2,15 +2,16 @@ import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'next/link';
|
||||
import ButtonGroup from 'components/common/ButtonGroup';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
import { urlFilter } from 'lib/filters';
|
||||
import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import styles from './PagesTable.module.css';
|
||||
|
||||
export default function PagesTable({ websiteId, token, websiteDomain, limit, showFilters }) {
|
||||
export const FILTER_COMBINED = 0;
|
||||
export const FILTER_RAW = 1;
|
||||
|
||||
export default function PagesTable({ websiteId, websiteDomain, showFilters, ...props }) {
|
||||
const [filter, setFilter] = useState(FILTER_COMBINED);
|
||||
const {
|
||||
resolve,
|
||||
@ -48,20 +49,11 @@ export default function PagesTable({ websiteId, token, websiteDomain, limit, sho
|
||||
type="url"
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
limit={limit}
|
||||
dataFilter={urlFilter}
|
||||
filterOptions={{ domain: websiteDomain, raw: filter === FILTER_RAW }}
|
||||
renderLabel={renderLink}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const FilterButtons = ({ buttons, selected, onClick }) => {
|
||||
return (
|
||||
<ButtonLayout>
|
||||
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
||||
</ButtonLayout>
|
||||
);
|
||||
};
|
||||
|
@ -4,9 +4,18 @@ import tinycolor from 'tinycolor2';
|
||||
import CheckVisible from 'components/helpers/CheckVisible';
|
||||
import BarChart from './BarChart';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { THEME_COLORS } from 'lib/constants';
|
||||
import { THEME_COLORS, DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
|
||||
export default function PageviewsChart({ websiteId, data, unit, records, className, loading }) {
|
||||
export default function PageviewsChart({
|
||||
websiteId,
|
||||
data,
|
||||
unit,
|
||||
records,
|
||||
className,
|
||||
loading,
|
||||
animationDuration = DEFAULT_ANIMATION_DURATION,
|
||||
...props
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const [theme] = useTheme();
|
||||
const primaryColor = tinycolor(THEME_COLORS[theme].primary);
|
||||
@ -26,7 +35,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
|
||||
data: { datasets },
|
||||
} = chart;
|
||||
|
||||
datasets[0].data = data.uniques;
|
||||
datasets[0].data = data.sessions;
|
||||
datasets[0].label = intl.formatMessage({
|
||||
id: 'metrics.unique-visitors',
|
||||
defaultMessage: 'Unique visitors',
|
||||
@ -48,6 +57,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
|
||||
<CheckVisible>
|
||||
{visible => (
|
||||
<BarChart
|
||||
{...props}
|
||||
className={className}
|
||||
chartId={websiteId}
|
||||
datasets={[
|
||||
@ -56,7 +66,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
|
||||
id: 'metrics.unique-visitors',
|
||||
defaultMessage: 'Unique visitors',
|
||||
}),
|
||||
data: data.uniques,
|
||||
data: data.sessions,
|
||||
lineTension: 0,
|
||||
backgroundColor: colors.visitors.background,
|
||||
borderColor: colors.visitors.border,
|
||||
@ -76,7 +86,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
|
||||
]}
|
||||
unit={unit}
|
||||
records={records}
|
||||
animationDuration={visible ? 300 : 0}
|
||||
animationDuration={visible ? animationDuration : 0}
|
||||
onUpdate={handleUpdate}
|
||||
loading={loading}
|
||||
/>
|
||||
|
60
components/metrics/RealtimeChart.js
Normal file
60
components/metrics/RealtimeChart.js
Normal file
@ -0,0 +1,60 @@
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { format, parseISO, startOfMinute, subMinutes, isBefore } from 'date-fns';
|
||||
import PageviewsChart from './PageviewsChart';
|
||||
import { getDateArray } from 'lib/date';
|
||||
import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants';
|
||||
|
||||
function mapData(data) {
|
||||
let last = 0;
|
||||
const arr = [];
|
||||
|
||||
data.reduce((obj, val) => {
|
||||
const { created_at } = val;
|
||||
const t = startOfMinute(parseISO(created_at));
|
||||
if (t.getTime() > last) {
|
||||
obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 };
|
||||
arr.push(obj);
|
||||
last = t;
|
||||
} else {
|
||||
obj.y += 1;
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
export default function RealtimeChart({ data, unit, ...props }) {
|
||||
const endDate = startOfMinute(new Date());
|
||||
const startDate = subMinutes(endDate, REALTIME_RANGE);
|
||||
const prevEndDate = useRef(endDate);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (data) {
|
||||
return {
|
||||
pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit),
|
||||
sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit),
|
||||
};
|
||||
}
|
||||
return { pageviews: [], sessions: [] };
|
||||
}, [data]);
|
||||
|
||||
// Don't animate the bars shifting over because it looks weird
|
||||
const animationDuration = useMemo(() => {
|
||||
if (isBefore(prevEndDate.current, endDate)) {
|
||||
prevEndDate.current = endDate;
|
||||
return 0;
|
||||
}
|
||||
return DEFAULT_ANIMATION_DURATION;
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<PageviewsChart
|
||||
{...props}
|
||||
height={200}
|
||||
unit={unit}
|
||||
data={chartData}
|
||||
animationDuration={animationDuration}
|
||||
/>
|
||||
);
|
||||
}
|
49
components/metrics/RealtimeHeader.js
Normal file
49
components/metrics/RealtimeHeader.js
Normal file
@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import PageHeader from '../layout/PageHeader';
|
||||
import DropDown from '../common/DropDown';
|
||||
import MetricCard from './MetricCard';
|
||||
import styles from './RealtimeHeader.module.css';
|
||||
|
||||
export default function RealtimeHeader({ websites, data, websiteId, onSelect }) {
|
||||
const options = [
|
||||
{ label: <FormattedMessage id="label.all-websites" defaultMessage="All websites" />, value: 0 },
|
||||
].concat(
|
||||
websites.map(({ name, website_id }, index) => ({
|
||||
label: name,
|
||||
value: website_id,
|
||||
divider: index === 0,
|
||||
})),
|
||||
);
|
||||
|
||||
const { pageviews, sessions, events, countries } = data;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader>
|
||||
<div>
|
||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||
</div>
|
||||
<DropDown value={websiteId} options={options} onChange={onSelect} />
|
||||
</PageHeader>
|
||||
<div className={styles.metrics}>
|
||||
<MetricCard
|
||||
label={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
value={pageviews.length}
|
||||
/>
|
||||
<MetricCard
|
||||
label={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
value={sessions.length}
|
||||
/>
|
||||
<MetricCard
|
||||
label={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
|
||||
value={events.length}
|
||||
/>
|
||||
<MetricCard
|
||||
label={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
|
||||
value={countries.length}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
3
components/metrics/RealtimeHeader.module.css
Normal file
3
components/metrics/RealtimeHeader.module.css
Normal file
@ -0,0 +1,3 @@
|
||||
.metrics {
|
||||
display: flex;
|
||||
}
|
168
components/metrics/RealtimeLog.js
Normal file
168
components/metrics/RealtimeLog.js
Normal file
@ -0,0 +1,168 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { FormattedMessage } 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 useLocale from 'hooks/useLocale';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import { BROWSERS } from 'lib/constants';
|
||||
import Bolt from 'assets/bolt.svg';
|
||||
import Visitor from 'assets/visitor.svg';
|
||||
import Eye from 'assets/eye.svg';
|
||||
import { stringToColor } from 'lib/format';
|
||||
import styles from './RealtimeLog.module.css';
|
||||
|
||||
const TYPE_ALL = 0;
|
||||
const TYPE_PAGEVIEW = 1;
|
||||
const TYPE_SESSION = 2;
|
||||
const TYPE_EVENT = 3;
|
||||
|
||||
const TYPE_ICONS = {
|
||||
[TYPE_PAGEVIEW]: <Eye />,
|
||||
[TYPE_SESSION]: <Visitor />,
|
||||
[TYPE_EVENT]: <Bolt />,
|
||||
};
|
||||
|
||||
export default function RealtimeLog({ data, websites }) {
|
||||
const [locale] = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const [filter, setFilter] = useState(TYPE_ALL);
|
||||
|
||||
const logs = useMemo(() => {
|
||||
const { pageviews, sessions, events } = data;
|
||||
const logs = [...pageviews, ...sessions, ...events].sort(firstBy('created_at', -1));
|
||||
if (filter) {
|
||||
return logs.filter(row => getType(row) === filter);
|
||||
}
|
||||
return logs;
|
||||
}, [data, filter]);
|
||||
|
||||
const uuids = useMemo(() => {
|
||||
return data.sessions.reduce((obj, { session_id, session_uuid }) => {
|
||||
obj[session_id] = session_uuid;
|
||||
return obj;
|
||||
}, {});
|
||||
}, [data]);
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: <FormattedMessage id="label.all" defaultMessage="All" />,
|
||||
value: TYPE_ALL,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.views" defaultMessage="Views" />,
|
||||
value: TYPE_PAGEVIEW,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />,
|
||||
value: TYPE_SESSION,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.events" defaultMessage="Events" />,
|
||||
value: TYPE_EVENT,
|
||||
},
|
||||
];
|
||||
|
||||
function getType({ view_id, session_id, event_id }) {
|
||||
if (event_id) {
|
||||
return TYPE_EVENT;
|
||||
}
|
||||
if (view_id) {
|
||||
return TYPE_PAGEVIEW;
|
||||
}
|
||||
if (session_id) {
|
||||
return TYPE_SESSION;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getIcon(row) {
|
||||
return TYPE_ICONS[getType(row)];
|
||||
}
|
||||
|
||||
function getWebsite({ website_id }) {
|
||||
return websites.find(n => n.website_id === website_id)?.name;
|
||||
}
|
||||
|
||||
function getDetail({
|
||||
event_type,
|
||||
event_value,
|
||||
view_id,
|
||||
session_id,
|
||||
url,
|
||||
browser,
|
||||
os,
|
||||
country,
|
||||
device,
|
||||
}) {
|
||||
if (event_type) {
|
||||
return (
|
||||
<div>
|
||||
<Tag>{event_type}</Tag> {event_value}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (view_id) {
|
||||
return url;
|
||||
}
|
||||
if (session_id) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="message.log.visitor"
|
||||
defaultMessage="Visitor from {country} using {browser} on {os} {device}"
|
||||
values={{
|
||||
country: <b>{countryNames[country]}</b>,
|
||||
browser: BROWSERS[browser],
|
||||
os,
|
||||
device,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getTime({ created_at }) {
|
||||
return format(new Date(created_at), 'h:mm:ss');
|
||||
}
|
||||
|
||||
function getColor(row) {
|
||||
const { session_id } = row;
|
||||
|
||||
return stringToColor(uuids[session_id] || `${session_id}${getWebsite(row)}`);
|
||||
}
|
||||
|
||||
const Row = ({ index, style }) => {
|
||||
const row = logs[index];
|
||||
return (
|
||||
<div className={styles.row} style={style}>
|
||||
<div>
|
||||
<Dot color={getColor(row)} />
|
||||
</div>
|
||||
<div className={styles.time}>{getTime(row)}</div>
|
||||
<div className={styles.detail}>
|
||||
<Icon className={styles.icon} icon={getIcon(row)} />
|
||||
{getDetail(row)}
|
||||
</div>
|
||||
<div className={styles.website}>{getWebsite(row)}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.table}>
|
||||
<FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />
|
||||
<div className={styles.header}>
|
||||
<FormattedMessage id="label.realtime-logs" defaultMessage="Realtime logs" />
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
<FixedSizeList height={400} itemCount={logs.length} itemSize={40}>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
46
components/metrics/RealtimeLog.module.css
Normal file
46
components/metrics/RealtimeLog.module.css
Normal file
@ -0,0 +1,46 @@
|
||||
.table {
|
||||
font-size: var(--font-size-xsmall);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 16px;
|
||||
line-height: 40px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
border-bottom: 1px solid var(--gray300);
|
||||
}
|
||||
|
||||
.body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.time {
|
||||
min-width: 60px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.website {
|
||||
text-align: right;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
100
components/metrics/RealtimeViews.js
Normal file
100
components/metrics/RealtimeViews.js
Normal file
@ -0,0 +1,100 @@
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import firstBy from 'thenby';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import DataTable from './DataTable';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
|
||||
const FILTER_REFERRERS = 0;
|
||||
const FILTER_PAGES = 1;
|
||||
|
||||
export default function RealtimeViews({ websiteId, data, websites }) {
|
||||
const { pageviews } = data;
|
||||
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,
|
||||
[websites],
|
||||
);
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: <FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />,
|
||||
value: FILTER_REFERRERS,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.pages" defaultMessage="Pages" />,
|
||||
value: FILTER_PAGES,
|
||||
},
|
||||
];
|
||||
|
||||
const [referrers, pages] = useMemo(() => {
|
||||
if (pageviews) {
|
||||
const referrers = percentFilter(
|
||||
pageviews
|
||||
.reduce((arr, { referrer }) => {
|
||||
if (referrer?.startsWith('http')) {
|
||||
const hostname = new URL(referrer).hostname.replace(/^www\./, '');
|
||||
|
||||
if (hostname && !domains.includes(hostname)) {
|
||||
const row = arr.find(({ x }) => x === hostname);
|
||||
|
||||
if (!row) {
|
||||
arr.push({ x: hostname, y: 1 });
|
||||
} else {
|
||||
row.y += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, [])
|
||||
.sort(firstBy('y', -1)),
|
||||
);
|
||||
|
||||
const pages = percentFilter(
|
||||
pageviews
|
||||
.reduce((arr, { url, website_id }) => {
|
||||
if (url?.startsWith('/')) {
|
||||
if (!websiteId) {
|
||||
url = `${getDomain(website_id)}${url}`;
|
||||
}
|
||||
const row = arr.find(({ x }) => x === url);
|
||||
|
||||
if (!row) {
|
||||
arr.push({ x: url, y: 1 });
|
||||
} else {
|
||||
row.y += 1;
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, [])
|
||||
.sort(firstBy('y', -1)),
|
||||
);
|
||||
|
||||
return [referrers, pages];
|
||||
}
|
||||
return [];
|
||||
}, [pageviews]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />
|
||||
{filter === FILTER_REFERRERS && (
|
||||
<DataTable
|
||||
title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />}
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
data={referrers}
|
||||
height={400}
|
||||
/>
|
||||
)}
|
||||
{filter === FILTER_PAGES && (
|
||||
<DataTable
|
||||
title={<FormattedMessage id="metrics.pages" defaultMessage="Pages" />}
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
data={pages}
|
||||
height={400}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,12 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import FilterButtons from 'components/common/FilterButtons';
|
||||
import { refFilter } from 'lib/filters';
|
||||
import ButtonGroup from 'components/common/ButtonGroup';
|
||||
import { FILTER_DOMAIN_ONLY, FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
|
||||
import ButtonLayout from '../layout/ButtonLayout';
|
||||
|
||||
export default function ReferrersTable({ websiteId, websiteDomain, token, limit, showFilters }) {
|
||||
export const FILTER_DOMAIN_ONLY = 0;
|
||||
export const FILTER_COMBINED = 1;
|
||||
export const FILTER_RAW = 2;
|
||||
|
||||
export default function ReferrersTable({ websiteId, websiteDomain, showFilters, ...props }) {
|
||||
const [filter, setFilter] = useState(FILTER_COMBINED);
|
||||
|
||||
const buttons = [
|
||||
@ -35,13 +37,12 @@ export default function ReferrersTable({ websiteId, websiteDomain, token, limit,
|
||||
<>
|
||||
{showFilters && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />}
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />}
|
||||
type="referrer"
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
websiteId={websiteId}
|
||||
websiteDomain={websiteDomain}
|
||||
token={token}
|
||||
limit={limit}
|
||||
dataFilter={refFilter}
|
||||
filterOptions={{
|
||||
domain: websiteDomain,
|
||||
@ -53,11 +54,3 @@ export default function ReferrersTable({ websiteId, websiteDomain, token, limit,
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const FilterButtons = ({ buttons, selected, onClick }) => {
|
||||
return (
|
||||
<ButtonLayout>
|
||||
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
||||
</ButtonLayout>
|
||||
);
|
||||
};
|
||||
|
@ -14,15 +14,17 @@ import { getDateArray, getDateLength } from 'lib/date';
|
||||
import Times from 'assets/times.svg';
|
||||
import styles from './WebsiteChart.module.css';
|
||||
import ErrorMessage from '../common/ErrorMessage';
|
||||
import useShareToken from '../../hooks/useShareToken';
|
||||
import { TOKEN_HEADER } from '../../lib/constants';
|
||||
|
||||
export default function WebsiteChart({
|
||||
websiteId,
|
||||
token,
|
||||
title,
|
||||
stickyHeader = false,
|
||||
showLink = false,
|
||||
onDataLoad = () => {},
|
||||
}) {
|
||||
const shareToken = useShareToken();
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit, value, modified } = dateRange;
|
||||
const [timezone] = useTimezone();
|
||||
@ -35,24 +37,27 @@ export default function WebsiteChart({
|
||||
const { data, loading, error } = useFetch(
|
||||
`/api/website/${websiteId}/pageviews`,
|
||||
{
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
unit,
|
||||
tz: timezone,
|
||||
url,
|
||||
token,
|
||||
params: {
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
unit,
|
||||
tz: timezone,
|
||||
url,
|
||||
},
|
||||
onDataLoad,
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
},
|
||||
{ onDataLoad, update: [modified] },
|
||||
[modified],
|
||||
);
|
||||
|
||||
const [pageviews, uniques] = useMemo(() => {
|
||||
const chartData = useMemo(() => {
|
||||
if (data) {
|
||||
return [
|
||||
getDateArray(data.pageviews, startDate, endDate, unit),
|
||||
getDateArray(data.uniques, startDate, endDate, unit),
|
||||
];
|
||||
return {
|
||||
pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
|
||||
sessions: getDateArray(data.sessions, startDate, endDate, unit),
|
||||
};
|
||||
}
|
||||
return [[], []];
|
||||
return { pageviews: [], sessions: [] };
|
||||
}, [data]);
|
||||
|
||||
function handleCloseFilter() {
|
||||
@ -61,7 +66,7 @@ export default function WebsiteChart({
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<WebsiteHeader websiteId={websiteId} token={token} title={title} showLink={showLink} />
|
||||
<WebsiteHeader websiteId={websiteId} title={title} showLink={showLink} />
|
||||
<div className={classNames(styles.header, 'row')}>
|
||||
<StickyHeader
|
||||
className={classNames(styles.metrics, 'col row')}
|
||||
@ -70,7 +75,7 @@ export default function WebsiteChart({
|
||||
>
|
||||
{url && <PageFilter url={url} onClick={handleCloseFilter} />}
|
||||
<div className="col-12 col-lg-9">
|
||||
<MetricsBar websiteId={websiteId} token={token} />
|
||||
<MetricsBar websiteId={websiteId} />
|
||||
</div>
|
||||
<div className={classNames(styles.filter, 'col-12 col-lg-3')}>
|
||||
<DateFilter
|
||||
@ -87,7 +92,7 @@ export default function WebsiteChart({
|
||||
{error && <ErrorMessage />}
|
||||
<PageviewsChart
|
||||
websiteId={websiteId}
|
||||
data={{ pageviews, uniques }}
|
||||
data={chartData}
|
||||
unit={unit}
|
||||
records={getDateLength(startDate, endDate, unit)}
|
||||
loading={loading}
|
||||
|
@ -8,11 +8,11 @@ import ActiveUsers from './ActiveUsers';
|
||||
import Arrow from 'assets/arrow-right.svg';
|
||||
import styles from './WebsiteHeader.module.css';
|
||||
|
||||
export default function WebsiteHeader({ websiteId, token, title, showLink = false }) {
|
||||
export default function WebsiteHeader({ websiteId, title, showLink = false }) {
|
||||
return (
|
||||
<PageHeader>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<ActiveUsers className={styles.active} websiteId={websiteId} token={token} />
|
||||
<ActiveUsers className={styles.active} websiteId={websiteId} />
|
||||
<ButtonLayout align="right">
|
||||
<RefreshButton websiteId={websiteId} />
|
||||
{showLink && (
|
||||
@ -24,7 +24,7 @@ export default function WebsiteHeader({ websiteId, token, title, showLink = fals
|
||||
size="small"
|
||||
iconRight
|
||||
>
|
||||
<FormattedMessage id="button.view-details" defaultMessage="View details" />
|
||||
<FormattedMessage id="label.view-details" defaultMessage="View details" />
|
||||
</Link>
|
||||
)}
|
||||
</ButtonLayout>
|
||||
|
158
components/pages/RealtimeDashboard.js
Normal file
158
components/pages/RealtimeDashboard.js
Normal file
@ -0,0 +1,158 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { subMinutes, startOfMinute } from 'date-fns';
|
||||
import firstBy from 'thenby';
|
||||
import Page from 'components/layout/Page';
|
||||
import GridLayout, { GridRow, GridColumn } from 'components/layout/GridLayout';
|
||||
import RealtimeChart from 'components/metrics/RealtimeChart';
|
||||
import RealtimeLog from 'components/metrics/RealtimeLog';
|
||||
import RealtimeHeader from 'components/metrics/RealtimeHeader';
|
||||
import WorldMap from 'components/common/WorldMap';
|
||||
import DataTable from 'components/metrics/DataTable';
|
||||
import RealtimeViews from 'components/metrics/RealtimeViews';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import { TOKEN_HEADER, REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
|
||||
import styles from './RealtimeDashboard.module.css';
|
||||
|
||||
function mergeData(state, data, time) {
|
||||
const ids = state.map(({ __id }) => __id);
|
||||
return state
|
||||
.concat(data.filter(({ __id }) => !ids.includes(__id)))
|
||||
.filter(({ created_at }) => new Date(created_at).getTime() >= time);
|
||||
}
|
||||
|
||||
function filterWebsite(data, id) {
|
||||
return data.filter(({ website_id }) => website_id === id);
|
||||
}
|
||||
|
||||
export default function RealtimeDashboard() {
|
||||
const [locale] = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const [data, setData] = useState();
|
||||
const [websiteId, setWebsiteId] = useState(0);
|
||||
const { data: init, loading } = useFetch('/api/realtime/init');
|
||||
const { data: updates } = useFetch('/api/realtime/update', {
|
||||
params: { start_at: data?.timestamp },
|
||||
disabled: !init?.websites?.length || !data,
|
||||
interval: REALTIME_INTERVAL,
|
||||
headers: { [TOKEN_HEADER]: init?.token },
|
||||
});
|
||||
|
||||
const renderCountryName = useCallback(
|
||||
({ x }) => <span className={locale}>{countryNames[x]}</span>,
|
||||
[countryNames],
|
||||
);
|
||||
|
||||
const realtimeData = useMemo(() => {
|
||||
if (data) {
|
||||
const { pageviews, sessions, events } = data;
|
||||
|
||||
if (websiteId) {
|
||||
return {
|
||||
pageviews: filterWebsite(pageviews, websiteId),
|
||||
sessions: filterWebsite(sessions, websiteId),
|
||||
events: filterWebsite(events, websiteId),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}, [data, websiteId]);
|
||||
|
||||
const countries = useMemo(() => {
|
||||
if (realtimeData?.sessions) {
|
||||
return percentFilter(
|
||||
realtimeData.sessions
|
||||
.reduce((arr, { country }) => {
|
||||
if (country) {
|
||||
const row = arr.find(({ x }) => x === country);
|
||||
|
||||
if (!row) {
|
||||
arr.push({ x: country, y: 1 });
|
||||
} else {
|
||||
row.y += 1;
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}, [])
|
||||
.sort(firstBy('y', -1)),
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}, [realtimeData?.sessions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (init && !data) {
|
||||
const { websites, data } = init;
|
||||
|
||||
setData({ websites, ...data });
|
||||
}
|
||||
}, [init]);
|
||||
|
||||
useEffect(() => {
|
||||
if (updates) {
|
||||
const { pageviews, sessions, events, timestamp } = updates;
|
||||
const time = subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime();
|
||||
|
||||
setData(state => ({
|
||||
...state,
|
||||
pageviews: mergeData(state.pageviews, pageviews, time),
|
||||
sessions: mergeData(state.sessions, sessions, time),
|
||||
events: mergeData(state.events, events, time),
|
||||
timestamp,
|
||||
}));
|
||||
}
|
||||
}, [updates]);
|
||||
|
||||
if (!init || !data || loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { websites } = data;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<RealtimeHeader
|
||||
websites={websites}
|
||||
websiteId={websiteId}
|
||||
data={{ ...realtimeData, countries }}
|
||||
onSelect={setWebsiteId}
|
||||
/>
|
||||
<div className={styles.chart}>
|
||||
<RealtimeChart
|
||||
websiteId={websiteId}
|
||||
data={realtimeData}
|
||||
unit="minute"
|
||||
records={REALTIME_RANGE}
|
||||
/>
|
||||
</div>
|
||||
<GridLayout>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} lg={4}>
|
||||
<RealtimeViews websiteId={websiteId} data={realtimeData} websites={websites} />
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} lg={8}>
|
||||
<RealtimeLog data={realtimeData} websites={websites} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} lg={4}>
|
||||
<DataTable
|
||||
title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
|
||||
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
|
||||
data={countries}
|
||||
renderLabel={renderCountryName}
|
||||
height={500}
|
||||
/>
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} lg={8}>
|
||||
<WorldMap data={countries} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
</GridLayout>
|
||||
</Page>
|
||||
);
|
||||
}
|
7
components/pages/RealtimeDashboard.module.css
Normal file
7
components/pages/RealtimeDashboard.module.css
Normal file
@ -0,0 +1,7 @@
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chart {
|
||||
margin-bottom: 30px;
|
||||
}
|
@ -7,7 +7,7 @@ import Page from '../layout/Page';
|
||||
import PageHeader from '../layout/PageHeader';
|
||||
import useFetch from '../../hooks/useFetch';
|
||||
import DropDown from '../common/DropDown';
|
||||
import styles from './Test.module.css';
|
||||
import styles from './TestConsole.module.css';
|
||||
import WebsiteChart from '../metrics/WebsiteChart';
|
||||
import EventsChart from '../metrics/EventsChart';
|
||||
import Button from '../common/Button';
|
||||
|
@ -4,6 +4,7 @@ import classNames from 'classnames';
|
||||
import WebsiteChart from 'components/metrics/WebsiteChart';
|
||||
import WorldMap from 'components/common/WorldMap';
|
||||
import Page from 'components/layout/Page';
|
||||
import GridLayout, { GridRow, GridColumn } from 'components/layout/GridLayout';
|
||||
import MenuLayout from 'components/layout/MenuLayout';
|
||||
import Link from 'components/common/Link';
|
||||
import Loading from 'components/common/Loading';
|
||||
@ -19,6 +20,8 @@ import EventsTable from '../metrics/EventsTable';
|
||||
import EventsChart from '../metrics/EventsChart';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import useShareToken from 'hooks/useShareToken';
|
||||
import { DEFAULT_ANIMATION_DURATION, TOKEN_HEADER } from 'lib/constants';
|
||||
|
||||
const views = {
|
||||
url: PagesTable,
|
||||
@ -30,8 +33,11 @@ const views = {
|
||||
event: EventsTable,
|
||||
};
|
||||
|
||||
export default function WebsiteDetails({ websiteId, token }) {
|
||||
const { data } = useFetch(`/api/website/${websiteId}`, { token });
|
||||
export default function WebsiteDetails({ websiteId }) {
|
||||
const shareToken = useShareToken();
|
||||
const { data } = useFetch(`/api/website/${websiteId}`, {
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
});
|
||||
const [chartLoaded, setChartLoaded] = useState(false);
|
||||
const [countryData, setCountryData] = useState();
|
||||
const [eventsData, setEventsData] = useState();
|
||||
@ -50,7 +56,7 @@ export default function WebsiteDetails({ websiteId, token }) {
|
||||
icon={<Arrow />}
|
||||
size="small"
|
||||
>
|
||||
<FormattedMessage id="button.back" defaultMessage="Back" />
|
||||
<FormattedMessage id="label.back" defaultMessage="Back" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
@ -91,7 +97,6 @@ export default function WebsiteDetails({ websiteId, token }) {
|
||||
|
||||
const tableProps = {
|
||||
websiteId,
|
||||
token,
|
||||
websiteDomain: data?.domain,
|
||||
limit: 10,
|
||||
};
|
||||
@ -100,7 +105,7 @@ export default function WebsiteDetails({ websiteId, token }) {
|
||||
|
||||
function handleDataLoad() {
|
||||
if (!chartLoaded) {
|
||||
setTimeout(() => setChartLoaded(true), 300);
|
||||
setTimeout(() => setChartLoaded(true), DEFAULT_ANIMATION_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,7 +119,6 @@ export default function WebsiteDetails({ websiteId, token }) {
|
||||
<div className={classNames(styles.chart, 'col')}>
|
||||
<WebsiteChart
|
||||
websiteId={websiteId}
|
||||
token={token}
|
||||
title={data.name}
|
||||
onDataLoad={handleDataLoad}
|
||||
showLink={false}
|
||||
@ -124,54 +128,59 @@ export default function WebsiteDetails({ websiteId, token }) {
|
||||
</div>
|
||||
{!chartLoaded && <Loading />}
|
||||
{chartLoaded && !view && (
|
||||
<>
|
||||
<div className={classNames(styles.row, 'row')}>
|
||||
<div className="col-md-12 col-lg-6">
|
||||
<GridLayout>
|
||||
<GridRow>
|
||||
<GridColumn md={12} lg={6}>
|
||||
<PagesTable {...tableProps} />
|
||||
</div>
|
||||
<div className="col-md-12 col-lg-6">
|
||||
</GridColumn>
|
||||
<GridColumn md={12} lg={6}>
|
||||
<ReferrersTable {...tableProps} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames(styles.row, 'row')}>
|
||||
<div className="col-md-12 col-lg-4">
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn md={12} lg={4}>
|
||||
<BrowsersTable {...tableProps} />
|
||||
</div>
|
||||
<div className="col-md-12 col-lg-4">
|
||||
</GridColumn>
|
||||
<GridColumn md={12} lg={4}>
|
||||
<OSTable {...tableProps} />
|
||||
</div>
|
||||
<div className="col-md-12 col-lg-4">
|
||||
</GridColumn>
|
||||
<GridColumn md={12} lg={4}>
|
||||
<DevicesTable {...tableProps} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames(styles.row, 'row')}>
|
||||
<div className="col-12 col-md-12 col-lg-8">
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} md={12} lg={8}>
|
||||
<WorldMap data={countryData} />
|
||||
</div>
|
||||
<div className="col-12 col-md-12 col-lg-4">
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} md={12} lg={4}>
|
||||
<CountriesTable {...tableProps} onDataLoad={setCountryData} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(styles.row, 'row', { [styles.hidden]: !eventsData?.length > 0 })}
|
||||
>
|
||||
<div className="col-12 col-md-12 col-lg-4">
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow className={classNames({ [styles.hidden]: !eventsData?.length > 0 })}>
|
||||
<GridColumn xs={12} md={12} lg={4}>
|
||||
<EventsTable {...tableProps} onDataLoad={setEventsData} />
|
||||
</div>
|
||||
<div className="col-12 col-md-12 col-lg-8 pt-5 pb-5">
|
||||
<EventsChart websiteId={websiteId} token={token} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} md={12} lg={8}>
|
||||
<EventsChart className={styles.eventschart} websiteId={websiteId} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
</GridLayout>
|
||||
)}
|
||||
{view && (
|
||||
{view && chartLoaded && (
|
||||
<MenuLayout
|
||||
className={styles.view}
|
||||
menuClassName={styles.menu}
|
||||
contentClassName={styles.content}
|
||||
menu={menuOptions}
|
||||
>
|
||||
<DetailsComponent {...tableProps} limit={false} showFilters={true} />
|
||||
<DetailsComponent
|
||||
{...tableProps}
|
||||
height={500}
|
||||
limit={false}
|
||||
animte={false}
|
||||
showFilters
|
||||
virtualize
|
||||
/>
|
||||
</MenuLayout>
|
||||
)}
|
||||
</Page>
|
||||
|
@ -26,37 +26,10 @@
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.row {
|
||||
border-top: 1px solid var(--gray300);
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.row > [class*='col-'] {
|
||||
border-left: 1px solid var(--gray300);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.row > [class*='col-']:first-child {
|
||||
border-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.row > [class*='col-']:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.row {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.row > [class*='col-'] {
|
||||
border-top: 1px solid var(--gray300);
|
||||
border-left: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.eventschart {
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import Arrow from 'assets/arrow-right.svg';
|
||||
import styles from './WebsiteList.module.css';
|
||||
|
||||
export default function WebsiteList({ userId }) {
|
||||
const { data } = useFetch('/api/websites', { user_id: userId });
|
||||
const { data } = useFetch('/api/websites', { params: { user_id: userId } });
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
|
@ -25,7 +25,7 @@ export default function AccountSettings() {
|
||||
const [deleteAccount, setDeleteAccount] = useState();
|
||||
const [saved, setSaved] = useState(0);
|
||||
const [message, setMessage] = useState();
|
||||
const { data } = useFetch(`/api/accounts`, {}, { update: [saved] });
|
||||
const { data } = useFetch(`/api/accounts`, {}, [saved]);
|
||||
|
||||
const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null);
|
||||
|
||||
@ -42,10 +42,10 @@ export default function AccountSettings() {
|
||||
row.username !== 'admin' ? (
|
||||
<ButtonLayout align="right">
|
||||
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
|
||||
<FormattedMessage id="button.edit" defaultMessage="Edit" />
|
||||
<FormattedMessage id="label.edit" defaultMessage="Edit" />
|
||||
</Button>
|
||||
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
|
||||
<FormattedMessage id="button.delete" defaultMessage="Delete" />
|
||||
<FormattedMessage id="label.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
</ButtonLayout>
|
||||
) : null;
|
||||
@ -98,12 +98,12 @@ export default function AccountSettings() {
|
||||
<FormattedMessage id="label.accounts" defaultMessage="Accounts" />
|
||||
</div>
|
||||
<Button icon={<Plus />} size="small" onClick={() => setAddAccount(true)}>
|
||||
<FormattedMessage id="button.add-account" defaultMessage="Add account" />
|
||||
<FormattedMessage id="label.add-account" defaultMessage="Add account" />
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Table columns={columns} rows={data} />
|
||||
{editAccount && (
|
||||
<Modal title={<FormattedMessage id="title.edit-account" defaultMessage="Edit account" />}>
|
||||
<Modal title={<FormattedMessage id="label.edit-account" defaultMessage="Edit account" />}>
|
||||
<AccountEditForm
|
||||
values={{ ...editAccount, password: '' }}
|
||||
onSave={handleSave}
|
||||
@ -112,13 +112,13 @@ export default function AccountSettings() {
|
||||
</Modal>
|
||||
)}
|
||||
{addAccount && (
|
||||
<Modal title={<FormattedMessage id="title.add-account" defaultMessage="Add account" />}>
|
||||
<Modal title={<FormattedMessage id="label.add-account" defaultMessage="Add account" />}>
|
||||
<AccountEditForm onSave={handleSave} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
{deleteAccount && (
|
||||
<Modal
|
||||
title={<FormattedMessage id="title.delete-account" defaultMessage="Delete account" />}
|
||||
title={<FormattedMessage id="label.delete-account" defaultMessage="Delete account" />}
|
||||
>
|
||||
<DeleteForm
|
||||
values={{ type: 'account', id: deleteAccount.user_id, name: deleteAccount.username }}
|
||||
|
@ -19,7 +19,7 @@ export default function DateRangeSetting() {
|
||||
<>
|
||||
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={setDateRange} />
|
||||
<Button className={styles.button} size="small" onClick={handleReset}>
|
||||
<FormattedMessage id="button.reset" defaultMessage="Reset" />
|
||||
<FormattedMessage id="label.reset" defaultMessage="Reset" />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
@ -34,7 +34,7 @@ export default function ProfileSettings() {
|
||||
<FormattedMessage id="label.profile" defaultMessage="Profile" />
|
||||
</div>
|
||||
<Button icon={<Dots />} size="small" onClick={() => setChangePassword(true)}>
|
||||
<FormattedMessage id="button.change-password" defaultMessage="Change password" />
|
||||
<FormattedMessage id="label.change-password" defaultMessage="Change password" />
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<dl className={styles.list}>
|
||||
@ -57,7 +57,7 @@ export default function ProfileSettings() {
|
||||
</dl>
|
||||
{changePassword && (
|
||||
<Modal
|
||||
title={<FormattedMessage id="title.change-password" defaultMessage="Change password" />}
|
||||
title={<FormattedMessage id="label.change-password" defaultMessage="Change password" />}
|
||||
>
|
||||
<ChangePasswordForm
|
||||
values={{ user_id }}
|
||||
|
@ -24,7 +24,7 @@ export default function TimezoneSetting() {
|
||||
onChange={saveTimezone}
|
||||
/>
|
||||
<Button className={styles.button} size="small" onClick={handleReset}>
|
||||
<FormattedMessage id="button.reset" defaultMessage="Reset" />
|
||||
<FormattedMessage id="label.reset" defaultMessage="Reset" />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
@ -29,7 +29,7 @@ export default function WebsiteSettings() {
|
||||
const [showUrl, setShowUrl] = useState();
|
||||
const [saved, setSaved] = useState(0);
|
||||
const [message, setMessage] = useState();
|
||||
const { data } = useFetch(`/api/websites`, {}, { update: [saved] });
|
||||
const { data } = useFetch(`/api/websites`, {}, [saved]);
|
||||
|
||||
const Buttons = row => (
|
||||
<ButtonLayout align="right">
|
||||
@ -52,10 +52,10 @@ export default function WebsiteSettings() {
|
||||
onClick={() => setShowCode(row)}
|
||||
/>
|
||||
<Button icon={<Pen />} size="small" onClick={() => setEditWebsite(row)}>
|
||||
<FormattedMessage id="button.edit" defaultMessage="Edit" />
|
||||
<FormattedMessage id="label.edit" defaultMessage="Edit" />
|
||||
</Button>
|
||||
<Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}>
|
||||
<FormattedMessage id="button.delete" defaultMessage="Delete" />
|
||||
<FormattedMessage id="label.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
</ButtonLayout>
|
||||
);
|
||||
@ -113,7 +113,7 @@ export default function WebsiteSettings() {
|
||||
}
|
||||
>
|
||||
<Button icon={<Plus />} size="medium" onClick={() => setAddWebsite(true)}>
|
||||
<FormattedMessage id="button.add-website" defaultMessage="Add website" />
|
||||
<FormattedMessage id="label.add-website" defaultMessage="Add website" />
|
||||
</Button>
|
||||
</EmptyPlaceholder>
|
||||
);
|
||||
@ -125,23 +125,23 @@ export default function WebsiteSettings() {
|
||||
<FormattedMessage id="label.websites" defaultMessage="Websites" />
|
||||
</div>
|
||||
<Button icon={<Plus />} size="small" onClick={() => setAddWebsite(true)}>
|
||||
<FormattedMessage id="button.add-website" defaultMessage="Add website" />
|
||||
<FormattedMessage id="label.add-website" defaultMessage="Add website" />
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Table columns={columns} rows={data} empty={empty} />
|
||||
{editWebsite && (
|
||||
<Modal title={<FormattedMessage id="title.edit-website" defaultMessage="Edit website" />}>
|
||||
<Modal title={<FormattedMessage id="label.edit-website" defaultMessage="Edit website" />}>
|
||||
<WebsiteEditForm values={editWebsite} onSave={handleSave} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
{addWebsite && (
|
||||
<Modal title={<FormattedMessage id="title.add-website" defaultMessage="Add website" />}>
|
||||
<Modal title={<FormattedMessage id="label.add-website" defaultMessage="Add website" />}>
|
||||
<WebsiteEditForm onSave={handleSave} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
{deleteWebsite && (
|
||||
<Modal
|
||||
title={<FormattedMessage id="title.delete-website" defaultMessage="Delete website" />}
|
||||
title={<FormattedMessage id="label.delete-website" defaultMessage="Delete website" />}
|
||||
>
|
||||
<DeleteForm
|
||||
values={{ type: 'website', id: deleteWebsite.website_id, name: deleteWebsite.name }}
|
||||
@ -151,12 +151,12 @@ export default function WebsiteSettings() {
|
||||
</Modal>
|
||||
)}
|
||||
{showCode && (
|
||||
<Modal title={<FormattedMessage id="title.tracking-code" defaultMessage="Tracking code" />}>
|
||||
<Modal title={<FormattedMessage id="label.tracking-code" defaultMessage="Tracking code" />}>
|
||||
<TrackingCodeForm values={showCode} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
{showUrl && (
|
||||
<Modal title={<FormattedMessage id="title.share-url" defaultMessage="Share URL" />}>
|
||||
<Modal title={<FormattedMessage id="label.share-url" defaultMessage="Share URL" />}>
|
||||
<ShareUrlForm values={showUrl} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
|
@ -4,24 +4,22 @@ import { get } from 'lib/web';
|
||||
import { updateQuery } from 'redux/actions/queries';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export default function useFetch(url, params = {}, options = {}) {
|
||||
export default function useFetch(url, options = {}, update = []) {
|
||||
const dispatch = useDispatch();
|
||||
const [data, setData] = useState();
|
||||
const [status, setStatus] = useState();
|
||||
const [error, setError] = useState();
|
||||
const [loading, setLoadiing] = useState(false);
|
||||
const [count, setCount] = useState(0);
|
||||
const { basePath } = useRouter();
|
||||
const keys = Object.keys(params)
|
||||
.sort()
|
||||
.map(key => params[key]);
|
||||
const { update = [], onDataLoad = () => {} } = options;
|
||||
const { params = {}, disabled, headers, delay = 0, interval, onDataLoad } = options;
|
||||
|
||||
async function loadData() {
|
||||
async function loadData(params) {
|
||||
try {
|
||||
setLoadiing(true);
|
||||
setError(null);
|
||||
const time = performance.now();
|
||||
const { data, status } = await get(`${basePath}${url}`, params);
|
||||
const { data, status } = await get(`${basePath}${url}`, params, headers);
|
||||
|
||||
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
|
||||
|
||||
@ -33,7 +31,7 @@ export default function useFetch(url, params = {}, options = {}) {
|
||||
}
|
||||
|
||||
setStatus(status);
|
||||
onDataLoad(data);
|
||||
onDataLoad?.(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(e);
|
||||
@ -43,18 +41,24 @@ export default function useFetch(url, params = {}, options = {}) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
const { interval, delay = 0 } = options;
|
||||
if (url && !disabled) {
|
||||
const id = setTimeout(() => loadData(params), delay);
|
||||
|
||||
setTimeout(() => loadData(), delay);
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}
|
||||
}, [url, !!disabled, count, ...update]);
|
||||
|
||||
const id = interval ? setInterval(() => loadData(), interval) : null;
|
||||
useEffect(() => {
|
||||
if (interval && !disabled) {
|
||||
const id = setInterval(() => setCount(state => state + 1), interval);
|
||||
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
};
|
||||
}
|
||||
}, [url, ...keys, ...update]);
|
||||
}, [interval, !!disabled]);
|
||||
|
||||
return { data, status, error, loading };
|
||||
}
|
||||
|
25
hooks/useShareToken.js
Normal file
25
hooks/useShareToken.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { get } from 'lib/web';
|
||||
import { setShareToken } from 'redux/actions/app';
|
||||
|
||||
export default function useShareToken(shareId) {
|
||||
const dispatch = useDispatch();
|
||||
const shareToken = useSelector(state => state.app.shareToken);
|
||||
|
||||
async function loadToken(id) {
|
||||
const { data } = await get(`/api/share/${id}`);
|
||||
|
||||
if (data) {
|
||||
dispatch(setShareToken(data));
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (shareId) {
|
||||
loadToken(shareId);
|
||||
}
|
||||
}, [shareId]);
|
||||
|
||||
return shareToken;
|
||||
}
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Tilføj konto",
|
||||
"button.add-website": "Tilføj hjemmeside",
|
||||
"button.back": "Tilbage",
|
||||
"button.cancel": "Afvis",
|
||||
"button.change-password": "Skift adgangskode",
|
||||
"button.copy-to-clipboard": "Kopier til udklipsholder",
|
||||
"button.date-range": "Datointerval",
|
||||
"button.delete": "Slet",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Rediger",
|
||||
"button.login": "Log ind",
|
||||
"button.more": "Mere",
|
||||
"button.refresh": "Opdater",
|
||||
"button.reset": "Reset",
|
||||
"button.save": "Gem",
|
||||
"button.single-day": "Enkelt dag",
|
||||
"button.view-details": "Vis detajler",
|
||||
"label.accounts": "Kontoer",
|
||||
"label.add-account": "Tilføj konto",
|
||||
"label.add-website": "Tilføj hjemmeside",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Tilbage",
|
||||
"label.cancel": "Afvis",
|
||||
"label.change-password": "Skift adgangskode",
|
||||
"label.confirm-password": "Godkendt adgangskode",
|
||||
"label.copy-to-clipboard": "Kopier til udklipsholder",
|
||||
"label.current-password": "Nuværende adgangskode",
|
||||
"label.custom-range": "Tilpasset interval",
|
||||
"label.dashboard": "Betjeningspanel",
|
||||
"label.date-range": "Datointerval",
|
||||
"label.default-date-range": "Default date range",
|
||||
"label.delete": "Slet",
|
||||
"label.delete-account": "Slet konto",
|
||||
"label.delete-website": "Slet hjemmeside",
|
||||
"label.dismiss": "Dismiss",
|
||||
"label.domain": "Domæne",
|
||||
"label.edit": "Rediger",
|
||||
"label.edit-account": "Rediger konto",
|
||||
"label.edit-website": "Rediger hjemmeside",
|
||||
"label.enable-share-url": "Aktivér delings-URL",
|
||||
"label.invalid": "Ugyldig",
|
||||
"label.invalid-domain": "Ugyldigt domæne",
|
||||
"label.last-days": "Sidste {x} dage",
|
||||
"label.last-hours": "Sidste {x} timer",
|
||||
"label.logged-in-as": "Loggede ind som {username}",
|
||||
"label.login": "Log ind",
|
||||
"label.logout": "Log ud",
|
||||
"label.more": "Mere",
|
||||
"label.name": "Navn",
|
||||
"label.new-password": "Ny adgangskode",
|
||||
"label.password": "Adgangskode",
|
||||
"label.passwords-dont-match": "Adgangskoder matcher ikke",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Opdater",
|
||||
"label.required": "Påkrævet",
|
||||
"label.reset": "Reset",
|
||||
"label.save": "Gem",
|
||||
"label.settings": "Indstillinger",
|
||||
"label.share-url": "Del URL",
|
||||
"label.single-day": "Enkelt dag",
|
||||
"label.this-month": "Denne måned",
|
||||
"label.this-week": "Denne uge",
|
||||
"label.this-year": "Dette år",
|
||||
"label.timezone": "Timezone",
|
||||
"label.today": "Idag",
|
||||
"label.tracking-code": "Sporingskode",
|
||||
"label.unknown": "Ukendt",
|
||||
"label.username": "Brugernavn",
|
||||
"label.view-details": "Vis detajler",
|
||||
"label.websites": "Hjemmesider",
|
||||
"message.active-users": "{x} nuværende {x, plural, one {bruger} other {brugere}}",
|
||||
"message.confirm-delete": "Er du sikker på at du vil slette {target}?",
|
||||
@ -55,6 +65,7 @@
|
||||
"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.no-data-available": "Ingen data tilgængelig.",
|
||||
"message.no-websites-configured": "Du har ikke konfigureret nogen websteder.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Henvisninger",
|
||||
"metrics.unique-visitors": "Unikke besøgende",
|
||||
"metrics.views": "Visninger",
|
||||
"metrics.visitors": "Besøgende",
|
||||
"title.add-account": "Tilføj konto",
|
||||
"title.add-website": "Tilføj hjemmeside",
|
||||
"title.change-password": "Skift adgangskode",
|
||||
"title.delete-account": "Slet konto",
|
||||
"title.delete-website": "Slet hjemmeside",
|
||||
"title.edit-account": "Rediger konto",
|
||||
"title.edit-website": "Rediger hjemmeside",
|
||||
"title.share-url": "Del URL",
|
||||
"title.tracking-code": "Sporingskode"
|
||||
"metrics.visitors": "Besøgende"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Konto hinzufügen",
|
||||
"button.add-website": "Webseite hinzufügen",
|
||||
"button.back": "Zurück",
|
||||
"button.cancel": "Abbrechen",
|
||||
"button.change-password": "Passwort ändern",
|
||||
"button.copy-to-clipboard": "In die Zwischenablage kopieren",
|
||||
"button.date-range": "Datumsbereich",
|
||||
"button.delete": "Löschen",
|
||||
"button.dismiss": "Verwerfen",
|
||||
"button.edit": "Bearbeiten",
|
||||
"button.login": "Anmelden",
|
||||
"button.more": "Mehr",
|
||||
"button.refresh": "Aktualisieren",
|
||||
"button.reset": "Zurücksetzen",
|
||||
"button.save": "Speichern",
|
||||
"button.single-day": "Ein Tag",
|
||||
"button.view-details": "Details anzeigen",
|
||||
"label.accounts": "Konten",
|
||||
"label.add-account": "Konto hinzugfügen",
|
||||
"label.add-website": "Webseite hinzufügen",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Zurück",
|
||||
"label.cancel": "Abbrechen",
|
||||
"label.change-password": "Passwort ändern",
|
||||
"label.confirm-password": "Passwort wiederholen",
|
||||
"label.copy-to-clipboard": "In die Zwischenablage kopieren",
|
||||
"label.current-password": "Derzeitiges Passwort",
|
||||
"label.custom-range": "Benutzerdefinierter Bereich",
|
||||
"label.dashboard": "Übersicht",
|
||||
"label.date-range": "Datumsbereich",
|
||||
"label.default-date-range": "Voreingestellter Datumsbereich",
|
||||
"label.delete": "Löschen",
|
||||
"label.delete-account": "Konto löschen",
|
||||
"label.delete-website": "Webseite löschen",
|
||||
"label.dismiss": "Verwerfen",
|
||||
"label.domain": "Domain",
|
||||
"label.edit": "Bearbeiten",
|
||||
"label.edit-account": "Konto bearbeiten",
|
||||
"label.edit-website": "Webseite bearbeiten",
|
||||
"label.enable-share-url": "Freigabe-URL aktivieren",
|
||||
"label.invalid": "Ungültig",
|
||||
"label.invalid-domain": "Ungültige Domain",
|
||||
"label.last-days": "Letzten {x} Tage",
|
||||
"label.last-hours": "Letzten {x} Stunden",
|
||||
"label.logged-in-as": "Angemeldet als {username}",
|
||||
"label.login": "Anmelden",
|
||||
"label.logout": "Abmelden",
|
||||
"label.more": "Mehr",
|
||||
"label.name": "Name",
|
||||
"label.new-password": "Neues Passwort",
|
||||
"label.password": "Passwort",
|
||||
"label.passwords-dont-match": "Passwörter stimmen nicht überein",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Aktualisieren",
|
||||
"label.required": "Erforderlich",
|
||||
"label.reset": "Zurücksetzen",
|
||||
"label.save": "Speichern",
|
||||
"label.settings": "Einstellungen",
|
||||
"label.share-url": "Freigabe-URL",
|
||||
"label.single-day": "Ein Tag",
|
||||
"label.this-month": "Diesen Monat",
|
||||
"label.this-week": "Diese Woche",
|
||||
"label.this-year": "Dieses Jahr",
|
||||
"label.timezone": "Zeitzone",
|
||||
"label.today": "Heute",
|
||||
"label.tracking-code": "Tracking Kennung",
|
||||
"label.unknown": "Unbekannt",
|
||||
"label.username": "Benutzername",
|
||||
"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?",
|
||||
@ -55,6 +65,7 @@
|
||||
"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": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "Eine neue Version umami {version} ist verfügbar!",
|
||||
"message.no-data-available": "Keine Daten vorhanden.",
|
||||
"message.no-websites-configured": "Es ist keine Webseite vorhanden.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Referrers",
|
||||
"metrics.unique-visitors": "Eindeutige Besucher",
|
||||
"metrics.views": "Aufrufe",
|
||||
"metrics.visitors": "Besucher",
|
||||
"title.add-account": "Konto hinzugfügen",
|
||||
"title.add-website": "Webseite hinzufügen",
|
||||
"title.change-password": "Passwort ändern",
|
||||
"title.delete-account": "Konto löschen",
|
||||
"title.delete-website": "Webseite löschen",
|
||||
"title.edit-account": "Konto bearbeiten",
|
||||
"title.edit-website": "Webseite bearbeiten",
|
||||
"title.share-url": "Freigabe-URL",
|
||||
"title.tracking-code": "Tracking Kennung"
|
||||
"metrics.visitors": "Besucher"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Προσθήκη λογαριασμού",
|
||||
"button.add-website": "Προσθήκη ιστότοπου",
|
||||
"button.back": "Πίσω",
|
||||
"button.cancel": "Ακύρωση",
|
||||
"button.change-password": "Αλλαγή κωδικού",
|
||||
"button.copy-to-clipboard": "Αντιγραφή στο πρόχειρο",
|
||||
"button.date-range": "Εύρος ημερομηνιών",
|
||||
"button.delete": "Διαγραφή",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Επεξεργασία",
|
||||
"button.login": "Είσοδος",
|
||||
"button.more": "Περισσότερα",
|
||||
"button.refresh": "Ανανέωση",
|
||||
"button.reset": "Επαναφορά",
|
||||
"button.save": "Αποθήκευση",
|
||||
"button.single-day": "Ημερήσια",
|
||||
"button.view-details": "Λεπτομέρειες",
|
||||
"label.accounts": "Λογαριασμοί",
|
||||
"label.add-account": "Προσθήκη λογαριασμού",
|
||||
"label.add-website": "Προσθήκη ιστότοπου",
|
||||
"label.administrator": "Διαχειριστής",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "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": "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": "Realtime",
|
||||
"label.realtime-logs": "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};",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "Λήψη κώδικα παρακολούθησης",
|
||||
"message.go-to-settings": "Μεταβείτε στις ρυθμίσεις",
|
||||
"message.incorrect-username-password": "Εσφαλμένο όνομα χρήστη / κωδικός πρόσβασης.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.no-data-available": "Δεν υπάρχουν διαθέσιμα δεδομένα.",
|
||||
"message.no-websites-configured": "Δεν έχετε ρυθμίσει κανένα ιστότοπο.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Παραπομπές",
|
||||
"metrics.unique-visitors": "Μοναδικοί επισκέπτες",
|
||||
"metrics.views": "Προβολές",
|
||||
"metrics.visitors": "Επισκέπτες",
|
||||
"title.add-account": "Προσθήκη λογαριασμού",
|
||||
"title.add-website": "Προσθήκη ιστότοπου",
|
||||
"title.change-password": "Αλλαγή κωδικού",
|
||||
"title.delete-account": "Διαγραφή λογαριασμού",
|
||||
"title.delete-website": "Διαγραφή ιστότοπου",
|
||||
"title.edit-account": "Επεξεργασία λογαριασμού",
|
||||
"title.edit-website": "Επεξεργασία ιστότοπου",
|
||||
"title.share-url": "Κοινοποίηση διεύθυνσης URL",
|
||||
"title.tracking-code": "Κωδικός παρακολούθησης"
|
||||
"metrics.visitors": "Επισκέπτες"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Add account",
|
||||
"button.add-website": "Add website",
|
||||
"button.back": "Back",
|
||||
"button.cancel": "Cancel",
|
||||
"button.change-password": "Change password",
|
||||
"button.copy-to-clipboard": "Copy to clipboard",
|
||||
"button.date-range": "Date range",
|
||||
"button.delete": "Delete",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Edit",
|
||||
"button.login": "Login",
|
||||
"button.more": "More",
|
||||
"button.refresh": "Refresh",
|
||||
"button.reset": "Reset",
|
||||
"button.save": "Save",
|
||||
"button.single-day": "Single day",
|
||||
"button.view-details": "View details",
|
||||
"label.accounts": "Accounts",
|
||||
"label.add-account": "Add account",
|
||||
"label.add-website": "Add website",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Back",
|
||||
"label.cancel": "Cancel",
|
||||
"label.change-password": "Change password",
|
||||
"label.confirm-password": "Confirm password",
|
||||
"label.copy-to-clipboard": "Copy to clipboard",
|
||||
"label.current-password": "Current password",
|
||||
"label.custom-range": "Custom range",
|
||||
"label.dashboard": "Dashboard",
|
||||
"label.date-range": "Date range",
|
||||
"label.default-date-range": "Default date range",
|
||||
"label.delete": "Delete",
|
||||
"label.delete-account": "Delete account",
|
||||
"label.delete-website": "Delete website",
|
||||
"label.dismiss": "Dismiss",
|
||||
"label.domain": "Domain",
|
||||
"label.edit": "Edit",
|
||||
"label.edit-account": "Edit account",
|
||||
"label.edit-website": "Edit website",
|
||||
"label.enable-share-url": "Enable share URL",
|
||||
"label.invalid": "Invalid",
|
||||
"label.invalid-domain": "Invalid domain",
|
||||
"label.last-days": "Last {x} days",
|
||||
"label.last-hours": "Last {x} hours",
|
||||
"label.logged-in-as": "Logged in as {username}",
|
||||
"label.login": "Login",
|
||||
"label.logout": "Logout",
|
||||
"label.more": "More",
|
||||
"label.name": "Name",
|
||||
"label.new-password": "New password",
|
||||
"label.password": "Password",
|
||||
"label.passwords-dont-match": "Passwords don't match",
|
||||
"label.profile": "Profile",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Refresh",
|
||||
"label.required": "Required",
|
||||
"label.reset": "Reset",
|
||||
"label.save": "Save",
|
||||
"label.settings": "Settings",
|
||||
"label.share-url": "Share URL",
|
||||
"label.single-day": "Single day",
|
||||
"label.this-month": "This month",
|
||||
"label.this-week": "This week",
|
||||
"label.this-year": "This year",
|
||||
"label.timezone": "Timezone",
|
||||
"label.today": "Today",
|
||||
"label.tracking-code": "Tracking code",
|
||||
"label.unknown": "Unknown",
|
||||
"label.username": "Username",
|
||||
"label.view-details": "View details",
|
||||
"label.websites": "Websites",
|
||||
"message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
|
||||
"message.confirm-delete": "Are your sure you want to delete {target}?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "Get tracking code",
|
||||
"message.go-to-settings": "Go to settings",
|
||||
"message.incorrect-username-password": "Incorrect username/password.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.no-data-available": "No data available.",
|
||||
"message.no-websites-configured": "You don't have any websites configured.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Referrers",
|
||||
"metrics.unique-visitors": "Unique visitors",
|
||||
"metrics.views": "Views",
|
||||
"metrics.visitors": "Visitors",
|
||||
"title.add-account": "Add account",
|
||||
"title.add-website": "Add website",
|
||||
"title.change-password": "Change password",
|
||||
"title.delete-account": "Delete account",
|
||||
"title.delete-website": "Delete website",
|
||||
"title.edit-account": "Edit account",
|
||||
"title.edit-website": "Edit website",
|
||||
"title.share-url": "Share URL",
|
||||
"title.tracking-code": "Tracking code"
|
||||
"metrics.visitors": "Visitors"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Agregar usuario",
|
||||
"button.add-website": "Agregar sitio",
|
||||
"button.back": "Atrás",
|
||||
"button.cancel": "Cancelar",
|
||||
"button.change-password": "Cambiar contraseña",
|
||||
"button.copy-to-clipboard": "Copiar al portapapeles",
|
||||
"button.date-range": "Date range",
|
||||
"button.delete": "Eliminar",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Editar",
|
||||
"button.login": "Iniciar sesión",
|
||||
"button.more": "Más",
|
||||
"button.refresh": "Refresh",
|
||||
"button.reset": "Reset",
|
||||
"button.save": "Guardar",
|
||||
"button.single-day": "Single day",
|
||||
"button.view-details": "Ver detalles",
|
||||
"label.accounts": "Usuarios",
|
||||
"label.add-account": "Agregar usuario",
|
||||
"label.add-website": "Agregar sitio",
|
||||
"label.administrator": "Administrador",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Atrás",
|
||||
"label.cancel": "Cancelar",
|
||||
"label.change-password": "Cambiar contraseña",
|
||||
"label.confirm-password": "Confirmar contraseña",
|
||||
"label.copy-to-clipboard": "Copiar al portapapeles",
|
||||
"label.current-password": "Contraseña actual",
|
||||
"label.custom-range": "Custom range",
|
||||
"label.dashboard": "Panel de control",
|
||||
"label.date-range": "Date range",
|
||||
"label.default-date-range": "Default date range",
|
||||
"label.delete": "Eliminar",
|
||||
"label.delete-account": "Eliminar usuario",
|
||||
"label.delete-website": "Eliminar sitio",
|
||||
"label.dismiss": "Dismiss",
|
||||
"label.domain": "Dominio",
|
||||
"label.edit": "Editar",
|
||||
"label.edit-account": "Editar usuario",
|
||||
"label.edit-website": "Editar sitio",
|
||||
"label.enable-share-url": "Habilitar compartir URL",
|
||||
"label.invalid": "Inválido",
|
||||
"label.invalid-domain": "Dominio inválido",
|
||||
"label.last-days": "Últimos {x} días",
|
||||
"label.last-hours": "Últimas {x} horas",
|
||||
"label.logged-in-as": "Sesión iniciada como {username}",
|
||||
"label.login": "Iniciar sesión",
|
||||
"label.logout": "Cerrar sesión",
|
||||
"label.more": "Más",
|
||||
"label.name": "Nombre",
|
||||
"label.new-password": "Nueva contraseña",
|
||||
"label.password": "Contraseña",
|
||||
"label.passwords-dont-match": "Las contraseñas no coinciden",
|
||||
"label.profile": "Perfil",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Refresh",
|
||||
"label.required": "Requerido",
|
||||
"label.reset": "Reset",
|
||||
"label.save": "Guardar",
|
||||
"label.settings": "Configuraciones",
|
||||
"label.share-url": "Compartir URL",
|
||||
"label.single-day": "Single day",
|
||||
"label.this-month": "Este mes",
|
||||
"label.this-week": "Esta semana",
|
||||
"label.this-year": "Este año",
|
||||
"label.timezone": "Timezone",
|
||||
"label.today": "Hoy",
|
||||
"label.tracking-code": "Código de rastreo",
|
||||
"label.unknown": "Unknown",
|
||||
"label.username": "Nombre de usuario",
|
||||
"label.view-details": "Ver detalles",
|
||||
"label.websites": "Sitios",
|
||||
"message.active-users": "{x} {x, plural, one {activo} other {activos}}",
|
||||
"message.confirm-delete": "¿Estás seguro(a) de querer eliminar {target}?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "Obtener código de rastreo",
|
||||
"message.go-to-settings": "Ir a la configuración",
|
||||
"message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.no-data-available": "Sin información disponible.",
|
||||
"message.no-websites-configured": "No tienes ningún sitio configurado.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Referentes",
|
||||
"metrics.unique-visitors": "Visitantes únicos",
|
||||
"metrics.views": "Vistas",
|
||||
"metrics.visitors": "Visitantes",
|
||||
"title.add-account": "Agregar usuario",
|
||||
"title.add-website": "Agregar sitio",
|
||||
"title.change-password": "Cambiar contraseña",
|
||||
"title.delete-account": "Eliminar usuario",
|
||||
"title.delete-website": "Eliminar sitio",
|
||||
"title.edit-account": "Editar usuario",
|
||||
"title.edit-website": "Editar sitio",
|
||||
"title.share-url": "Compartir URL",
|
||||
"title.tracking-code": "Código de rastreo"
|
||||
"metrics.visitors": "Visitantes"
|
||||
}
|
||||
|
99
lang/fi-FI.json
Normal file
99
lang/fi-FI.json
Normal file
@ -0,0 +1,99 @@
|
||||
{
|
||||
"label.accounts": "Tilit",
|
||||
"label.add-account": "Lisää tili",
|
||||
"label.add-website": "Lisää verkkosivu",
|
||||
"label.administrator": "Järjestelmänvalvoja",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Takaisin",
|
||||
"label.cancel": "Peruuta",
|
||||
"label.change-password": "Vaihda salasana",
|
||||
"label.confirm-password": "Vahvista salasana",
|
||||
"label.copy-to-clipboard": "Kopioi leikepöydälle",
|
||||
"label.current-password": "Nykyinen salasana",
|
||||
"label.custom-range": "Mukautettu jakso",
|
||||
"label.dashboard": "Dashboard",
|
||||
"label.date-range": "Ajanjakso",
|
||||
"label.default-date-range": "Oletusajanjakso",
|
||||
"label.delete": "Poista",
|
||||
"label.delete-account": "Poista tili",
|
||||
"label.delete-website": "Poista verkkosivu",
|
||||
"label.dismiss": "Hylkää",
|
||||
"label.domain": "Verkkotunnus",
|
||||
"label.edit": "Muokkaa",
|
||||
"label.edit-account": "Muokkaa tiliä",
|
||||
"label.edit-website": "Muokkaa verkkosivua",
|
||||
"label.enable-share-url": "Ota jakamisen URL-osoite käyttöön",
|
||||
"label.invalid": "Virheellinen",
|
||||
"label.invalid-domain": "Virheellinen verkkotunnus",
|
||||
"label.last-days": "Viimeisimmät {x} päivät",
|
||||
"label.last-hours": "Viimeisimmät {x} tunnit",
|
||||
"label.logged-in-as": "Kirjautuneena sisään nimellä {username}",
|
||||
"label.login": "Kirjaudu sisään",
|
||||
"label.logout": "Kirjaudu ulos",
|
||||
"label.more": "Lisää",
|
||||
"label.name": "Nimi",
|
||||
"label.new-password": "Uusi salasana",
|
||||
"label.password": "Salasana",
|
||||
"label.passwords-dont-match": "Salasanat eivät täsmää",
|
||||
"label.profile": "Profiili",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Päivitä",
|
||||
"label.required": "Vaaditaan",
|
||||
"label.reset": "Nollaa",
|
||||
"label.save": "Tallenna",
|
||||
"label.settings": "Asetukset",
|
||||
"label.share-url": "Jaa URL",
|
||||
"label.single-day": "Yksi päivä",
|
||||
"label.this-month": "Tämä kuukausi",
|
||||
"label.this-week": "Tämä viikko",
|
||||
"label.this-year": "Tämä vuosi",
|
||||
"label.timezone": "Aikavyöhyke",
|
||||
"label.today": "Tänään",
|
||||
"label.tracking-code": "Seurantakoodi",
|
||||
"label.unknown": "Tuntematon",
|
||||
"label.username": "Käyttäjänimi",
|
||||
"label.view-details": "Katso tiedot",
|
||||
"label.websites": "Verkkosivut",
|
||||
"message.active-users": "{x} nykyinen {x, plural, yksi {visitor} muut {visitors}}",
|
||||
"message.confirm-delete": "Haluatko varmasti poistaa {target}?",
|
||||
"message.copied": "Kopioitu!",
|
||||
"message.delete-warning": "Kaikki siihen liittyvät tiedot poistetaan.",
|
||||
"message.failure": "Jotain meni väärin.",
|
||||
"message.get-share-url": "Hanki jakamisen URL-osoite",
|
||||
"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.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.",
|
||||
"message.page-not-found": "Sivua ei löydetty.",
|
||||
"message.powered-by": "Voimanlähteenä {name}",
|
||||
"message.save-success": "Tallennettu onnistuneesti.",
|
||||
"message.share-url": "Tämä on julkisesti jaettu URL-osoitteelle {target}.",
|
||||
"message.track-stats": "Jos haluat seurata kohteen {target} tilastoja, aseta seuraava koodi verkkosivustosi {head} osioon.",
|
||||
"message.type-delete": "Kirjoita {delete} alla olevaan ruutuun vahvistaaksesi.",
|
||||
"metrics.actions": "Toiminnat",
|
||||
"metrics.average-visit-time": "Keskimääräinen vierailuaika",
|
||||
"metrics.bounce-rate": "Välitön poistuminen",
|
||||
"metrics.browsers": "Selaimet",
|
||||
"metrics.countries": "Maat",
|
||||
"metrics.device.desktop": "Pöytäkone",
|
||||
"metrics.device.laptop": "Kannettava tietokone",
|
||||
"metrics.device.mobile": "Mobiili",
|
||||
"metrics.device.tablet": "Tabletti",
|
||||
"metrics.devices": "Laitteet",
|
||||
"metrics.events": "Tapahtumat",
|
||||
"metrics.filter.combined": "Yhdistetty",
|
||||
"metrics.filter.domain-only": "Vain verkkotunnus",
|
||||
"metrics.filter.raw": "Käsittelemätön",
|
||||
"metrics.operating-systems": "Käyttöjärjestelmät",
|
||||
"metrics.page-views": "Sivun näyttökertoja",
|
||||
"metrics.pages": "Sivut",
|
||||
"metrics.referrers": "Viittaajat",
|
||||
"metrics.unique-visitors": "Uniikit vierailijat",
|
||||
"metrics.views": "Näyttökertoja",
|
||||
"metrics.visitors": "Vierailijat"
|
||||
}
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Ger brúkara",
|
||||
"button.add-website": "Legg heimasíðu til",
|
||||
"button.back": "Aftur",
|
||||
"button.cancel": "Strika",
|
||||
"button.change-password": "Broyt loyniorð",
|
||||
"button.copy-to-clipboard": "Kopier til clipboard",
|
||||
"button.date-range": "Vel dato",
|
||||
"button.delete": "Sletta",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Ger broyting",
|
||||
"button.login": "Rita inn",
|
||||
"button.more": "Meira",
|
||||
"button.refresh": "Endurskapa",
|
||||
"button.reset": "Nulstilla",
|
||||
"button.save": "Goym",
|
||||
"button.single-day": "Einkultur dagur",
|
||||
"button.view-details": "Vís upplýsingar",
|
||||
"label.accounts": "Brúkarar",
|
||||
"label.add-account": "Ger brúkara",
|
||||
"label.add-website": "Legg heimasíðu avtrat",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Aftur",
|
||||
"label.cancel": "Strika",
|
||||
"label.change-password": "Skift loyniorð",
|
||||
"label.confirm-password": "Vátta loyniorð",
|
||||
"label.copy-to-clipboard": "Kopier til clipboard",
|
||||
"label.current-password": "Núverandi loyniorð",
|
||||
"label.custom-range": "Tillaga spenni",
|
||||
"label.dashboard": "Yvirlitsskíggi",
|
||||
"label.date-range": "Vel dato",
|
||||
"label.default-date-range": "Standard dato",
|
||||
"label.delete": "Sletta",
|
||||
"label.delete-account": "Sletta brúkara",
|
||||
"label.delete-website": "Sletta heimasíðu",
|
||||
"label.dismiss": "Dismiss",
|
||||
"label.domain": "Økisnavn",
|
||||
"label.edit": "Ger broyting",
|
||||
"label.edit-account": "Broyt brúkara",
|
||||
"label.edit-website": "Broyt heimasíðu",
|
||||
"label.enable-share-url": "Virkja deili leinki",
|
||||
"label.invalid": "Ógilda",
|
||||
"label.invalid-domain": "Ógilt økisnavn",
|
||||
"label.last-days": "Seinastu {x} dagarnar",
|
||||
"label.last-hours": "Seinastu {x} tímanar",
|
||||
"label.logged-in-as": "Ritaður inn sum {username}",
|
||||
"label.login": "Rita inn",
|
||||
"label.logout": "Rita út",
|
||||
"label.more": "Meira",
|
||||
"label.name": "Navn",
|
||||
"label.new-password": "Nýtt loyniorð",
|
||||
"label.password": "Loyniorð",
|
||||
"label.passwords-dont-match": "Loyniorðini eru ikki eins",
|
||||
"label.profile": "Brúkari",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Endurskapa",
|
||||
"label.required": "Krav",
|
||||
"label.reset": "Nulstilla",
|
||||
"label.save": "Goym",
|
||||
"label.settings": "Stillingar",
|
||||
"label.share-url": "Deil leinku",
|
||||
"label.single-day": "Einkultur dagur",
|
||||
"label.this-month": "Hendan mánan",
|
||||
"label.this-week": "Hesa vikuna",
|
||||
"label.this-year": "Hetta árið",
|
||||
"label.timezone": "Tíðarsona",
|
||||
"label.today": "Í dag",
|
||||
"label.tracking-code": "Spori kota",
|
||||
"label.unknown": "Ókent",
|
||||
"label.username": "Brúkaranavn",
|
||||
"label.view-details": "Vís upplýsingar",
|
||||
"label.websites": "Heimasíður",
|
||||
"message.active-users": "{x} í løtuni {x, plural, one {vitjandi} other { vitjandi }}",
|
||||
"message.confirm-delete": "Ert tú sikkur at tú ynskir at sletta {target}?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "Fá sporings kotu",
|
||||
"message.go-to-settings": "Far til stillingar",
|
||||
"message.incorrect-username-password": "Skeivt brúkaranavn/loyniorð.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.no-data-available": "Einki data tøk.",
|
||||
"message.no-websites-configured": "Tú hevur ongar heimasíður stillaða til.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Framsendingar",
|
||||
"metrics.unique-visitors": "Einsýna vitjanir",
|
||||
"metrics.views": "Vitjanir",
|
||||
"metrics.visitors": "Vitjandi",
|
||||
"title.add-account": "Ger brúkara",
|
||||
"title.add-website": "Legg heimasíðu avtrat",
|
||||
"title.change-password": "Skift loyniorð",
|
||||
"title.delete-account": "Sletta brúkara",
|
||||
"title.delete-website": "Sletta heimasíðu",
|
||||
"title.edit-account": "Broyt brúkara",
|
||||
"title.edit-website": "Broyt heimasíðu",
|
||||
"title.share-url": "Deil leinku",
|
||||
"title.tracking-code": "Spori kota"
|
||||
"metrics.visitors": "Vitjandi"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Ajouter un compte",
|
||||
"button.add-website": "Ajouter un site",
|
||||
"button.back": "Retour",
|
||||
"button.cancel": "Annuler",
|
||||
"button.change-password": "Changer de mot de passse",
|
||||
"button.copy-to-clipboard": "Copier dans le presse papier",
|
||||
"button.date-range": "Date range",
|
||||
"button.delete": "Supprimer",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Modifier",
|
||||
"button.login": "Connexion",
|
||||
"button.more": "Plus",
|
||||
"button.refresh": "Refresh",
|
||||
"button.reset": "Reset",
|
||||
"button.save": "Sauvegarder",
|
||||
"button.single-day": "Single day",
|
||||
"button.view-details": "Voir les details",
|
||||
"label.accounts": "Comptes",
|
||||
"label.add-account": "Ajouter un compte",
|
||||
"label.add-website": "Ajouter un site",
|
||||
"label.administrator": "Administrateur",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Retour",
|
||||
"label.cancel": "Annuler",
|
||||
"label.change-password": "Changer le mot de passe",
|
||||
"label.confirm-password": "Confirmation du mot de passe",
|
||||
"label.copy-to-clipboard": "Copier dans le presse papier",
|
||||
"label.current-password": "Mot de passe actuel",
|
||||
"label.custom-range": "Plage personnalisée",
|
||||
"label.dashboard": "Tableau de bord",
|
||||
"label.date-range": "Date range",
|
||||
"label.default-date-range": "Default date range",
|
||||
"label.delete": "Supprimer",
|
||||
"label.delete-account": "Supprimer le compte",
|
||||
"label.delete-website": "Suprimer le site",
|
||||
"label.dismiss": "Dismiss",
|
||||
"label.domain": "Domaine",
|
||||
"label.edit": "Modifier",
|
||||
"label.edit-account": "Modifier le compte",
|
||||
"label.edit-website": "Modifier le site",
|
||||
"label.enable-share-url": "Activer le partage d'URL",
|
||||
"label.invalid": "Invalide",
|
||||
"label.invalid-domain": "Domaine invalide",
|
||||
"label.last-days": "{x} derniers jours",
|
||||
"label.last-hours": "{x} dernières heures",
|
||||
"label.logged-in-as": "Connecté en tant que {username}",
|
||||
"label.login": "Connexion",
|
||||
"label.logout": "Déconnexion",
|
||||
"label.more": "Plus",
|
||||
"label.name": "Nom",
|
||||
"label.new-password": "Nouveau mot de passe",
|
||||
"label.password": "Mot de passe",
|
||||
"label.passwords-dont-match": "Les mots de passe ne correspondent pas",
|
||||
"label.profile": "Profile",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Refresh",
|
||||
"label.required": "Requis",
|
||||
"label.reset": "Reset",
|
||||
"label.save": "Sauvegarder",
|
||||
"label.settings": "Paramètres",
|
||||
"label.share-url": "Partager l'URL",
|
||||
"label.single-day": "Single day",
|
||||
"label.this-month": "Ce mois ci",
|
||||
"label.this-week": "Cette semaine",
|
||||
"label.this-year": "Cette année",
|
||||
"label.timezone": "Timezone",
|
||||
"label.today": "Aujourd'hui",
|
||||
"label.tracking-code": "Code de suivi",
|
||||
"label.unknown": "Unknown",
|
||||
"label.username": "Nom d'utilisateur",
|
||||
"label.view-details": "Voir les details",
|
||||
"label.websites": "Sites",
|
||||
"message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement",
|
||||
"message.confirm-delete": "Êtes-vous sur de vouloir supprimer {target}?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "Obtenez le code de suivi",
|
||||
"message.go-to-settings": "Aller aux paramètres",
|
||||
"message.incorrect-username-password": "nom d'utilisateurs/mot de passe incorrect.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.no-data-available": "Pas de données disponibles.",
|
||||
"message.no-websites-configured": "Vous n'avez configuré aucun site Web.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "URL Référentes",
|
||||
"metrics.unique-visitors": "Visiteurs uniques",
|
||||
"metrics.views": "Vues",
|
||||
"metrics.visitors": "Visiteurs",
|
||||
"title.add-account": "Ajouter un compte",
|
||||
"title.add-website": "Ajouter un site",
|
||||
"title.change-password": "Changer le mot de passe",
|
||||
"title.delete-account": "Supprimer le compte",
|
||||
"title.delete-website": "Suprimer le site",
|
||||
"title.edit-account": "Modifier le compte",
|
||||
"title.edit-website": "Modifier le site",
|
||||
"title.share-url": "Partager l'URL",
|
||||
"title.tracking-code": "Code de suivi"
|
||||
"metrics.visitors": "Visiteurs"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Tambah akun",
|
||||
"button.add-website": "Tambah situs web",
|
||||
"button.back": "Kembali",
|
||||
"button.cancel": "Batal",
|
||||
"button.change-password": "Ganti password",
|
||||
"button.copy-to-clipboard": "Salin ke papan klip",
|
||||
"button.date-range": "Rentang tanggal",
|
||||
"button.delete": "Hapus",
|
||||
"button.dismiss": "Tutup",
|
||||
"button.edit": "Sunting",
|
||||
"button.login": "Masuk",
|
||||
"button.more": "Lebih banyak",
|
||||
"button.refresh": "Segarkan",
|
||||
"button.reset": "Atur ulang",
|
||||
"button.save": "Simpan",
|
||||
"button.single-day": "Sehari",
|
||||
"button.view-details": "Lihat Detil",
|
||||
"label.accounts": "Akun",
|
||||
"label.add-account": "Tambah akun",
|
||||
"label.add-website": "Tambah situs web",
|
||||
"label.administrator": "Pengelola",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Kembali",
|
||||
"label.cancel": "Batal",
|
||||
"label.change-password": "Ganti kata sandi",
|
||||
"label.confirm-password": "Konfirmasi kata sandi",
|
||||
"label.copy-to-clipboard": "Salin ke papan klip",
|
||||
"label.current-password": "Kata sandi sekarang",
|
||||
"label.custom-range": "Rentang khusus",
|
||||
"label.dashboard": "Dasbor",
|
||||
"label.date-range": "Rentang tanggal",
|
||||
"label.default-date-range": "Rentang tanggal default",
|
||||
"label.delete": "Hapus",
|
||||
"label.delete-account": "Hapus akun",
|
||||
"label.delete-website": "Hapus situs web",
|
||||
"label.dismiss": "Tutup",
|
||||
"label.domain": "Domain",
|
||||
"label.edit": "Sunting",
|
||||
"label.edit-account": "Sunting akun",
|
||||
"label.edit-website": "Sunting situs web",
|
||||
"label.enable-share-url": "Aktifkan URL berbagi",
|
||||
"label.invalid": "Tidak valid",
|
||||
"label.invalid-domain": "Domain tidak valid",
|
||||
"label.last-days": "{x} hari terakhir",
|
||||
"label.last-hours": "{x} jam terakhir",
|
||||
"label.logged-in-as": "Masuk sebagai {username}",
|
||||
"label.login": "Masuk",
|
||||
"label.logout": "Keluar",
|
||||
"label.more": "Lebih banyak",
|
||||
"label.name": "Nama",
|
||||
"label.new-password": "Kata sandi baru",
|
||||
"label.password": "Kata sandi",
|
||||
"label.passwords-dont-match": "Kata sandi tidak cocok",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Segarkan",
|
||||
"label.required": "Wajib",
|
||||
"label.reset": "Atur ulang",
|
||||
"label.save": "Simpan",
|
||||
"label.settings": "Pengaturan",
|
||||
"label.share-url": "Bagikan URL",
|
||||
"label.single-day": "Sehari",
|
||||
"label.this-month": "Bulan ini",
|
||||
"label.this-week": "Minggu ini",
|
||||
"label.this-year": "Tahun ini",
|
||||
"label.timezone": "Zona waktu",
|
||||
"label.today": "Hari ini",
|
||||
"label.tracking-code": "Kode lacak",
|
||||
"label.unknown": "Tidak diketahui",
|
||||
"label.username": "Nama pengguna",
|
||||
"label.view-details": "Lihat Detil",
|
||||
"label.websites": "Situs web",
|
||||
"message.active-users": "{x} pengunjung saat ini",
|
||||
"message.confirm-delete": "Apakah kamu yakin ingin menghapus {target}?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "Dapatkan kode pelacakan",
|
||||
"message.go-to-settings": "Pergi ke pengaturan",
|
||||
"message.incorrect-username-password": "Nama pengguna/kata sandi salah.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "Versi terbaru umami {version} telah tersedia!",
|
||||
"message.no-data-available": "Tidak ada data.",
|
||||
"message.no-websites-configured": "Anda tidak memiliki situs web yang dikonfigurasi.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Perujuk",
|
||||
"metrics.unique-visitors": "Pengunjung unik",
|
||||
"metrics.views": "Tampilan",
|
||||
"metrics.visitors": "Pengunjung",
|
||||
"title.add-account": "Tambah akun",
|
||||
"title.add-website": "Tambah situs web",
|
||||
"title.change-password": "Ganti kata sandi",
|
||||
"title.delete-account": "Hapus akun",
|
||||
"title.delete-website": "Hapus situs web",
|
||||
"title.edit-account": "Sunting akun",
|
||||
"title.edit-website": "Sunting situs web",
|
||||
"title.share-url": "Bagikan URL",
|
||||
"title.tracking-code": "Kode lacak"
|
||||
"metrics.visitors": "Pengunjung"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "アカウントを追加する",
|
||||
"button.add-website": "Webサイトを追加する",
|
||||
"button.back": "戻る",
|
||||
"button.cancel": "キャンセル",
|
||||
"button.change-password": "パスワード変更",
|
||||
"button.copy-to-clipboard": "クリップボードにコピー",
|
||||
"button.date-range": "日付範囲",
|
||||
"button.delete": "削除",
|
||||
"button.dismiss": "無視する",
|
||||
"button.edit": "編集",
|
||||
"button.login": "ログイン",
|
||||
"button.more": "さらに表示",
|
||||
"button.refresh": "更新",
|
||||
"button.reset": "リセット",
|
||||
"button.save": "保存",
|
||||
"button.single-day": "一日のみ",
|
||||
"button.view-details": "詳細を見る",
|
||||
"label.accounts": "アカウント",
|
||||
"label.add-account": "アカウントの追加",
|
||||
"label.add-website": "Webサイトの追加",
|
||||
"label.administrator": "管理者",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "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": "Webサイトの削除",
|
||||
"label.dismiss": "無視する",
|
||||
"label.domain": "ドメイン",
|
||||
"label.edit": "編集",
|
||||
"label.edit-account": "アカウントの編集",
|
||||
"label.edit-website": "Webサイトの編集",
|
||||
"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": "Realtime",
|
||||
"label.realtime-logs": "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": "Webサイト",
|
||||
"message.active-users": "{x}人が閲覧中です。",
|
||||
"message.confirm-delete": "{target}を削除してもよろしいですか?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "トラッキングコードを取得",
|
||||
"message.go-to-settings": "設定する",
|
||||
"message.incorrect-username-password": "ユーザー名/パスワードが正しくありません。",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "新しいバージョン({version})が利用可能です!",
|
||||
"message.no-data-available": "データがありません。",
|
||||
"message.no-websites-configured": "Webサイトが設定されていません。",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "リファラー",
|
||||
"metrics.unique-visitors": "ユニーク訪問者数",
|
||||
"metrics.views": "閲覧数",
|
||||
"metrics.visitors": "訪問者数",
|
||||
"title.add-account": "アカウントの追加",
|
||||
"title.add-website": "Webサイトの追加",
|
||||
"title.change-password": "パスワード変更",
|
||||
"title.delete-account": "アカウントの削除",
|
||||
"title.delete-website": "Webサイトの削除",
|
||||
"title.edit-account": "アカウントの編集",
|
||||
"title.edit-website": "Webサイトの編集",
|
||||
"title.share-url": "共有リンク",
|
||||
"title.tracking-code": "トラッキングコード"
|
||||
"metrics.visitors": "訪問者数"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Хэрэглэгч нэмэх",
|
||||
"button.add-website": "Веб нэмэх",
|
||||
"button.back": "Буцах",
|
||||
"button.cancel": "Цуцлах",
|
||||
"button.change-password": "Нууц үг солих",
|
||||
"button.copy-to-clipboard": "Хуулах",
|
||||
"button.date-range": "Хугацааны мужид",
|
||||
"button.delete": "Устгах",
|
||||
"button.dismiss": "Үл хэргэсэх",
|
||||
"button.edit": "Засах",
|
||||
"button.login": "Нэвтрэх",
|
||||
"button.more": "Цааш",
|
||||
"button.refresh": "Сэргээх",
|
||||
"button.reset": "Хуучин хэвд нь оруулах",
|
||||
"button.save": "Хадгалах",
|
||||
"button.single-day": "Нэг өдөр",
|
||||
"button.view-details": "Дэлгэрүүлж харах",
|
||||
"label.accounts": "Хэрэглэгчид",
|
||||
"label.add-account": "Хэрэглэгч нэмэх",
|
||||
"label.add-website": "Веб нэмэх",
|
||||
"label.administrator": "Админ",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "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": "Realtime",
|
||||
"label.realtime-logs": "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}-г устгахдаа итгэлтэй байна уу?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "Мөрдөх код авах",
|
||||
"message.go-to-settings": "Тохиргоо руу очих",
|
||||
"message.incorrect-username-password": "Буруу хэрэглэгчийн нэр/нууц үг.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "Umami-гийн шинэ хувилбар {version} гарсан байна!",
|
||||
"message.no-data-available": "Өгөгдөл алга.",
|
||||
"message.no-websites-configured": "Та ямар нэгэн веб тохируулаагүй байна.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Чиглүүлэгч",
|
||||
"metrics.unique-visitors": "Зочид",
|
||||
"metrics.views": "Үзсэн",
|
||||
"metrics.visitors": "Зочид",
|
||||
"title.add-account": "Хэрэглэгч нэмэх",
|
||||
"title.add-website": "Веб нэмэх",
|
||||
"title.change-password": "Нууц үг солих",
|
||||
"title.delete-account": "Хэрэглэгч устгах",
|
||||
"title.delete-website": "Веб устгах",
|
||||
"title.edit-account": "Хэрэглэгч засах",
|
||||
"title.edit-website": "Веб засах",
|
||||
"title.share-url": "Хуваалцах холбоос",
|
||||
"title.tracking-code": "Мөрдөх код"
|
||||
"metrics.visitors": "Зочид"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Legg til konto",
|
||||
"button.add-website": "Legg til nettsted",
|
||||
"button.back": "Tilbake",
|
||||
"button.cancel": "Avvis",
|
||||
"button.change-password": "Bytt passord",
|
||||
"button.copy-to-clipboard": "Kopier til utklippstavle",
|
||||
"button.date-range": "Datointervall",
|
||||
"button.delete": "Slett",
|
||||
"button.dismiss": "Avbryt",
|
||||
"button.edit": "Rediger",
|
||||
"button.login": "Logg inn",
|
||||
"button.more": "Mer",
|
||||
"button.refresh": "Oppdater",
|
||||
"button.reset": "Nullstill",
|
||||
"button.save": "Lagre",
|
||||
"button.single-day": "Enkelt dag",
|
||||
"button.view-details": "Vis detaljer",
|
||||
"label.accounts": "Kontoer",
|
||||
"label.add-account": "Legg til konto",
|
||||
"label.add-website": "Legg til nettsted",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Tilbake",
|
||||
"label.cancel": "Avvis",
|
||||
"label.change-password": "Bytt passord",
|
||||
"label.confirm-password": "Godkjenn passord",
|
||||
"label.copy-to-clipboard": "Kopier til utklippstavle",
|
||||
"label.current-password": "Nåværende passord",
|
||||
"label.custom-range": "Egendefinert utvalg",
|
||||
"label.dashboard": "Dashboard",
|
||||
"label.date-range": "Datointervall",
|
||||
"label.default-date-range": "Standard datoperiode",
|
||||
"label.delete": "Slett",
|
||||
"label.delete-account": "Slett konto",
|
||||
"label.delete-website": "Slett nettstedet",
|
||||
"label.dismiss": "Avbryt",
|
||||
"label.domain": "Domene",
|
||||
"label.edit": "Rediger",
|
||||
"label.edit-account": "Rediger konto",
|
||||
"label.edit-website": "Rediger nettsted",
|
||||
"label.enable-share-url": "Aktiver delings-URL",
|
||||
"label.invalid": "Ugyldig",
|
||||
"label.invalid-domain": "Ugyldig domene",
|
||||
"label.last-days": "Siste {x} dager",
|
||||
"label.last-hours": "Siste {x} timer",
|
||||
"label.logged-in-as": "Logget på som {brukernavn}",
|
||||
"label.login": "Logg inn",
|
||||
"label.logout": "Logg ut",
|
||||
"label.more": "Mer",
|
||||
"label.name": "Navn",
|
||||
"label.new-password": "Nytt passord",
|
||||
"label.password": "Passord",
|
||||
"label.passwords-dont-match": "Passordene er ikke like",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Oppdater",
|
||||
"label.required": "Påkrevd",
|
||||
"label.reset": "Nullstill",
|
||||
"label.save": "Lagre",
|
||||
"label.settings": "Innstillinger",
|
||||
"label.share-url": "Del URL",
|
||||
"label.single-day": "Enkelt dag",
|
||||
"label.this-month": "Denne måneden",
|
||||
"label.this-week": "Denne uka",
|
||||
"label.this-year": "I år",
|
||||
"label.timezone": "Tidssone",
|
||||
"label.today": "I dag",
|
||||
"label.tracking-code": "Sporingskode",
|
||||
"label.unknown": "Ukjent",
|
||||
"label.username": "Brukernavn",
|
||||
"label.view-details": "Vis detaljer",
|
||||
"label.websites": "Nettsteder",
|
||||
"message.active-users": "{x} {x, plural, one {besøkende} other {besøkende}} nå",
|
||||
"message.confirm-delete": "Er du sikker på at du vil slette {target}?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "Få sporingskode",
|
||||
"message.go-to-settings": "Gå til innstillinger",
|
||||
"message.incorrect-username-password": "Ugyldig brukernavn/passord.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "En ny versjon av umami {version} er tilgjengelig!",
|
||||
"message.no-data-available": "Ingen data tilgjengelig.",
|
||||
"message.no-websites-configured": "Du har ikke satt opp noen nettsteder.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Referanser",
|
||||
"metrics.unique-visitors": "Unike besøkende",
|
||||
"metrics.views": "Visninger",
|
||||
"metrics.visitors": "Besøkende",
|
||||
"title.add-account": "Legg til konto",
|
||||
"title.add-website": "Legg til nettsted",
|
||||
"title.change-password": "Bytt passord",
|
||||
"title.delete-account": "Slett konto",
|
||||
"title.delete-website": "Slett nettstedet",
|
||||
"title.edit-account": "Rediger konto",
|
||||
"title.edit-website": "Rediger nettsted",
|
||||
"title.share-url": "Del URL",
|
||||
"title.tracking-code": "Sporingskode"
|
||||
"metrics.visitors": "Besøkende"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Account toevoegen",
|
||||
"button.add-website": "Website toevoegen",
|
||||
"button.back": "Terug",
|
||||
"button.cancel": "Annuleren",
|
||||
"button.change-password": "Wachtwoord wijzigen",
|
||||
"button.copy-to-clipboard": "Kopiëer naar klembord",
|
||||
"button.date-range": "Datumbereik",
|
||||
"button.delete": "Verwijderen",
|
||||
"button.dismiss": "Negeren",
|
||||
"button.edit": "Bewerken",
|
||||
"button.login": "Inloggen",
|
||||
"button.more": "Toon meer",
|
||||
"button.refresh": "Vernieuwen",
|
||||
"button.reset": "Resetten",
|
||||
"button.save": "Opslaan",
|
||||
"button.single-day": "Enkele dag",
|
||||
"button.view-details": "Meer details",
|
||||
"label.accounts": "Gebruikers",
|
||||
"label.add-account": "Account toevoegen",
|
||||
"label.add-website": "Website toevoegen",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Terug",
|
||||
"label.cancel": "Annuleren",
|
||||
"label.change-password": "Wachtwoord wijzigen",
|
||||
"label.confirm-password": "Wachtwoord bevestigen",
|
||||
"label.copy-to-clipboard": "Kopiëer naar klembord",
|
||||
"label.current-password": "Huidig wachtwoord",
|
||||
"label.custom-range": "Aangepast bereik",
|
||||
"label.dashboard": "Overzicht",
|
||||
"label.date-range": "Datumbereik",
|
||||
"label.default-date-range": "Standaard bereik",
|
||||
"label.delete": "Verwijderen",
|
||||
"label.delete-account": "Account verwijderen",
|
||||
"label.delete-website": "Website verwijderen",
|
||||
"label.dismiss": "Negeren",
|
||||
"label.domain": "Domein",
|
||||
"label.edit": "Bewerken",
|
||||
"label.edit-account": "Account bewerken",
|
||||
"label.edit-website": "Website bewerken",
|
||||
"label.enable-share-url": "Sta delen via openbare URL toe",
|
||||
"label.invalid": "Ongeldig",
|
||||
"label.invalid-domain": "Ongeldig domein",
|
||||
"label.last-days": "Laatste {x} dagen",
|
||||
"label.last-hours": "Laatste {x} uur",
|
||||
"label.logged-in-as": "Ingelogd als {username}",
|
||||
"label.login": "Inloggen",
|
||||
"label.logout": "Uitloggen",
|
||||
"label.more": "Toon meer",
|
||||
"label.name": "Naam",
|
||||
"label.new-password": "Nieuw wachtwoord",
|
||||
"label.password": "Wachtwoord",
|
||||
"label.passwords-dont-match": "Wachtwoorden komen niet overeen",
|
||||
"label.profile": "Profiel",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Vernieuwen",
|
||||
"label.required": "Verplicht",
|
||||
"label.reset": "Resetten",
|
||||
"label.save": "Opslaan",
|
||||
"label.settings": "Instellingen",
|
||||
"label.share-url": "URL delen",
|
||||
"label.single-day": "Enkele dag",
|
||||
"label.this-month": "Deze maand",
|
||||
"label.this-week": "Deze week",
|
||||
"label.this-year": "Dit jaar",
|
||||
"label.timezone": "Tijdzone",
|
||||
"label.today": "Vandaag",
|
||||
"label.tracking-code": "Tracking code",
|
||||
"label.unknown": "Onbekend",
|
||||
"label.username": "Gebruikersnaam",
|
||||
"label.view-details": "Meer details",
|
||||
"label.websites": "Websites",
|
||||
"message.active-users": "{x} actieve {x, plural, one {bezoeker} other {bezoekers}}",
|
||||
"message.confirm-delete": "Weet je zeker dat je {target} wilt verwijderen?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "Tracking code",
|
||||
"message.go-to-settings": "Naar instellingen",
|
||||
"message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "Een nieuwe versie van umami {version} is beschikbaar!",
|
||||
"message.no-data-available": "Geen gegevens beschikbaar.",
|
||||
"message.no-websites-configured": "Je hebt geen websites ingesteld.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Verwijzers",
|
||||
"metrics.unique-visitors": "Unieke bezoekers",
|
||||
"metrics.views": "Weergaven",
|
||||
"metrics.visitors": "Bezoekers",
|
||||
"title.add-account": "Account toevoegen",
|
||||
"title.add-website": "Website toevoegen",
|
||||
"title.change-password": "Wachtwoord wijzigen",
|
||||
"title.delete-account": "Account verwijderen",
|
||||
"title.delete-website": "Website verwijderen",
|
||||
"title.edit-account": "Account bewerken",
|
||||
"title.edit-website": "Website bewerken",
|
||||
"title.share-url": "URL delen",
|
||||
"title.tracking-code": "Tracking code"
|
||||
"metrics.visitors": "Bezoekers"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Adicionar conta",
|
||||
"button.add-website": "Adicionar website",
|
||||
"button.back": "Voltar",
|
||||
"button.cancel": "Cancelar",
|
||||
"button.change-password": "Alterar palavra-passe",
|
||||
"button.copy-to-clipboard": "Copiar para a área de transferência",
|
||||
"button.date-range": "Intervalo de datas",
|
||||
"button.delete": "Eliminar",
|
||||
"button.dismiss": "Ignorar",
|
||||
"button.edit": "Editar",
|
||||
"button.login": "Iniciar sessão",
|
||||
"button.more": "Mais",
|
||||
"button.refresh": "Atualizar",
|
||||
"button.reset": "Repor",
|
||||
"button.save": "Guardar",
|
||||
"button.single-day": "Dia único",
|
||||
"button.view-details": "Ver detalhes",
|
||||
"label.accounts": "Contas",
|
||||
"label.add-account": "Adicionar conta",
|
||||
"label.add-website": "Adicionar website",
|
||||
"label.administrator": "Administrador",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Voltar",
|
||||
"label.cancel": "Cancelar",
|
||||
"label.change-password": "Alterar palavra-passe",
|
||||
"label.confirm-password": "Confirmar palavra-passe",
|
||||
"label.copy-to-clipboard": "Copiar para a área de transferência",
|
||||
"label.current-password": "Palavra-passe atual",
|
||||
"label.custom-range": "Intervalo personalizado",
|
||||
"label.dashboard": "Dashboard",
|
||||
"label.date-range": "Intervalo de datas",
|
||||
"label.default-date-range": "Intervalo de datas predefinido",
|
||||
"label.delete": "Eliminar",
|
||||
"label.delete-account": "Eliminar conta",
|
||||
"label.delete-website": "Eliminar website",
|
||||
"label.dismiss": "Ignorar",
|
||||
"label.domain": "Domínio",
|
||||
"label.edit": "Editar",
|
||||
"label.edit-account": "Editar conta",
|
||||
"label.edit-website": "Editar website",
|
||||
"label.enable-share-url": "Ativar link de partilha",
|
||||
"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 palavra-passe",
|
||||
"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.refresh": "Atualizar",
|
||||
"label.required": "Obrigatório",
|
||||
"label.reset": "Repor",
|
||||
"label.save": "Guardar",
|
||||
"label.settings": "Definições",
|
||||
"label.share-url": "Partilhar link",
|
||||
"label.single-day": "Dia único",
|
||||
"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 tracking",
|
||||
"label.unknown": "Desconhecido",
|
||||
"label.username": "Nome de utilizador",
|
||||
"label.view-details": "Ver detalhes",
|
||||
"label.websites": "Websites",
|
||||
"message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento",
|
||||
"message.confirm-delete": "Tens a certeza que queres eliminar {target}?",
|
||||
@ -55,6 +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.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.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Referrers",
|
||||
"metrics.unique-visitors": "Visitantes únicos",
|
||||
"metrics.views": "Visualizações",
|
||||
"metrics.visitors": "Visitantes",
|
||||
"title.add-account": "Adicionar conta",
|
||||
"title.add-website": "Adicionar website",
|
||||
"title.change-password": "Alterar palavra-passe",
|
||||
"title.delete-account": "Eliminar conta",
|
||||
"title.delete-website": "Eliminar website",
|
||||
"title.edit-account": "Editar conta",
|
||||
"title.edit-website": "Editar website",
|
||||
"title.share-url": "Partilhar link",
|
||||
"title.tracking-code": "Código de tracking"
|
||||
"metrics.visitors": "Visitantes"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Adaugă cont",
|
||||
"button.add-website": "Adaugă site web",
|
||||
"button.back": "Înapoi",
|
||||
"button.cancel": "Anulează",
|
||||
"button.change-password": "Schimbă parola",
|
||||
"button.copy-to-clipboard": "Copiază în clipboard",
|
||||
"button.date-range": "Interval de date",
|
||||
"button.delete": "Șterge",
|
||||
"button.dismiss": "Renunță",
|
||||
"button.edit": "Editare",
|
||||
"button.login": "Autentificare",
|
||||
"button.more": "Mai mult",
|
||||
"button.refresh": "Reîmprospătare",
|
||||
"button.reset": "Resetează",
|
||||
"button.save": "Salvează",
|
||||
"button.single-day": "O singură zi",
|
||||
"button.view-details": "Vizualizare detalii",
|
||||
"label.accounts": "Conturi",
|
||||
"label.add-account": "Adăugare cont",
|
||||
"label.add-website": "Adăugare site web",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Înapoi",
|
||||
"label.cancel": "Anulează",
|
||||
"label.change-password": "Schimbare parolă",
|
||||
"label.confirm-password": "Confirmare parolă",
|
||||
"label.copy-to-clipboard": "Copiază în clipboard",
|
||||
"label.current-password": "Parola curentă",
|
||||
"label.custom-range": "Interval personalizat",
|
||||
"label.dashboard": "Tablou de bord",
|
||||
"label.date-range": "Interval de date",
|
||||
"label.default-date-range": "Interval de date implicit",
|
||||
"label.delete": "Șterge",
|
||||
"label.delete-account": "Ștergere cont",
|
||||
"label.delete-website": "Ștergere site web",
|
||||
"label.dismiss": "Renunță",
|
||||
"label.domain": "Domeniu",
|
||||
"label.edit": "Editare",
|
||||
"label.edit-account": "Editare cont",
|
||||
"label.edit-website": "Editare site web",
|
||||
"label.enable-share-url": "Activare adresa URL de distribuire",
|
||||
"label.invalid": "Invalid",
|
||||
"label.invalid-domain": "Invalid domain",
|
||||
"label.last-days": "Ultimele {x} zile",
|
||||
"label.last-hours": "Ultimele {x} ore",
|
||||
"label.logged-in-as": "Autentificat ca {username}",
|
||||
"label.login": "Autentificare",
|
||||
"label.logout": "Dezautentificare",
|
||||
"label.more": "Mai mult",
|
||||
"label.name": "Nume",
|
||||
"label.new-password": "Parola nouă",
|
||||
"label.password": "Parolă",
|
||||
"label.passwords-dont-match": "Parolele nu se potrivesc",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Reîmprospătare",
|
||||
"label.required": "Obligatoriu",
|
||||
"label.reset": "Resetează",
|
||||
"label.save": "Salvează",
|
||||
"label.settings": "Setări",
|
||||
"label.share-url": "Partajare URL",
|
||||
"label.single-day": "O singură zi",
|
||||
"label.this-month": "Această lună",
|
||||
"label.this-week": "Această săptămână",
|
||||
"label.this-year": "Acest an",
|
||||
"label.timezone": "Fus orar",
|
||||
"label.today": "Astăzi",
|
||||
"label.tracking-code": "Cod de urmărire",
|
||||
"label.unknown": "Necunoscut",
|
||||
"label.username": "Username",
|
||||
"label.view-details": "Vizualizare detalii",
|
||||
"label.websites": "Site-uri web",
|
||||
"message.active-users": "{x} {x, plural, one {vizitator activ} other {vizitatori activi}}",
|
||||
"message.confirm-delete": "Sunteți sigur că doriți să ștergeți {target}?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "Obține codul de urmărire",
|
||||
"message.go-to-settings": "Mergi la Setări",
|
||||
"message.incorrect-username-password": "Username/parolă incorecte.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "Este disponibilă o nouă versiune {version} de umami!",
|
||||
"message.no-data-available": "Nicio informație disponibilă.",
|
||||
"message.no-websites-configured": "Nu aveți niciun site web configurat.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Site-uri de proveniență",
|
||||
"metrics.unique-visitors": "Vizitatori unici",
|
||||
"metrics.views": "Vizualizări",
|
||||
"metrics.visitors": "Vizitatori",
|
||||
"title.add-account": "Adăugare cont",
|
||||
"title.add-website": "Adăugare site web",
|
||||
"title.change-password": "Schimbare parolă",
|
||||
"title.delete-account": "Ștergere cont",
|
||||
"title.delete-website": "Ștergere site web",
|
||||
"title.edit-account": "Editare cont",
|
||||
"title.edit-website": "Editare site web",
|
||||
"title.share-url": "Partajare URL",
|
||||
"title.tracking-code": "Cod de urmărire"
|
||||
"metrics.visitors": "Vizitatori"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Добавить аккаунт",
|
||||
"button.add-website": "Добавить сайт",
|
||||
"button.back": "Назад",
|
||||
"button.cancel": "Отменить",
|
||||
"button.change-password": "Изменить пароль",
|
||||
"button.copy-to-clipboard": "Скопировать в буфер обмена",
|
||||
"button.date-range": "Диапазон дат",
|
||||
"button.delete": "Удалить",
|
||||
"button.dismiss": "Отклонить",
|
||||
"button.edit": "Редактировать",
|
||||
"button.login": "Войти",
|
||||
"button.more": "Больше",
|
||||
"button.refresh": "Обновить",
|
||||
"button.reset": "Сбросить",
|
||||
"button.save": "Сохранить",
|
||||
"button.single-day": "Один день",
|
||||
"button.view-details": "Посмотреть детали",
|
||||
"label.accounts": "Аккаунты",
|
||||
"label.add-account": "Добавить аккаунт",
|
||||
"label.add-website": "Добавить сайт",
|
||||
"label.administrator": "Администратор",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "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": "Realtime",
|
||||
"label.realtime-logs": "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} текущих посетителей",
|
||||
"message.confirm-delete": "Вы уверены, что хотите удалить {target}?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "Получить код отслеживания",
|
||||
"message.go-to-settings": "Перейти к настройкам",
|
||||
"message.incorrect-username-password": "Неверное имя пользователя/пароль.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "Доступна новая версия umami {version}",
|
||||
"message.no-data-available": "Нет данных.",
|
||||
"message.no-websites-configured": "У вас нет настроенных сайтов.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Источники",
|
||||
"metrics.unique-visitors": "Уникальные посетители",
|
||||
"metrics.views": "Просмотры",
|
||||
"metrics.visitors": "Посетители",
|
||||
"title.add-account": "Добавить аккаунт",
|
||||
"title.add-website": "Добавить сайт",
|
||||
"title.change-password": "Изменить пароль",
|
||||
"title.delete-account": "Удалить аккаунт",
|
||||
"title.delete-website": "Удалить сайт",
|
||||
"title.edit-account": "Редактировать аккаунт",
|
||||
"title.edit-website": "Редактировать сайт",
|
||||
"title.share-url": "Поделиться ссылкой",
|
||||
"title.tracking-code": "Код отслеживания"
|
||||
"metrics.visitors": "Посетители"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Lägg till konto",
|
||||
"button.add-website": "Lägg till webbsajt",
|
||||
"button.back": "Tillbaka",
|
||||
"button.cancel": "Avbryt",
|
||||
"button.change-password": "Byt lösenord",
|
||||
"button.copy-to-clipboard": "Kopiera till urklipp",
|
||||
"button.date-range": "Datumomfång",
|
||||
"button.delete": "Radera",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Redigera",
|
||||
"button.login": "Logga in",
|
||||
"button.more": "Mer",
|
||||
"button.refresh": "Uppdatera",
|
||||
"button.reset": "Återställ",
|
||||
"button.save": "Spara",
|
||||
"button.single-day": "En dag",
|
||||
"button.view-details": "Visa detaljer",
|
||||
"label.accounts": "Konton",
|
||||
"label.add-account": "Lägg till konto",
|
||||
"label.add-website": "Lägg till webbsajt",
|
||||
"label.administrator": "Administratör",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Tillbaka",
|
||||
"label.cancel": "Avbryt",
|
||||
"label.change-password": "Byt lösenord",
|
||||
"label.confirm-password": "Bekräfta lösenord",
|
||||
"label.copy-to-clipboard": "Kopiera till urklipp",
|
||||
"label.current-password": "Nuvarande lösenord",
|
||||
"label.custom-range": "Anpassat urval",
|
||||
"label.dashboard": "Instrumentpanel",
|
||||
"label.date-range": "Datumomfång",
|
||||
"label.default-date-range": "Standard datum-urval",
|
||||
"label.delete": "Radera",
|
||||
"label.delete-account": "Radera konto",
|
||||
"label.delete-website": "Radera webbsajt",
|
||||
"label.dismiss": "Dismiss",
|
||||
"label.domain": "Domän",
|
||||
"label.edit": "Redigera",
|
||||
"label.edit-account": "Redigera konto",
|
||||
"label.edit-website": "Redigera webbsajt",
|
||||
"label.enable-share-url": "Aktivera delnings-URL",
|
||||
"label.invalid": "Ogiltig",
|
||||
"label.invalid-domain": "Ogiltig domän",
|
||||
"label.last-days": "Senaste {x} dagarna",
|
||||
"label.last-hours": "Senaste {x} timmarna",
|
||||
"label.logged-in-as": "Inloggad som {username}",
|
||||
"label.login": "Logga in",
|
||||
"label.logout": "Logga ut",
|
||||
"label.more": "Mer",
|
||||
"label.name": "Namn",
|
||||
"label.new-password": "Nytt lösenord",
|
||||
"label.password": "Lösenord",
|
||||
"label.passwords-dont-match": "Lösenorden är inte samma",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Uppdatera",
|
||||
"label.required": "Krävs",
|
||||
"label.reset": "Återställ",
|
||||
"label.save": "Spara",
|
||||
"label.settings": "Inställningar",
|
||||
"label.share-url": "Delnings-URL",
|
||||
"label.single-day": "En dag",
|
||||
"label.this-month": "Denna månad",
|
||||
"label.this-week": "Denna vecka",
|
||||
"label.this-year": "Detta år",
|
||||
"label.timezone": "Tidszon",
|
||||
"label.today": "Idag",
|
||||
"label.tracking-code": "Spårningskod",
|
||||
"label.unknown": "Okänd",
|
||||
"label.username": "Användarnamn",
|
||||
"label.view-details": "Visa detaljer",
|
||||
"label.websites": "Webbsajt",
|
||||
"message.active-users": "{x} {x, plural, one {besökare} other {besökare}} just nu",
|
||||
"message.confirm-delete": "Är du säker på att du vill radera {target}?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "Visa spårningskod",
|
||||
"message.go-to-settings": "Gå till inställningar",
|
||||
"message.incorrect-username-password": "Felaktikt användarnamn/lösenord.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.no-data-available": "Ingen data tillgänglig.",
|
||||
"message.no-websites-configured": "Du har inga webbsajter.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Hänvisare",
|
||||
"metrics.unique-visitors": "Unika besökare",
|
||||
"metrics.views": "Visningar",
|
||||
"metrics.visitors": "Besökare",
|
||||
"title.add-account": "Lägg till konto",
|
||||
"title.add-website": "Lägg till webbsajt",
|
||||
"title.change-password": "Byt lösenord",
|
||||
"title.delete-account": "Radera konto",
|
||||
"title.delete-website": "Radera webbsajt",
|
||||
"title.edit-account": "Redigera konto",
|
||||
"title.edit-website": "Redigera webbsajt",
|
||||
"title.share-url": "Delnings-URL",
|
||||
"title.tracking-code": "Spårningskod"
|
||||
"metrics.visitors": "Besökare"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Yeni Hesap Ekle",
|
||||
"button.add-website": "Web sitesi ekle",
|
||||
"button.back": "Geri",
|
||||
"button.cancel": "İptal",
|
||||
"button.change-password": "Şifre değiştir",
|
||||
"button.copy-to-clipboard": "Panoya kopyala",
|
||||
"button.date-range": "Tarih aralığı",
|
||||
"button.delete": "Sil",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "Düzenle",
|
||||
"button.login": "Giriş Yap",
|
||||
"button.more": "Detaylı göster",
|
||||
"button.refresh": "Yenile",
|
||||
"button.reset": "Sıfırla",
|
||||
"button.save": "Kaydet",
|
||||
"button.single-day": "Tekil gün",
|
||||
"button.view-details": "Detayı incele",
|
||||
"label.accounts": "Hesaplar",
|
||||
"label.add-account": "Hesap ekle",
|
||||
"label.add-website": "Web sitesi ekle",
|
||||
"label.administrator": "Yönetici",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "All websites",
|
||||
"label.back": "Geri",
|
||||
"label.cancel": "İptal",
|
||||
"label.change-password": "Şifre değiştir",
|
||||
"label.confirm-password": "Parolayı onayla",
|
||||
"label.copy-to-clipboard": "Panoya kopyala",
|
||||
"label.current-password": "Mevcut parola",
|
||||
"label.custom-range": "Özelleştirilmiş aralık",
|
||||
"label.dashboard": "Kontrol Paneli",
|
||||
"label.date-range": "Tarih aralığı",
|
||||
"label.default-date-range": "Varsayılan tarih aralığı",
|
||||
"label.delete": "Sil",
|
||||
"label.delete-account": "Hesabı sil",
|
||||
"label.delete-website": "Web sitesini sil",
|
||||
"label.dismiss": "Dismiss",
|
||||
"label.domain": "Alan adı",
|
||||
"label.edit": "Düzenle",
|
||||
"label.edit-account": "Hesabı düzenle",
|
||||
"label.edit-website": "Web sitesini düzenle",
|
||||
"label.enable-share-url": "Anonim paylaşım URL'i aktif",
|
||||
"label.invalid": "Geçeriz",
|
||||
"label.invalid-domain": "Geçersiz alan adı",
|
||||
"label.last-days": "Son {x} gün",
|
||||
"label.last-hours": "Son {x} saat",
|
||||
"label.logged-in-as": "{username} olarak giriş yapıldı.",
|
||||
"label.login": "Giriş Yap",
|
||||
"label.logout": "Çıkış Yap",
|
||||
"label.more": "Detaylı göster",
|
||||
"label.name": "İsim",
|
||||
"label.new-password": "Yeni parola",
|
||||
"label.password": "Parola",
|
||||
"label.passwords-dont-match": "Parolalar uyuşmuyor",
|
||||
"label.profile": "Profil",
|
||||
"label.realtime": "Realtime",
|
||||
"label.realtime-logs": "Realtime logs",
|
||||
"label.refresh": "Yenile",
|
||||
"label.required": "Zorunlu alan",
|
||||
"label.reset": "Sıfırla",
|
||||
"label.save": "Kaydet",
|
||||
"label.settings": "Ayarlar",
|
||||
"label.share-url": "Paylaşım adresi",
|
||||
"label.single-day": "Tekil gün",
|
||||
"label.this-month": "Bu ay",
|
||||
"label.this-week": "Bu hafta",
|
||||
"label.this-year": "Bu yıl",
|
||||
"label.timezone": "Zaman dilimi",
|
||||
"label.today": "Bugün",
|
||||
"label.tracking-code": "İzleme kodu",
|
||||
"label.unknown": "Bilinmeyen",
|
||||
"label.username": "Kullanıcı adı",
|
||||
"label.view-details": "Detayı incele",
|
||||
"label.websites": "Web siteleri",
|
||||
"message.active-users": "{x} aktif ziyaretçi",
|
||||
"message.confirm-delete": "{target} kaydını silmek istediğinizden emin misiniz?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "İzleme kodunu al",
|
||||
"message.go-to-settings": "Ayarlara git",
|
||||
"message.incorrect-username-password": "Hatalı kullanıcı adı ya da parola.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.no-data-available": "Henüz hiç veri yok.",
|
||||
"message.no-websites-configured": "Henüz hiç web sitesi tanımlamadınız",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Yönlendirenler",
|
||||
"metrics.unique-visitors": "Tekil kullanıcı",
|
||||
"metrics.views": "Görüntüleme",
|
||||
"metrics.visitors": "Ziyaretçi",
|
||||
"title.add-account": "Hesap ekle",
|
||||
"title.add-website": "Web sitesi ekle",
|
||||
"title.change-password": "Şifre değiştir",
|
||||
"title.delete-account": "Hesabı sil",
|
||||
"title.delete-website": "Web sitesini sil",
|
||||
"title.edit-account": "Hesabı düzenle",
|
||||
"title.edit-website": "Web sitesini düzenle",
|
||||
"title.share-url": "Paylaşım adresi",
|
||||
"title.tracking-code": "İzleme kodu"
|
||||
"metrics.visitors": "Ziyaretçi"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "Додати обліковий запис",
|
||||
"button.add-website": "Додати веб-сайт",
|
||||
"button.back": "Назад",
|
||||
"button.cancel": "Відмінити",
|
||||
"button.change-password": "Змінити пароль",
|
||||
"button.copy-to-clipboard": "Копіювати до буферу обміну",
|
||||
"button.date-range": "Діапазон дат",
|
||||
"button.delete": "Видалити",
|
||||
"button.dismiss": "Відхилити",
|
||||
"button.edit": "Редагувати",
|
||||
"button.login": "Увійти",
|
||||
"button.more": "Більше",
|
||||
"button.refresh": "Оновити",
|
||||
"button.reset": "Скинути",
|
||||
"button.save": "Зберегти",
|
||||
"button.single-day": "Один день",
|
||||
"button.view-details": "Переглянути деталі",
|
||||
"label.accounts": "Облікові записи",
|
||||
"label.add-account": "Додати обліковий запис",
|
||||
"label.add-website": "Додати website",
|
||||
"label.administrator": "Адміністратор",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "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": "Realtime",
|
||||
"label.realtime-logs": "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} поточних відвідувачів",
|
||||
"message.confirm-delete": "Ви впевнені, що бажаєте видалити {target}?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "Отримати код для відслідковування",
|
||||
"message.go-to-settings": "Перейти до налаштувань",
|
||||
"message.incorrect-username-password": "Невірне ім'я користувача або пароль.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "Нова версія umami {version} доступна!",
|
||||
"message.no-data-available": "Немає даних.",
|
||||
"message.no-websites-configured": "У вас немає налаштованих веб-сайтів.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "Джерела",
|
||||
"metrics.unique-visitors": "Унікальні відвідувачі",
|
||||
"metrics.views": "Перегляди",
|
||||
"metrics.visitors": "Відвідувачі",
|
||||
"title.add-account": "Додати обліковий запис",
|
||||
"title.add-website": "Додати website",
|
||||
"title.change-password": "Змінити пароль",
|
||||
"title.delete-account": "Видалити обліковий запис",
|
||||
"title.delete-website": "Видалити веб-сайт",
|
||||
"title.edit-account": "Редагувати обліковий запис",
|
||||
"title.edit-website": "Редагувати веб-сайт",
|
||||
"title.share-url": "Поділитися посилання",
|
||||
"title.tracking-code": "Код для відслідковування"
|
||||
"metrics.visitors": "Відвідувачі"
|
||||
}
|
||||
|
@ -1,50 +1,60 @@
|
||||
{
|
||||
"button.add-account": "添加账户",
|
||||
"button.add-website": "添加网站",
|
||||
"button.back": "返回",
|
||||
"button.cancel": "取消",
|
||||
"button.change-password": "更新密码",
|
||||
"button.copy-to-clipboard": "复制",
|
||||
"button.date-range": "多日",
|
||||
"button.delete": "删除",
|
||||
"button.dismiss": "Dismiss",
|
||||
"button.edit": "编辑",
|
||||
"button.login": "登录",
|
||||
"button.more": "更多",
|
||||
"button.refresh": "刷新",
|
||||
"button.reset": "重置",
|
||||
"button.save": "保存",
|
||||
"button.single-day": "单日",
|
||||
"button.view-details": "查看更多",
|
||||
"label.accounts": "账户",
|
||||
"label.add-account": "添加账户",
|
||||
"label.add-website": "添加网站",
|
||||
"label.administrator": "管理员",
|
||||
"label.all": "All",
|
||||
"label.all-websites": "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": "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": "Realtime",
|
||||
"label.realtime-logs": "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} 人",
|
||||
"message.confirm-delete": "你确定要删除{target}吗?",
|
||||
@ -55,6 +65,7 @@
|
||||
"message.get-tracking-code": "获得跟踪代码",
|
||||
"message.go-to-settings": "去设置",
|
||||
"message.incorrect-username-password": "用户名密码不正确.",
|
||||
"message.log.visitor": "Visitor from {country} using {browser} on {os} {device}",
|
||||
"message.new-version-available": "A new version of umami {version} is available!",
|
||||
"message.no-data-available": "无可用数据.",
|
||||
"message.no-websites-configured": "你还没有设置任何网站.",
|
||||
@ -84,14 +95,5 @@
|
||||
"metrics.referrers": "指入域名",
|
||||
"metrics.unique-visitors": "独立访客",
|
||||
"metrics.views": "页面流量",
|
||||
"metrics.visitors": "独立访客",
|
||||
"title.add-account": "添加账户",
|
||||
"title.add-website": "添加网站",
|
||||
"title.change-password": "更新密码",
|
||||
"title.delete-account": "删除账户",
|
||||
"title.delete-website": "删除网站",
|
||||
"title.edit-account": "编辑账户",
|
||||
"title.edit-website": "编辑网站",
|
||||
"title.share-url": "共享链接",
|
||||
"title.tracking-code": "跟踪代码"
|
||||
"metrics.visitors": "独立访客"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { parse } from 'cookie';
|
||||
import { parseSecureToken, parseToken } from './crypto';
|
||||
import { AUTH_COOKIE_NAME } from './constants';
|
||||
import { AUTH_COOKIE_NAME, TOKEN_HEADER } from './constants';
|
||||
import { getWebsiteById } from './queries';
|
||||
|
||||
export async function getAuthToken(req) {
|
||||
@ -26,13 +26,14 @@ export async function isValidToken(token, validation) {
|
||||
}
|
||||
|
||||
export async function allowQuery(req, skipToken) {
|
||||
const { id, token } = req.query;
|
||||
const { id } = req.query;
|
||||
const token = req.headers[TOKEN_HEADER];
|
||||
const websiteId = +id;
|
||||
|
||||
const website = await getWebsiteById(websiteId);
|
||||
|
||||
if (website) {
|
||||
if (token && !skipToken) {
|
||||
if (token && token !== 'undefined' && !skipToken) {
|
||||
return isValidToken(token, { website_id: websiteId });
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,16 @@ export const TIMEZONE_CONFIG = 'umami.timezone';
|
||||
export const DATE_RANGE_CONFIG = 'umami.date-range';
|
||||
export const THEME_CONFIG = 'umami.theme';
|
||||
export const VERSION_CHECK = 'umami.version-check';
|
||||
export const TOKEN_HEADER = 'x-umami-token';
|
||||
|
||||
export const DEFAULT_LOCALE = 'en-US';
|
||||
export const DEFAULT_THEME = 'light';
|
||||
export const DEFAUL_CHART_HEIGHT = 400;
|
||||
export const DEFAULT_ANIMATION_DURATION = 300;
|
||||
export const DEFAULT_DATE_RANGE = '24hour';
|
||||
|
||||
export const REALTIME_RANGE = 30;
|
||||
export const REALTIME_INTERVAL = 3000;
|
||||
|
||||
export const THEME_COLORS = {
|
||||
light: {
|
||||
@ -42,13 +52,15 @@ export const EVENT_COLORS = [
|
||||
'#44b556',
|
||||
'#e68619',
|
||||
'#e34850',
|
||||
'#1b959a',
|
||||
'#d83790',
|
||||
'#85d044',
|
||||
'#f7bd12',
|
||||
'#01bad7',
|
||||
'#6734bc',
|
||||
'#89c541',
|
||||
'#ffc301',
|
||||
'#ec1562',
|
||||
'#ffec16',
|
||||
];
|
||||
|
||||
export const DEFAULT_DATE_RANGE = '24hour';
|
||||
|
||||
export const POSTGRESQL = 'postgresql';
|
||||
export const MYSQL = 'mysql';
|
||||
|
||||
@ -68,10 +80,6 @@ export const POSTGRESQL_DATE_FORMATS = {
|
||||
year: 'YYYY-01-01',
|
||||
};
|
||||
|
||||
export const FILTER_DOMAIN_ONLY = 0;
|
||||
export const FILTER_COMBINED = 1;
|
||||
export const FILTER_RAW = 2;
|
||||
|
||||
export const DOMAIN_REGEX = /localhost(:\d{1,5})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}/;
|
||||
|
||||
export const DESKTOP_SCREEN_WIDTH = 1920;
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
addYears,
|
||||
subHours,
|
||||
subDays,
|
||||
startOfMinute,
|
||||
startOfHour,
|
||||
startOfDay,
|
||||
startOfWeek,
|
||||
@ -17,6 +18,7 @@ import {
|
||||
endOfWeek,
|
||||
endOfMonth,
|
||||
endOfYear,
|
||||
differenceInMinutes,
|
||||
differenceInHours,
|
||||
differenceInCalendarDays,
|
||||
differenceInCalendarMonths,
|
||||
@ -114,6 +116,7 @@ export function getDateFromString(str) {
|
||||
}
|
||||
|
||||
const dateFuncs = {
|
||||
minute: [differenceInMinutes, addMinutes, startOfMinute],
|
||||
hour: [differenceInHours, addHours, startOfHour],
|
||||
day: [differenceInCalendarDays, addDays, startOfDay],
|
||||
month: [differenceInCalendarMonths, addMonths, startOfMonth],
|
||||
|
@ -62,3 +62,19 @@ export function formatLongNumber(value) {
|
||||
|
||||
return formatNumber(n);
|
||||
}
|
||||
|
||||
export function stringToColor(str) {
|
||||
if (!str) {
|
||||
return '#ffffff';
|
||||
}
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
let color = '#';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
let value = (hash >> (i * 8)) & 0xff;
|
||||
color += ('00' + value.toString(16)).substr(-2);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
nb,
|
||||
id,
|
||||
uk,
|
||||
fi,
|
||||
} from 'date-fns/locale';
|
||||
import enMessages from 'lang-compiled/en-US.json';
|
||||
import nlMessages from 'lang-compiled/nl-NL.json';
|
||||
@ -37,6 +38,7 @@ 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';
|
||||
|
||||
export const messages = {
|
||||
'en-US': enMessages,
|
||||
@ -58,6 +60,7 @@ export const messages = {
|
||||
'nb-NO': nbNOMessages,
|
||||
'id-ID': idMessages,
|
||||
'uk-UA': ukMessages,
|
||||
'fi-FI': fiMessages,
|
||||
};
|
||||
|
||||
export const dateLocales = {
|
||||
@ -80,6 +83,7 @@ export const dateLocales = {
|
||||
'nb-NO': nb,
|
||||
'id-ID': id,
|
||||
'uk-UA': uk,
|
||||
'fi-FI': fi,
|
||||
};
|
||||
|
||||
export const menuOptions = [
|
||||
@ -99,6 +103,7 @@ export const menuOptions = [
|
||||
{ label: 'Português', value: 'pt-PT', display: 'pt' },
|
||||
{ label: 'Русский', value: 'ru-RU', display: 'ru' },
|
||||
{ label: 'Română', value: 'ro-RO', display: 'ro' },
|
||||
{ label: 'Suomi', value: 'fi-FI', display: 'fi' },
|
||||
{ label: 'Svenska', value: 'sv-SE', display: 'sv' },
|
||||
{ label: 'Türkçe', value: 'tr-TR', display: 'tr' },
|
||||
{ label: 'українська', value: 'uk-UA', display: 'uk' },
|
||||
|
@ -166,16 +166,6 @@ export async function createSession(website_id, data) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function getSessionById(session_id) {
|
||||
return runQuery(
|
||||
prisma.session.findOne({
|
||||
where: {
|
||||
session_id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getSessionByUuid(session_uuid) {
|
||||
return runQuery(
|
||||
prisma.session.findOne({
|
||||
@ -285,7 +275,58 @@ export async function createAccount(data) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getMetrics(website_id, start_at, end_at, filters = {}) {
|
||||
export async function getSessions(websites, start_at) {
|
||||
return runQuery(
|
||||
prisma.session.findMany({
|
||||
where: {
|
||||
website: {
|
||||
website_id: {
|
||||
in: websites,
|
||||
},
|
||||
},
|
||||
created_at: {
|
||||
gte: start_at,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPageviews(websites, start_at) {
|
||||
return runQuery(
|
||||
prisma.pageview.findMany({
|
||||
where: {
|
||||
website: {
|
||||
website_id: {
|
||||
in: websites,
|
||||
},
|
||||
},
|
||||
created_at: {
|
||||
gte: start_at,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getEvents(websites, start_at) {
|
||||
return runQuery(
|
||||
prisma.event.findMany({
|
||||
where: {
|
||||
website: {
|
||||
website_id: {
|
||||
in: websites,
|
||||
},
|
||||
},
|
||||
created_at: {
|
||||
gte: start_at,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function getWebsiteStats(website_id, start_at, end_at, filters = {}) {
|
||||
const params = [website_id, start_at, end_at];
|
||||
const { url } = filters;
|
||||
let urlFilter = '';
|
||||
@ -317,7 +358,7 @@ export function getMetrics(website_id, start_at, end_at, filters = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getPageviews(
|
||||
export function getPageviewStats(
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
@ -425,7 +466,7 @@ export function getActiveVisitors(website_id) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getEvents(
|
||||
export function getEventMetrics(
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
@ -459,3 +500,30 @@ export function getEvents(
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getRealtimeData(websites, time) {
|
||||
const [pageviews, sessions, events] = await Promise.all([
|
||||
getPageviews(websites, time),
|
||||
getSessions(websites, time),
|
||||
getEvents(websites, time),
|
||||
]);
|
||||
|
||||
return {
|
||||
pageviews: pageviews.map(({ view_id, ...props }) => ({
|
||||
__id: `p${view_id}`,
|
||||
view_id,
|
||||
...props,
|
||||
})),
|
||||
sessions: sessions.map(({ session_id, ...props }) => ({
|
||||
__id: `s${session_id}`,
|
||||
session_id,
|
||||
...props,
|
||||
})),
|
||||
events: events.map(({ event_id, ...props }) => ({
|
||||
__id: `e${event_id}`,
|
||||
event_id,
|
||||
...props,
|
||||
})),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
@ -28,3 +28,7 @@ export function getQueryString(params = {}) {
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export function makeUrl(url, params) {
|
||||
return `${url}${getQueryString(params)}`;
|
||||
}
|
||||
|
14
lib/web.js
14
lib/web.js
@ -1,4 +1,4 @@
|
||||
import { getQueryString } from './url';
|
||||
import { makeUrl } from './url';
|
||||
|
||||
export const apiRequest = (method, url, body, headers) =>
|
||||
fetch(url, {
|
||||
@ -19,13 +19,17 @@ export const apiRequest = (method, url, body, headers) =>
|
||||
return res.text().then(data => ({ ok: res.ok, status: res.status, res: res, data }));
|
||||
});
|
||||
|
||||
export const get = (url, params) => apiRequest('get', `${url}${getQueryString(params)}`);
|
||||
export const get = (url, params, headers) =>
|
||||
apiRequest('get', makeUrl(url, params), undefined, headers);
|
||||
|
||||
export const del = (url, params) => apiRequest('delete', `${url}${getQueryString(params)}`);
|
||||
export const del = (url, params, headers) =>
|
||||
apiRequest('delete', makeUrl(url, params), undefined, headers);
|
||||
|
||||
export const post = (url, params) => apiRequest('post', url, JSON.stringify(params));
|
||||
export const post = (url, params, headers) =>
|
||||
apiRequest('post', url, JSON.stringify(params), headers);
|
||||
|
||||
export const put = (url, params) => apiRequest('put', url, JSON.stringify(params));
|
||||
export const put = (url, params, headers) =>
|
||||
apiRequest('put', url, JSON.stringify(params), headers);
|
||||
|
||||
export const hook = (_this, method, callback) => {
|
||||
const orig = _this[method];
|
||||
|
27
package.json
27
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "umami",
|
||||
"version": "0.80.0",
|
||||
"version": "0.96.0",
|
||||
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
||||
"author": "Mike Cao <mike@mikecao.com>",
|
||||
"license": "MIT",
|
||||
@ -56,7 +56,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "2.8.0",
|
||||
"@prisma/client": "2.9.0",
|
||||
"@reduxjs/toolkit": "^1.4.0",
|
||||
"bcrypt": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
@ -65,17 +65,17 @@
|
||||
"cookie": "^0.4.1",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^2.16.1",
|
||||
"date-fns-tz": "^1.0.10",
|
||||
"detect-browser": "^5.1.1",
|
||||
"date-fns-tz": "^1.0.12",
|
||||
"detect-browser": "^5.2.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"formik": "^2.1.7",
|
||||
"formik": "^2.2.0",
|
||||
"immer": "^7.0.9",
|
||||
"is-localhost-ip": "^1.4.0",
|
||||
"isbot-fast": "^1.2.0",
|
||||
"jose": "^2.0.2",
|
||||
"maxmind": "^4.2.0",
|
||||
"maxmind": "^4.3.0",
|
||||
"moment-timezone": "^0.5.31",
|
||||
"next": "^9.5.4",
|
||||
"next": "^9.5.5",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-intl": "^5.8.4",
|
||||
@ -83,6 +83,7 @@
|
||||
"react-simple-maps": "^2.1.2",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-tooltip": "^4.2.10",
|
||||
"react-use-measure": "^2.0.2",
|
||||
"react-window": "^1.8.5",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
@ -91,11 +92,11 @@
|
||||
"thenby": "^1.3.4",
|
||||
"timezone-support": "^2.0.2",
|
||||
"tinycolor2": "^1.4.2",
|
||||
"uuid": "^8.3.0"
|
||||
"uuid": "^8.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^2.13.0",
|
||||
"@prisma/cli": "2.8.0",
|
||||
"@formatjs/cli": "^2.13.2",
|
||||
"@prisma/cli": "2.9.0",
|
||||
"@rollup/plugin-buble": "^0.21.3",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"@rollup/plugin-replace": "^2.3.3",
|
||||
@ -103,10 +104,10 @@
|
||||
"cross-env": "^7.0.2",
|
||||
"del": "^6.0.0",
|
||||
"dotenv-cli": "^4.0.0",
|
||||
"eslint": "^7.10.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-config-prettier": "^6.12.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.21.2",
|
||||
"eslint-plugin-react": "^7.21.4",
|
||||
"eslint-plugin-react-hooks": "^4.1.2",
|
||||
"extract-react-intl-messages": "^4.1.1",
|
||||
"husky": "^4.3.0",
|
||||
@ -118,7 +119,7 @@
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"prettier": "^2.1.2",
|
||||
"prettier-eslint": "^11.0.0",
|
||||
"rollup": "^2.28.2",
|
||||
"rollup": "^2.30.0",
|
||||
"rollup-plugin-hashbang": "^2.2.2",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"stylelint": "^13.7.2",
|
||||
|
26
pages/api/realtime/init.js
Normal file
26
pages/api/realtime/init.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { subMinutes } from 'date-fns';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { ok, methodNotAllowed } from 'lib/response';
|
||||
import { getUserWebsites, getRealtimeData } from 'lib/queries';
|
||||
import { createToken } from 'lib/crypto';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const { user_id } = req.auth;
|
||||
|
||||
const websites = await getUserWebsites(user_id);
|
||||
const ids = websites.map(({ website_id }) => website_id);
|
||||
const token = await createToken({ websites: ids });
|
||||
const data = await getRealtimeData(ids, subMinutes(new Date(), 30));
|
||||
|
||||
return ok(res, {
|
||||
websites,
|
||||
token,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
27
pages/api/realtime/update.js
Normal file
27
pages/api/realtime/update.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { ok, methodNotAllowed, badRequest } from 'lib/response';
|
||||
import { getRealtimeData } from 'lib/queries';
|
||||
import { parseToken } from 'lib/crypto';
|
||||
import { TOKEN_HEADER } from 'lib/constants';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const { start_at } = req.query;
|
||||
|
||||
const token = req.headers[TOKEN_HEADER];
|
||||
|
||||
if (!token) {
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
const { websites } = await parseToken(token);
|
||||
|
||||
const data = await getRealtimeData(websites, new Date(+start_at));
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import moment from 'moment-timezone';
|
||||
import { getEvents } from 'lib/queries';
|
||||
import { getEventMetrics } from 'lib/queries';
|
||||
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
|
||||
import { allowQuery } from 'lib/auth';
|
||||
|
||||
@ -21,7 +21,7 @@ export default async (req, res) => {
|
||||
const startDate = new Date(+start_at);
|
||||
const endDate = new Date(+end_at);
|
||||
|
||||
const events = await getEvents(websiteId, startDate, endDate, tz, unit, { url });
|
||||
const events = await getEventMetrics(websiteId, startDate, endDate, tz, unit, { url });
|
||||
|
||||
return ok(res, events);
|
||||
}
|
||||
|
@ -1,27 +1,67 @@
|
||||
import { getMetrics } from 'lib/queries';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
|
||||
import { getPageviewMetrics, getSessionMetrics } from 'lib/queries';
|
||||
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
|
||||
import { DOMAIN_REGEX } from 'lib/constants';
|
||||
import { allowQuery } from 'lib/auth';
|
||||
|
||||
const sessionColumns = ['browser', 'os', 'device', 'country'];
|
||||
const pageviewColumns = ['url', 'referrer'];
|
||||
|
||||
function getTable(type) {
|
||||
if (type === 'event') {
|
||||
return 'event';
|
||||
}
|
||||
|
||||
if (sessionColumns.includes(type)) {
|
||||
return 'session';
|
||||
}
|
||||
|
||||
return 'pageview';
|
||||
}
|
||||
|
||||
function getColumn(type) {
|
||||
if (type === 'event') {
|
||||
return `concat(event_type, ':', event_value)`;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
export default async (req, res) => {
|
||||
if (req.method === 'GET') {
|
||||
if (!(await allowQuery(req))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { id, start_at, end_at, url } = req.query;
|
||||
const { id, type, start_at, end_at, domain, url } = req.query;
|
||||
|
||||
if (domain && !DOMAIN_REGEX.test(domain)) {
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
const websiteId = +id;
|
||||
const startDate = new Date(+start_at);
|
||||
const endDate = new Date(+end_at);
|
||||
|
||||
const metrics = await getMetrics(websiteId, startDate, endDate, { url });
|
||||
if (sessionColumns.includes(type)) {
|
||||
const data = await getSessionMetrics(websiteId, startDate, endDate, type, { url });
|
||||
|
||||
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
|
||||
obj[key] = Number(metrics[0][key]) || 0;
|
||||
return obj;
|
||||
}, {});
|
||||
return ok(res, data);
|
||||
}
|
||||
|
||||
return ok(res, stats);
|
||||
if (type === 'event' || pageviewColumns.includes(type)) {
|
||||
const data = await getPageviewMetrics(
|
||||
websiteId,
|
||||
startDate,
|
||||
endDate,
|
||||
getColumn(type),
|
||||
getTable(type),
|
||||
{
|
||||
domain: type !== 'event' && domain,
|
||||
url: type !== 'url' && url,
|
||||
},
|
||||
);
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import moment from 'moment-timezone';
|
||||
import { getPageviews } from 'lib/queries';
|
||||
import { getPageviewStats } from 'lib/queries';
|
||||
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
|
||||
import { allowQuery } from 'lib/auth';
|
||||
|
||||
@ -21,12 +21,12 @@ export default async (req, res) => {
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
const [pageviews, uniques] = await Promise.all([
|
||||
getPageviews(websiteId, startDate, endDate, tz, unit, '*', url),
|
||||
getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url),
|
||||
const [pageviews, sessions] = await Promise.all([
|
||||
getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', url),
|
||||
getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url),
|
||||
]);
|
||||
|
||||
return ok(res, { pageviews, uniques });
|
||||
return ok(res, { pageviews, sessions });
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
|
@ -1,68 +0,0 @@
|
||||
import { getPageviewMetrics, getSessionMetrics } from 'lib/queries';
|
||||
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
|
||||
import { DOMAIN_REGEX } from 'lib/constants';
|
||||
import { allowQuery } from 'lib/auth';
|
||||
|
||||
const sessionColumns = ['browser', 'os', 'device', 'country'];
|
||||
const pageviewColumns = ['url', 'referrer'];
|
||||
|
||||
function getTable(type) {
|
||||
if (type === 'event') {
|
||||
return 'event';
|
||||
}
|
||||
|
||||
if (sessionColumns.includes(type)) {
|
||||
return 'session';
|
||||
}
|
||||
|
||||
return 'pageview';
|
||||
}
|
||||
|
||||
function getColumn(type) {
|
||||
if (type === 'event') {
|
||||
return `concat(event_type, ':', event_value)`;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
export default async (req, res) => {
|
||||
if (req.method === 'GET') {
|
||||
if (!(await allowQuery(req))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { id, type, start_at, end_at, domain, url } = req.query;
|
||||
|
||||
if (domain && !DOMAIN_REGEX.test(domain)) {
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
const websiteId = +id;
|
||||
const startDate = new Date(+start_at);
|
||||
const endDate = new Date(+end_at);
|
||||
|
||||
if (sessionColumns.includes(type)) {
|
||||
const data = await getSessionMetrics(websiteId, startDate, endDate, type, { url });
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
|
||||
if (type === 'event' || pageviewColumns.includes(type)) {
|
||||
const data = await getPageviewMetrics(
|
||||
websiteId,
|
||||
startDate,
|
||||
endDate,
|
||||
getColumn(type),
|
||||
getTable(type),
|
||||
{
|
||||
domain,
|
||||
url: type !== 'url' && url,
|
||||
},
|
||||
);
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user