Merge pull request #296 from mikecao/dev

v0.96.0 Realtime dashboard
This commit is contained in:
Mike Cao 2020-10-13 18:47:35 -07:00 committed by GitHub
commit f13d7cfed5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 2377 additions and 1201 deletions

1
assets/bolt.svg Normal file
View 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
View 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
View 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

View File

@ -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
View 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',
})}
/>
);
}

View 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;
}

View File

@ -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>

View File

@ -5,3 +5,7 @@
align-items: center;
min-height: 600px;
}
.icon {
margin-bottom: 30px;
}

View 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>
);
}

View File

@ -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}

View File

@ -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
View 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>;
}

View File

@ -1,5 +1,4 @@
.type {
font-size: var(--font-size-small);
.tag {
padding: 2px 4px;
border: 1px solid var(--gray300);
border-radius: 4px;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
);

View 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>;
};

View 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;
}
}

View File

@ -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>

View File

@ -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

View File

@ -12,11 +12,3 @@
font-weight: 600;
margin-right: 4px;
}
.dot {
background: var(--green400);
width: 10px;
height: 10px;
border-radius: 100%;
margin-right: 10px;
}

View File

@ -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) {

View File

@ -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}
/>
);

View File

@ -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}
/>
);

View 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>
);
};

View 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;
}

View File

@ -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)}
/>

View File

@ -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)}

View File

@ -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}
</>
);

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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}
/>
);

View File

@ -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>
);
};

View File

@ -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}
/>

View 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}
/>
);
}

View 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>
</>
);
}

View File

@ -0,0 +1,3 @@
.metrics {
display: flex;
}

View 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>
);
}

View 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;
}

View 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}
/>
)}
</>
);
}

View File

@ -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>
);
};

View File

@ -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}

View File

@ -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>

View 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>
);
}

View File

@ -0,0 +1,7 @@
.container {
display: flex;
}
.chart {
margin-bottom: 30px;
}

View File

@ -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';

View File

@ -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>

View File

@ -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;
}

View File

@ -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;

View File

@ -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 }}

View File

@ -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>
</>
);

View File

@ -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 }}

View File

@ -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>
</>
);

View File

@ -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>
)}

View File

@ -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
View 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;
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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": "Επισκέπτες"
}

View File

@ -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"
}

View File

@ -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
View 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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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": "訪問者数"
}

View File

@ -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": "Зочид"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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": "Посетители"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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": "Відвідувачі"
}

View File

@ -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": "独立访客"
}

View File

@ -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 });
}

View File

@ -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;

View File

@ -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],

View File

@ -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;
}

View File

@ -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' },

View File

@ -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(),
};
}

View File

@ -28,3 +28,7 @@ export function getQueryString(params = {}) {
return '';
}
export function makeUrl(url, params) {
return `${url}${getQueryString(params)}`;
}

View File

@ -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];

View File

@ -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",

View File

@ -7,7 +7,7 @@ import { getIpAddress } from '../../lib/request';
export default async (req, res) => {
await useCors(req, res);
if (isBot(req.headers['user-agent'])) {
return ok(res);
}

View 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);
};

View 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);
};

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

Some files were not shown because too many files have changed in this diff Show More