Merge pull request #1003 from mikecao/dev

v1.27.0
pull/1029/head v1.27.0
Mike Cao 2022-03-02 20:13:30 -08:00 committed by GitHub
commit aa9dbd2092
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
179 changed files with 3663 additions and 3350 deletions

View File

@ -2,9 +2,11 @@
FROM node:12.22-alpine AS build FROM node:12.22-alpine AS build
ARG BASE_PATH ARG BASE_PATH
ARG DATABASE_TYPE ARG DATABASE_TYPE
ENV BASE_PATH=$BASE_PATH ENV BASE_PATH=$BASE_PATH
ENV DATABASE_URL "postgresql://umami:umami@db:5432/umami" \ ENV DATABASE_URL "postgresql://umami:umami@db:5432/umami"
DATABASE_TYPE=$DATABASE_TYPE ENV DATABASE_TYPE=$DATABASE_TYPE
WORKDIR /build WORKDIR /build
RUN yarn config set --home enableTelemetry 0 RUN yarn config set --home enableTelemetry 0

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Pro 6.0.0-alpha1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M424 392H24C11 392 0 403 0 416V416C0 429 11 440 24 440H424C437 440 448 429 448 416V416C448 403 437 392 424 392ZM424 72H24C11 72 0 83 0 96V96C0 109 11 120 24 120H424C437 120 448 109 448 96V96C448 83 437 72 424 72ZM424 232H24C11 232 0 243 0 256V256C0 269 11 280 24 280H424C437 280 448 269 448 256V256C448 243 437 232 424 232Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Pro 6.0.0-alpha2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M424 392H24C10.8 392 0 402.8 0 416V416C0 429.2 10.8 440 24 440H424C437.2 440 448 429.2 448 416V416C448 402.8 437.2 392 424 392ZM424 72H24C10.8 72 0 82.8 0 96V96C0 109.2 10.8 120 24 120H424C437.2 120 448 109.2 448 96V96C448 82.8 437.2 72 424 72ZM424 232H24C10.8 232 0 242.8 0 256V256C0 269.2 10.8 280 24 280H424C437.2 280 448 269.2 448 256V256C448 242.8 437.2 232 424 232Z"/></svg>

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 594 B

1
assets/gear.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 6.0.0-alpha2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M504.265 315.978C504.265 307.326 499.658 299.134 491.906 294.586L458.998 275.615C459.643 269.099 459.966 262.549 459.966 256S459.643 242.901 458.998 236.385L491.906 217.414C499.658 212.866 504.265 204.674 504.265 196.022C504.265 174.755 454.947 67.846 419.746 67.846C415.502 67.846 411.236 68.939 407.379 71.203L374.599 90.172C363.888 82.43 352.533 75.848 340.531 70.428V32.488C340.531 21.262 333.047 11.453 322.205 8.613C300.654 2.871 278.425 0 256.181 0C233.935 0 211.675 2.871 190.06 8.613C179.218 11.453 171.734 21.262 171.734 32.488V70.428C159.732 75.848 148.377 82.43 137.666 90.172L104.886 71.203C101.031 68.939 96.763 67.846 92.519 67.846C92.517 67.846 92.514 67.846 92.512 67.846C60.048 67.846 8 169.591 8 196.022C8 204.674 12.607 212.866 20.359 217.414L53.267 236.385C52.622 242.901 52.299 249.451 52.299 256S52.622 269.099 53.267 275.615L20.359 294.586C12.607 299.134 8 307.326 8 315.978C8 337.245 57.318 444.154 92.519 444.154C96.763 444.154 101.029 443.061 104.886 440.797L137.666 421.828C148.377 429.57 159.732 436.152 171.734 441.572V479.512C171.734 490.738 179.218 500.547 190.06 503.387C211.611 509.129 233.84 512 256.084 512C278.33 512 300.59 509.129 322.205 503.387C333.047 500.547 340.531 490.738 340.531 479.512V441.572C352.533 436.152 363.888 429.57 374.599 421.828L407.379 440.797C411.234 443.061 415.502 444.154 419.746 444.154C452.209 444.154 504.265 342.423 504.265 315.978ZM415.361 389.959C391.561 376.186 404.101 383.444 371.705 364.695C329.649 395.09 339.375 389.426 292.531 410.582V460.82C279.236 463.161 266.948 464 256.093 464C240.669 464 228.14 462.306 219.734 460.824V410.582C172.779 389.376 182.552 395.044 140.56 364.695C108.748 383.105 117.896 377.811 96.924 389.949C81.181 371.256 68.849 349.895 60.517 326.84C81.643 314.663 72.361 320.014 104.088 301.723C101.549 276.083 100.277 266.079 100.277 256.04C100.277 246.018 101.545 235.96 104.088 210.277C72.198 191.892 81.571 197.295 60.504 185.152C68.818 162.109 81.187 140.686 96.904 122.041C120.704 135.814 108.164 128.556 140.56 147.305C182.616 116.91 172.89 122.574 219.734 101.418V51.18C233.029 48.839 245.318 48 256.172 48C271.597 48 284.126 49.694 292.531 51.176V101.418C339.486 122.624 329.713 116.956 371.705 147.305C405.655 127.657 394.228 134.27 415.343 122.051C431.084 140.744 443.416 162.105 451.748 185.16C430.622 197.337 439.904 191.986 408.177 210.277C410.716 235.917 411.988 245.921 411.988 255.96C411.988 265.982 410.72 276.04 408.177 301.723C440.067 320.108 430.694 314.705 451.761 326.848C443.447 349.891 431.078 371.314 415.361 389.959ZM256.133 160C203.258 160 160.133 203.125 160.133 256S203.258 352 256.133 352S352.133 308.875 352.133 256S309.008 160 256.133 160ZM256.133 304C229.666 304 208.133 282.467 208.133 256S229.666 208 256.133 208S304.133 229.533 304.133 256S282.599 304 256.133 304Z "></path></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!-- Font Awesome Pro 6.0.0-alpha1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M345 375C354 384 354 400 345 409S320 418 311 409L192 290L73 409C64 418 48 418 39 409S30 384 39 375L158 256L39 137C30 128 30 112 39 103S64 94 73 103L192 222L311 103C320 94 336 94 345 103S354 128 345 137L226 256L345 375Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!-- Font Awesome Pro 6.0.0-alpha2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M312.973 375.032C322.342 384.401 322.342 399.604 312.973 408.973S288.401 418.342 279.032 408.973L160 289.941L40.968 408.973C31.599 418.342 16.396 418.342 7.027 408.973S-2.342 384.401 7.027 375.032L126.059 256L7.027 136.968C-2.342 127.599 -2.342 112.396 7.027 103.027S31.599 93.658 40.968 103.027L160 222.059L279.032 103.027C288.401 93.658 303.604 93.658 312.973 103.027S322.342 127.599 312.973 136.968L193.941 256L312.973 375.032Z"/></svg>

Before

Width:  |  Height:  |  Size: 441 B

After

Width:  |  Height:  |  Size: 653 B

View File

@ -0,0 +1,44 @@
import Button from 'components/common/Button';
import XMark from 'assets/xmark.svg';
import Bars from 'assets/bars.svg';
import { useState } from 'react';
import styles from './HamburgerButton.module.css';
import MobileMenu from './MobileMenu';
import { FormattedMessage } from 'react-intl';
const menuItems = [
{
label: <FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />,
value: '/dashboard',
},
{ label: <FormattedMessage id="label.realtime" defaultMessage="Realtime" />, value: '/realtime' },
{ label: <FormattedMessage id="label.settings" defaultMessage="Settings" />, value: '/settings' },
{
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
value: '/settings/profile',
},
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: '/logout' },
];
export default function HamburgerButton() {
const [active, setActive] = useState(false);
function handleClick() {
setActive(state => !state);
}
function handleClose() {
setActive(false);
}
return (
<>
<Button
className={styles.button}
icon={active ? <XMark /> : <Bars />}
onClick={handleClick}
/>
{active && <MobileMenu items={menuItems} onClose={handleClose} />}
</>
);
}

View File

@ -0,0 +1,9 @@
.button {
display: none;
}
@media only screen and (max-width: 768px) {
.button {
display: flex;
}
}

View File

@ -5,7 +5,7 @@ import NextLink from 'next/link';
import Icon from './Icon'; import Icon from './Icon';
import styles from './Link.module.css'; import styles from './Link.module.css';
function Link({ className, icon, children, size, iconRight, ...props }) { function Link({ className, icon, children, size, iconRight, onClick, ...props }) {
return ( return (
<NextLink {...props}> <NextLink {...props}>
<a <a
@ -15,6 +15,7 @@ function Link({ className, icon, children, size, iconRight, ...props }) {
[styles.xsmall]: size === 'xsmall', [styles.xsmall]: size === 'xsmall',
[styles.iconRight]: iconRight, [styles.iconRight]: iconRight,
})} })}
onClick={onClick}
> >
{icon && <Icon className={styles.icon} icon={icon} size={size} />} {icon && <Icon className={styles.icon} icon={icon} size={size} />}
{children} {children}

View File

@ -8,20 +8,12 @@ a.link:visited {
align-items: center; align-items: center;
} }
a.link:before { a.link span {
content: ''; border-bottom: 2px solid transparent;
position: absolute;
bottom: -2px;
width: 0;
height: 2px;
background: var(--primary400);
opacity: 0.5;
transition: width 100ms;
} }
a.link:hover:before { a.link:hover span {
width: 100%; border-bottom: 2px solid var(--primary400);
transition: width 100ms;
} }
a.link.large { a.link.large {

View File

@ -0,0 +1,21 @@
import Link from './Link';
import Button from './Button';
import XMark from 'assets/xmark.svg';
import styles from './MobileMenu.module.css';
export default function MobileMenu({ items = [], onClose }) {
return (
<div className={styles.menu}>
<div className={styles.header}>
<Button className={styles.button} icon={<XMark />} onClick={onClose} />
</div>
<div className={styles.items}>
{items.map(({ label, value }) => (
<Link key={value} href={value} className={styles.item} onClick={onClose}>
{label}
</Link>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,44 @@
.menu {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
z-index: 100;
display: flex;
flex-direction: column;
background-color: var(--gray50);
overflow: auto;
}
.items {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.item {
font-size: var(--font-size-large);
}
.item + .item {
margin-top: 20px;
}
.item:last-child {
margin-top: 60px;
}
.button {
margin-right: 15px;
}
.header {
display: flex;
justify-content: flex-end;
align-items: center;
height: 100px;
}

View File

@ -0,0 +1,66 @@
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import ReactTooltip from 'react-tooltip';
import styles from './OverflowText.module.css';
const OverflowText = ({ children, tooltipId }) => {
const measureEl = useRef();
const [isOverflown, setIsOverflown] = useState(false);
const measure = useCallback(
el => {
if (!el) return;
setIsOverflown(el.scrollWidth > el.clientWidth);
},
[setIsOverflown],
);
// Do one measure on mount
useEffect(() => {
measure(measureEl.current);
}, [measure]);
// Set up resize listener for subsequent measures
useEffect(() => {
if (!measureEl.current) return;
// Destructure ref in case it changes out from under us
const el = measureEl.current;
if ('ResizeObserver' in global) {
// Ideally, we have access to ResizeObservers
const observer = new ResizeObserver(() => {
measure(el);
});
observer.observe(el);
return () => observer.unobserve(el);
} else {
// Otherwise, fall back to measuring on window resizes
const handler = () => measure(el);
window.addEventListener('resize', handler, { passive: true });
return () => window.removeEventListener('resize', handler, { passive: true });
}
});
return (
<span
ref={measureEl}
data-tip={children.toString()}
data-effect="solid"
data-for={tooltipId}
className={styles.root}
>
{children}
{isOverflown && <ReactTooltip id={tooltipId}>{children}</ReactTooltip>}
</span>
);
};
OverflowText.propTypes = {
children: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
tooltipId: PropTypes.string.isRequired,
};
export default OverflowText;

View File

@ -0,0 +1,6 @@
.root {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@ -1,23 +1,27 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { setDateRange } from 'redux/actions/websites'; import useStore from 'store/queries';
import { setDateRange } from 'store/websites';
import Button from './Button'; import Button from './Button';
import Refresh from 'assets/redo.svg'; import Refresh from 'assets/redo.svg';
import Dots from 'assets/ellipsis-h.svg'; import Dots from 'assets/ellipsis-h.svg';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
function RefreshButton({ websiteId }) { function RefreshButton({ websiteId }) {
const dispatch = useDispatch();
const [dateRange] = useDateRange(websiteId); const [dateRange] = useDateRange(websiteId);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const completed = useSelector(state => state.queries[`/api/website/${websiteId}/stats`]); const selector = useCallback(state => state[`/website/${websiteId}/stats`], [websiteId]);
const completed = useStore(selector);
function handleClick() { function handleClick() {
if (dateRange) { if (!loading && dateRange) {
setLoading(true); setLoading(true);
dispatch(setDateRange(websiteId, dateRange)); if (/^[\d]+/.test(dateRange.value)) {
setDateRange(websiteId, dateRange.value);
} else {
setDateRange(websiteId, dateRange);
}
} }
} }

View File

@ -76,12 +76,13 @@ export default Table;
export const TableRow = ({ columns, row }) => ( export const TableRow = ({ columns, row }) => (
<div className={classNames(styles.row, 'row')}> <div className={classNames(styles.row, 'row')}>
{columns.map(({ key, render, className, style, cell }, index) => ( {columns.map(({ key, label, render, className, style, cell }, index) => (
<div <div
key={`${key}-${index}`} key={`${key}-${index}`}
className={classNames(styles.cell, className, cell?.className)} className={classNames(styles.cell, className, cell?.className)}
style={{ ...style, ...cell?.style }} style={{ ...style, ...cell?.style }}
> >
{label && <label>{label}</label>}
{render ? render(row) : row[key]} {render ? render(row) : row[key]}
</div> </div>
))} ))}

View File

@ -3,6 +3,12 @@
flex-direction: column; flex-direction: column;
} }
.table label {
display: none;
font-size: var(--font-size-xsmall);
font-weight: bold;
}
.header { .header {
border-bottom: 1px solid var(--gray300); border-bottom: 1px solid var(--gray300);
} }
@ -26,5 +32,24 @@
.cell { .cell {
display: flex; display: flex;
flex-direction: column;
align-items: flex-start; align-items: flex-start;
} }
@media only screen and (max-width: 992px) {
.table label {
display: block;
}
.header {
display: none;
}
.row {
flex-direction: column;
}
.cell {
margin-bottom: 20px;
}
}

View File

@ -2,9 +2,9 @@ import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { useSpring, animated } from 'react-spring'; import { useSpring, animated } from 'react-spring';
import styles from './Toast.module.css';
import Icon from 'components/common/Icon'; import Icon from 'components/common/Icon';
import Close from 'assets/times.svg'; import Close from 'assets/times.svg';
import styles from './Toast.module.css';
function Toast({ message, timeout = 3000, onClose }) { function Toast({ message, timeout = 3000, onClose }) {
const props = useSpring({ const props = useSpring({

View File

@ -1,5 +1,5 @@
.toast { .toast {
position: absolute; position: fixed;
top: 30px; top: 30px;
left: 0; left: 0;
right: 0; right: 0;

View File

@ -8,7 +8,7 @@ import FormLayout, {
FormMessage, FormMessage,
FormRow, FormRow,
} from 'components/layout/FormLayout'; } from 'components/layout/FormLayout';
import usePost from 'hooks/usePost'; import useApi from 'hooks/useApi';
const initialValues = { const initialValues = {
username: '', username: '',
@ -29,11 +29,11 @@ const validate = ({ user_id, username, password }) => {
}; };
export default function AccountEditForm({ values, onSave, onClose }) { export default function AccountEditForm({ values, onSave, onClose }) {
const post = usePost(); const { post } = useApi();
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const handleSubmit = async values => { const handleSubmit = async values => {
const { ok, data } = await post('/api/account', values); const { ok, data } = await post('/account', values);
if (ok) { if (ok) {
onSave(); onSave();

View File

@ -8,7 +8,7 @@ import FormLayout, {
FormMessage, FormMessage,
FormRow, FormRow,
} from 'components/layout/FormLayout'; } from 'components/layout/FormLayout';
import usePost from 'hooks/usePost'; import useApi from 'hooks/useApi';
const initialValues = { const initialValues = {
current_password: '', current_password: '',
@ -37,11 +37,11 @@ const validate = ({ current_password, new_password, confirm_password }) => {
}; };
export default function ChangePasswordForm({ values, onSave, onClose }) { export default function ChangePasswordForm({ values, onSave, onClose }) {
const post = usePost(); const { post } = useApi();
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const handleSubmit = async values => { const handleSubmit = async values => {
const { ok, data } = await post('/api/account/password', values); const { ok, data } = await post('/account/password', values);
if (ok) { if (ok) {
onSave(); onSave();

View File

@ -8,7 +8,7 @@ import FormLayout, {
FormMessage, FormMessage,
FormRow, FormRow,
} from 'components/layout/FormLayout'; } from 'components/layout/FormLayout';
import useDelete from 'hooks/useDelete'; import useApi from 'hooks/useApi';
const CONFIRMATION_WORD = 'DELETE'; const CONFIRMATION_WORD = 'DELETE';
@ -27,11 +27,11 @@ const validate = ({ confirmation }) => {
}; };
export default function DeleteForm({ values, onSave, onClose }) { export default function DeleteForm({ values, onSave, onClose }) {
const del = useDelete(); const { del } = useApi();
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const handleSubmit = async ({ type, id }) => { const handleSubmit = async ({ type, id }) => {
const { ok, data } = await del(`/api/${type}/${id}`); const { ok, data } = await del(`/${type}/${id}`);
if (ok) { if (ok) {
onSave(); onSave();

View File

@ -10,11 +10,12 @@ import FormLayout, {
FormRow, FormRow,
} from 'components/layout/FormLayout'; } from 'components/layout/FormLayout';
import Icon from 'components/common/Icon'; import Icon from 'components/common/Icon';
import useApi from 'hooks/useApi';
import { setItem } from 'lib/web';
import { AUTH_TOKEN } from 'lib/constants';
import { setUser } from 'store/app';
import Logo from 'assets/logo.svg'; import Logo from 'assets/logo.svg';
import styles from './LoginForm.module.css'; import styles from './LoginForm.module.css';
import usePost from 'hooks/usePost';
import { setItem } from 'lib/web';
import { AUTH_TOKEN } from '../../lib/constants';
const validate = ({ username, password }) => { const validate = ({ username, password }) => {
const errors = {}; const errors = {};
@ -30,12 +31,12 @@ const validate = ({ username, password }) => {
}; };
export default function LoginForm() { export default function LoginForm() {
const post = usePost(); const { post } = useApi();
const router = useRouter(); const router = useRouter();
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const handleSubmit = async ({ username, password }) => { const handleSubmit = async ({ username, password }) => {
const { ok, status, data } = await post('/api/auth/login', { const { ok, status, data } = await post('/auth/login', {
username, username,
password, password,
}); });
@ -43,7 +44,11 @@ export default function LoginForm() {
if (ok) { if (ok) {
setItem(AUTH_TOKEN, data.token); setItem(AUTH_TOKEN, data.token);
return router.push('/'); setUser(data.user);
await router.push('/');
return null;
} else { } else {
setMessage( setMessage(
status === 401 ? ( status === 401 ? (

View File

@ -8,7 +8,7 @@ import FormLayout, {
FormMessage, FormMessage,
FormRow, FormRow,
} from 'components/layout/FormLayout'; } from 'components/layout/FormLayout';
import usePost from 'hooks/usePost'; import useApi from 'hooks/useApi';
const CONFIRMATION_WORD = 'RESET'; const CONFIRMATION_WORD = 'RESET';
@ -27,11 +27,11 @@ const validate = ({ confirmation }) => {
}; };
export default function ResetForm({ values, onSave, onClose }) { export default function ResetForm({ values, onSave, onClose }) {
const post = usePost(); const { post } = useApi();
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const handleSubmit = async ({ type, id }) => { const handleSubmit = async ({ type, id }) => {
const { ok, data } = await post(`/api/${type}/${id}/reset`); const { ok, data } = await post(`/${type}/${id}/reset`);
if (ok) { if (ok) {
onSave(); onSave();

View File

@ -5,8 +5,6 @@ import Button from 'components/common/Button';
import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout'; import FormLayout, { FormButtons, FormRow } from 'components/layout/FormLayout';
import CopyButton from 'components/common/CopyButton'; import CopyButton from 'components/common/CopyButton';
const scriptName = process.env.TRACKER_SCRIPT_NAME || 'umami';
export default function TrackingCodeForm({ values, onClose }) { export default function TrackingCodeForm({ values, onClose }) {
const ref = useRef(); const ref = useRef();
const { basePath } = useRouter(); const { basePath } = useRouter();
@ -26,7 +24,7 @@ export default function TrackingCodeForm({ values, onClose }) {
rows={3} rows={3}
cols={60} cols={60}
spellCheck={false} spellCheck={false}
defaultValue={`<script async defer data-website-id="${values.website_uuid}" src="${document.location.origin}${basePath}/${scriptName}.js"></script>`} defaultValue={`<script async defer data-website-id="${values.website_uuid}" src="${document.location.origin}${basePath}/umami.js"></script>`}
readOnly readOnly
/> />
</FormRow> </FormRow>

View File

@ -10,7 +10,7 @@ import FormLayout, {
} from 'components/layout/FormLayout'; } from 'components/layout/FormLayout';
import Checkbox from 'components/common/Checkbox'; import Checkbox from 'components/common/Checkbox';
import { DOMAIN_REGEX } from 'lib/constants'; import { DOMAIN_REGEX } from 'lib/constants';
import usePost from 'hooks/usePost'; import useApi from 'hooks/useApi';
const initialValues = { const initialValues = {
name: '', name: '',
@ -34,11 +34,11 @@ const validate = ({ name, domain }) => {
}; };
export default function WebsiteEditForm({ values, onSave, onClose }) { export default function WebsiteEditForm({ values, onSave, onClose }) {
const post = usePost(); const { post } = useApi();
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const handleSubmit = async values => { const handleSubmit = async values => {
const { ok, data } = await post('/api/website', values); const { ok, data } = await post('/website', values);
if (ok) { if (ok) {
onSave(); onSave();

View File

@ -25,9 +25,8 @@ export default function StickyHeader({
} }
}; };
checkPosition();
if (enabled) { if (enabled) {
checkPosition();
window.addEventListener('scroll', checkPosition); window.addEventListener('scroll', checkPosition);
} }

View File

@ -1,6 +1,7 @@
.buttons { .buttons {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%;
} }
.buttons button + * { .buttons button + * {

View File

@ -4,15 +4,13 @@ import { FormattedMessage } from 'react-intl';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
import styles from './Footer.module.css'; import styles from './Footer.module.css';
import useVersion from 'hooks/useVersion'; import useVersion from 'hooks/useVersion';
import useLocale from 'hooks/useLocale'; import { HOMEPAGE_URL, VERSION_URL } from 'lib/constants';
export default function Footer() { export default function Footer() {
const { current } = useVersion(); const { current } = useVersion();
const { dir } = useLocale();
return ( return (
<footer className="container" dir={dir}> <footer className={classNames(styles.footer, 'row')}>
<div className={classNames(styles.footer, 'row')}>
<div className="col-12 col-md-4" /> <div className="col-12 col-md-4" />
<div className="col-12 col-md-4"> <div className="col-12 col-md-4">
<FormattedMessage <FormattedMessage
@ -20,7 +18,7 @@ export default function Footer() {
defaultMessage="Powered by {name}" defaultMessage="Powered by {name}"
values={{ values={{
name: ( name: (
<Link href="https://umami.is"> <Link href={HOMEPAGE_URL}>
<b>umami</b> <b>umami</b>
</Link> </Link>
), ),
@ -28,8 +26,7 @@ export default function Footer() {
/> />
</div> </div>
<div className={classNames(styles.version, 'col-12 col-md-4')}> <div className={classNames(styles.version, 'col-12 col-md-4')}>
<Link href={`https://github.com/mikecao/umami/releases`}>{`v${current}`}</Link> <Link href={VERSION_URL}>{`v${current}`}</Link>
</div>
</div> </div>
</footer> </footer>
); );

View File

@ -3,16 +3,17 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-size: var(--font-size-small); font-size: var(--font-size-small);
min-height: 100px;
text-align: center; text-align: center;
margin: 30px 0;
} }
.version { .version {
text-align: right; text-align: right;
padding-right: 10px;
} }
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
.version { .footer .version {
text-align: center; text-align: center;
} }
} }

View File

@ -1,48 +1,31 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
import Icon from 'components/common/Icon'; import Icon from 'components/common/Icon';
import LanguageButton from 'components/settings/LanguageButton'; import LanguageButton from 'components/settings/LanguageButton';
import ThemeButton from 'components/settings/ThemeButton'; import ThemeButton from 'components/settings/ThemeButton';
import HamburgerButton from 'components/common/HamburgerButton';
import UpdateNotice from 'components/common/UpdateNotice'; import UpdateNotice from 'components/common/UpdateNotice';
import UserButton from 'components/settings/UserButton'; import UserButton from 'components/settings/UserButton';
import Button from 'components/common/Button';
import Logo from 'assets/logo.svg'; import Logo from 'assets/logo.svg';
import styles from './Header.module.css'; import styles from './Header.module.css';
import useLocale from 'hooks/useLocale'; import useUser from 'hooks/useUser';
import XMark from 'assets/xmark.svg'; import { HOMEPAGE_URL } from 'lib/constants';
import Bars from 'assets/bars.svg';
export default function Header() { export default function Header() {
const user = useSelector(state => state.user); const { user } = useUser();
const [active, setActive] = useState(false);
const { dir } = useLocale();
function handleClick() {
setActive(state => !state);
}
return ( return (
<nav className="container" dir={dir}> <>
{user?.is_admin && <UpdateNotice />} {user?.is_admin && <UpdateNotice />}
<div className={classNames(styles.header, 'row align-items-center')}> <header className={classNames(styles.header, 'row')}>
<div className={styles.nav}>
<div className="">
<div className={styles.title}> <div className={styles.title}>
<Icon icon={<Logo />} size="large" className={styles.logo} /> <Icon icon={<Logo />} size="large" className={styles.logo} />
<Link href={user ? '/' : 'https://umami.is'}>umami</Link> <Link href={user ? '/' : HOMEPAGE_URL}>umami</Link>
</div> </div>
</div> <HamburgerButton />
<Button
className={styles.burger}
icon={active ? <XMark /> : <Bars />}
onClick={handleClick}
/>
{user && ( {user && (
<div className={styles.items}> <div className={styles.links}>
<div className={active ? classNames(styles.active) : ''}>
<Link href="/dashboard"> <Link href="/dashboard">
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" /> <FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
</Link> </Link>
@ -53,19 +36,13 @@ export default function Header() {
<FormattedMessage id="label.settings" defaultMessage="Settings" /> <FormattedMessage id="label.settings" defaultMessage="Settings" />
</Link> </Link>
</div> </div>
</div>
)} )}
<div className={styles.items}>
<div className={active ? classNames(styles.active) : ''}>
<div className={styles.buttons}> <div className={styles.buttons}>
<ThemeButton /> <ThemeButton />
<LanguageButton menuAlign="right" /> <LanguageButton menuAlign="right" />
{user && <UserButton />} {user && <UserButton />}
</div> </div>
</div> </header>
</div> </>
</div>
</div>
</nav>
); );
} }

View File

@ -1,17 +1,6 @@
.navbar {
align-items: stretch;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
}
.burger {
display: none;
}
.header { .header {
display: flex; display: flex;
align-items: center;
min-height: 100px; min-height: 100px;
width: 100%; width: 100%;
} }
@ -27,16 +16,8 @@
margin-right: 12px; margin-right: 12px;
} }
.nav { .links {
display: flex; flex: 1;
align-items: center;
font-size: var(--font-size-normal);
font-weight: 600;
width: 100%;
justify-content: space-between;
}
.items {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@ -44,7 +25,7 @@
font-weight: 600; font-weight: 600;
} }
.nav a + a { .links a + a {
margin-left: 40px; margin-left: 40px;
} }
@ -55,13 +36,13 @@
} }
@media only screen and (max-width: 992px) { @media only screen and (max-width: 992px) {
.nav { .header .buttons {
font-size: var(--font-size-large); flex: 1;
justify-content: space-between;
margin: 20px 0;
} }
.items { .links {
flex-wrap: wrap; order: 2;
margin: 20px 0;
min-width: 100%;
} }
} }
@ -70,47 +51,14 @@
padding: 0 15px; padding: 0 15px;
} }
.title { .buttons,
padding: 0.5rem; .links {
margin-bottom: 0.5rem;
}
.nav {
font-size: var(--font-size-normal);
flex-wrap: wrap;
justify-content: center;
flex-direction: column;
position: relative;
}
.items {
display: flex;
justify-content: unset;
font-size: var(--font-size-normal);
font-weight: 600;
}
.items > div {
display: none; display: none;
} }
.header .active { .title {
display: inherit; flex: 1;
width: 100%; padding: 0.5rem;
} margin-bottom: 0.5rem;
.items a {
width: 100%;
}
.burger {
display: block;
background: none;
border: 1px solid var(--gray900);
border-radius: 4px;
cursor: pointer;
position: absolute;
top: 0;
right: 0;
} }
} }

View File

@ -1,5 +1,4 @@
import React from 'react'; import { defineMessages } from 'react-intl';
import { defineMessages, FormattedMessage } from 'react-intl';
export const labels = defineMessages({ export const labels = defineMessages({
unknown: { id: 'label.unknown', defaultMessage: 'Unknown' }, unknown: { id: 'label.unknown', defaultMessage: 'Unknown' },
@ -13,5 +12,5 @@ export const devices = defineMessages({
}); });
export function getDeviceMessage(device) { export function getDeviceMessage(device) {
return <FormattedMessage {...(devices[device] || labels.unknown)} />; return devices[device] || labels.unknown;
} }

View File

@ -9,7 +9,7 @@ import styles from './ActiveUsers.module.css';
export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) { export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) {
const shareToken = useShareToken(); const shareToken = useShareToken();
const { data } = useFetch(!value && `/api/website/${websiteId}/active`, { const { data } = useFetch(!value && `/website/${websiteId}/active`, {
interval, interval,
headers: { [TOKEN_HEADER]: shareToken?.token }, headers: { [TOKEN_HEADER]: shareToken?.token },
}); });

View File

@ -7,7 +7,7 @@ import { dateFormat } from 'lib/date';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import useTheme from 'hooks/useTheme'; import useTheme from 'hooks/useTheme';
import useForceUpdate from 'hooks/useForceUpdate'; import useForceUpdate from 'hooks/useForceUpdate';
import { DEFAUL_CHART_HEIGHT, DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants'; import { DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
import styles from './BarChart.module.css'; import styles from './BarChart.module.css';
import ChartTooltip from './ChartTooltip'; import ChartTooltip from './ChartTooltip';
@ -16,7 +16,6 @@ export default function BarChart({
datasets, datasets,
unit, unit,
records, records,
height = DEFAUL_CHART_HEIGHT,
animationDuration = DEFAULT_ANIMATION_DURATION, animationDuration = DEFAULT_ANIMATION_DURATION,
className, className,
stacked = false, stacked = false,
@ -215,7 +214,6 @@ export default function BarChart({
data-tip="" data-tip=""
data-for={`${chartId}-tooltip`} data-for={`${chartId}-tooltip`}
className={classNames(styles.chart, className)} className={classNames(styles.chart, className)}
style={{ height }}
> >
<canvas ref={canvas} /> <canvas ref={canvas} />
</div> </div>

View File

@ -1,3 +1,10 @@
.chart { .chart {
position: relative; position: relative;
height: 400px;
}
@media only screen and (max-width: 992px) {
.chart {
height: 200px;
}
} }

View File

@ -95,3 +95,9 @@
background: var(--primary400); background: var(--primary400);
z-index: -1; z-index: -1;
} }
@media only screen and (max-width: 992px) {
.body {
height: auto;
}
}

View File

@ -11,7 +11,7 @@ export default function DevicesTable({ websiteId, ...props }) {
type="device" type="device"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />} metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId} websiteId={websiteId}
renderLabel={({ x }) => getDeviceMessage(x)} renderLabel={({ x }) => <FormattedMessage {...getDeviceMessage(x)} />}
/> />
); );
} }

View File

@ -17,7 +17,7 @@ export default function EventsChart({ websiteId, className, token }) {
const shareToken = useShareToken(); const shareToken = useShareToken();
const { data, loading } = useFetch( const { data, loading } = useFetch(
`/api/website/${websiteId}/events`, `/website/${websiteId}/events`,
{ {
params: { params: {
start_at: +startDate, start_at: +startDate,

View File

@ -19,7 +19,6 @@ const MetricCard = ({
<animated.div className={styles.value}>{props.x.interpolate(x => format(x))}</animated.div> <animated.div className={styles.value}>{props.x.interpolate(x => format(x))}</animated.div>
<div className={styles.label}> <div className={styles.label}>
{label} {label}
{~~change === 0 && !hideComparison && <span className={styles.change}>{format(0)}</span>}
{~~change !== 0 && !hideComparison && ( {~~change !== 0 && !hideComparison && (
<animated.span <animated.span
className={`${styles.change} ${ className={`${styles.change} ${

View File

@ -22,7 +22,7 @@ export default function MetricsBar({ websiteId, className }) {
} = usePageQuery(); } = usePageQuery();
const { data, error, loading } = useFetch( const { data, error, loading } = useFetch(
`/api/website/${websiteId}/stats`, `/website/${websiteId}/stats`,
{ {
params: { params: {
start_at: +startDate, start_at: +startDate,

View File

@ -26,8 +26,7 @@ export default function MetricsTable({
...props ...props
}) { }) {
const shareToken = useShareToken(); const shareToken = useShareToken();
const [dateRange] = useDateRange(websiteId); const [{ startDate, endDate, modified }] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange;
const { const {
resolve, resolve,
router, router,
@ -35,7 +34,7 @@ export default function MetricsTable({
} = usePageQuery(); } = usePageQuery();
const { data, loading, error } = useFetch( const { data, loading, error } = useFetch(
`/api/website/${websiteId}/metrics`, `/website/${websiteId}/metrics`,
{ {
params: { params: {
type, type,

View File

@ -1,6 +1,7 @@
.metrics { .metrics {
display: flex; display: flex;
margin-bottom: 10px; margin-bottom: 10px;
overflow: auto;
} }
@media only screen and (max-width: 576px) { @media only screen and (max-width: 576px) {

View File

@ -7,7 +7,7 @@ import Tag from 'components/common/Tag';
import Dot from 'components/common/Dot'; import Dot from 'components/common/Dot';
import FilterButtons from 'components/common/FilterButtons'; import FilterButtons from 'components/common/FilterButtons';
import NoData from 'components/common/NoData'; import NoData from 'components/common/NoData';
import { getDeviceMessage } from 'components/messages'; import { getDeviceMessage, labels } from 'components/messages';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import { BROWSERS } from 'lib/constants'; import { BROWSERS } from 'lib/constants';
@ -129,15 +129,10 @@ export default function RealtimeLog({ data, websites, websiteId }) {
id="message.log.visitor" id="message.log.visitor"
defaultMessage="Visitor from {country} using {browser} on {os} {device}" defaultMessage="Visitor from {country} using {browser} on {os} {device}"
values={{ values={{
country: ( country: <b>{countryNames[country] || intl.formatMessage(labels.unknown)}</b>,
<b>
{countryNames[country] ||
intl.formatMessage({ id: 'label.unknown', defaultMessage: 'Unknown' })}
</b>
),
browser: <b>{BROWSERS[browser]}</b>, browser: <b>{BROWSERS[browser]}</b>,
os: <b>{os}</b>, os: <b>{os}</b>,
device: <b>{getDeviceMessage(device)}</b>, device: <b>{intl.formatMessage(getDeviceMessage(device))}</b>,
}} }}
/> />
); );

View File

@ -95,8 +95,8 @@ export default function RealtimeViews({ websiteId, data, websites }) {
<DataTable <DataTable
title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />} title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />}
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />} metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
renderLabel={renderLink}
data={referrers} data={referrers}
height={400}
/> />
)} )}
{filter === FILTER_PAGES && ( {filter === FILTER_PAGES && (
@ -105,7 +105,6 @@ export default function RealtimeViews({ websiteId, data, websites }) {
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />} metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
renderLabel={renderLink} renderLabel={renderLink}
data={pages} data={pages}
height={400}
/> />
)} )}
</> </>

View File

@ -1,6 +1,5 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useRouter } from 'next/router';
import PageviewsChart from './PageviewsChart'; import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar'; import MetricsBar from './MetricsBar';
import WebsiteHeader from './WebsiteHeader'; import WebsiteHeader from './WebsiteHeader';
@ -12,11 +11,10 @@ import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone'; import useTimezone from 'hooks/useTimezone';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import useLocale from 'hooks/useLocale'; import { getDateArray, getDateLength, getDateRangeValues } from 'lib/date';
import { getDateArray, getDateLength, getDateRange, getDateRangeValues } from 'lib/date';
import useShareToken from 'hooks/useShareToken'; import useShareToken from 'hooks/useShareToken';
import useApi from 'hooks/useApi';
import { TOKEN_HEADER } from 'lib/constants'; import { TOKEN_HEADER } from 'lib/constants';
import { get } from 'lib/web';
import styles from './WebsiteChart.module.css'; import styles from './WebsiteChart.module.css';
export default function WebsiteChart({ export default function WebsiteChart({
@ -31,17 +29,16 @@ export default function WebsiteChart({
const shareToken = useShareToken(); const shareToken = useShareToken();
const [dateRange, setDateRange] = useDateRange(websiteId); const [dateRange, setDateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, value, modified } = dateRange; const { startDate, endDate, unit, value, modified } = dateRange;
const { locale } = useLocale();
const [timezone] = useTimezone(); const [timezone] = useTimezone();
const { basePath } = useRouter();
const { const {
router, router,
resolve, resolve,
query: { url, ref }, query: { url, ref },
} = usePageQuery(); } = usePageQuery();
const { get } = useApi();
const { data, loading, error } = useFetch( const { data, loading, error } = useFetch(
`/api/website/${websiteId}/pageviews`, `/website/${websiteId}/pageviews`,
{ {
params: { params: {
start_at: +startDate, start_at: +startDate,
@ -73,12 +70,10 @@ export default function WebsiteChart({
async function handleDateChange(value) { async function handleDateChange(value) {
if (value === 'all') { if (value === 'all') {
const { data, ok } = await get(`${basePath}/api/website/${websiteId}`); const { data, ok } = await get(`/website/${websiteId}`);
if (ok) { if (ok) {
setDateRange({ value, ...getDateRangeValues(new Date(data.created_at), Date.now()) }); setDateRange({ value, ...getDateRangeValues(new Date(data.created_at), Date.now()) });
} }
} else if (typeof value === 'string') {
setDateRange(getDateRange(value, locale));
} else { } else {
setDateRange(value); setDateRange(value);
} }

View File

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
import OverflowText from 'components/common/OverflowText';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import RefreshButton from 'components/common/RefreshButton'; import RefreshButton from 'components/common/RefreshButton';
import ButtonLayout from 'components/layout/ButtonLayout'; import ButtonLayout from 'components/layout/ButtonLayout';
@ -13,21 +15,28 @@ export default function WebsiteHeader({ websiteId, title, domain, showLink = fal
const header = showLink ? ( const header = showLink ? (
<> <>
<Favicon domain={domain} /> <Favicon domain={domain} />
<Link href="/website/[...id]" as={`/website/${websiteId}/${title}`}> <Link
{title} className={styles.titleLink}
href="/website/[...id]"
as={`/website/${websiteId}/${title}`}
>
<OverflowText tooltipId={`${websiteId}-title`}>{title}</OverflowText>
</Link> </Link>
</> </>
) : ( ) : (
<div> <>
<Favicon domain={domain} /> <Favicon domain={domain} />
{title} <OverflowText tooltipId={`${websiteId}-title`}>{title}</OverflowText>
</div> </>
); );
return ( return (
<PageHeader> <PageHeader className="row">
<div className={styles.title}>{header}</div> <div className={classNames(styles.title, 'col-12 col-lg-4')}>{header}</div>
<ActiveUsers className={styles.active} websiteId={websiteId} /> <div className={classNames(styles.active, 'col-6 col-lg-4')}>
<ActiveUsers websiteId={websiteId} />
</div>
<div className="col-6 col-lg-4">
<ButtonLayout align="right"> <ButtonLayout align="right">
<RefreshButton websiteId={websiteId} /> <RefreshButton websiteId={websiteId} />
{showLink && ( {showLink && (
@ -43,6 +52,7 @@ export default function WebsiteHeader({ websiteId, title, domain, showLink = fal
</Link> </Link>
)} )}
</ButtonLayout> </ButtonLayout>
</div>
</PageHeader> </PageHeader>
); );
} }

View File

@ -1,15 +1,28 @@
.title { .title {
color: var(--gray900); color: var(--gray900);
font-size: var(--font-size-large); font-size: var(--font-size-large);
line-height: var(--font-size-large); line-height: var(--font-size-xlarge);
align-items: center;
display: flex;
max-width: 100%;
overflow: hidden;
}
.titleLink {
max-width: 100%;
} }
.link { .link {
font-weight: 600; font-weight: 600;
} }
@media only screen and (max-width: 576px) { .active {
display: flex;
justify-content: center;
}
@media only screen and (max-width: 992px) {
.active { .active {
display: none; justify-content: flex-start;
} }
} }

View File

@ -33,8 +33,8 @@ export default function RealtimeDashboard() {
const countryNames = useCountryNames(locale); const countryNames = useCountryNames(locale);
const [data, setData] = useState(); const [data, setData] = useState();
const [websiteId, setWebsiteId] = useState(0); const [websiteId, setWebsiteId] = useState(0);
const { data: init, loading } = useFetch('/api/realtime/init'); const { data: init, loading } = useFetch('/realtime/init');
const { data: updates } = useFetch('/api/realtime/update', { const { data: updates } = useFetch('/realtime/update', {
params: { start_at: data?.timestamp }, params: { start_at: data?.timestamp },
disabled: !init?.websites?.length || !data, disabled: !init?.websites?.length || !data,
interval: REALTIME_INTERVAL, interval: REALTIME_INTERVAL,
@ -145,7 +145,6 @@ export default function RealtimeDashboard() {
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />} metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
data={countries} data={countries}
renderLabel={renderCountryName} renderLabel={renderCountryName}
height={500}
/> />
</GridColumn> </GridColumn>
<GridColumn xs={12} lg={8}> <GridColumn xs={12} lg={8}>

View File

@ -1,23 +1,27 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import MenuLayout from 'components/layout/MenuLayout'; import MenuLayout from 'components/layout/MenuLayout';
import WebsiteSettings from '../settings/WebsiteSettings'; import WebsiteSettings from 'components/settings/WebsiteSettings';
import AccountSettings from '../settings/AccountSettings'; import AccountSettings from 'components/settings/AccountSettings';
import ProfileSettings from '../settings/ProfileSettings'; import ProfileSettings from 'components/settings/ProfileSettings';
import { useSelector } from 'react-redux'; import useUser from 'hooks/useUser';
import { FormattedMessage } from 'react-intl';
const WEBSITES = '/settings'; const WEBSITES = '/settings';
const ACCOUNTS = '/settings/accounts'; const ACCOUNTS = '/settings/accounts';
const PROFILE = '/settings/profile'; const PROFILE = '/settings/profile';
export default function Settings() { export default function Settings() {
const user = useSelector(state => state.user); const { user } = useUser();
const [option, setOption] = useState(WEBSITES); const [option, setOption] = useState(WEBSITES);
const router = useRouter(); const router = useRouter();
const { pathname } = router; const { pathname } = router;
if (!user) {
return null;
}
const menuOptions = [ const menuOptions = [
{ {
label: <FormattedMessage id="label.websites" defaultMessage="Websites" />, label: <FormattedMessage id="label.websites" defaultMessage="Websites" />,

View File

@ -1,5 +1,4 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
@ -13,15 +12,16 @@ import Button from 'components/common/Button';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import Icon from 'components/common/Icon'; import Icon from 'components/common/Icon';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import useUser from 'hooks/useUser';
import ChevronDown from 'assets/chevron-down.svg'; import ChevronDown from 'assets/chevron-down.svg';
import styles from './TestConsole.module.css'; import styles from './TestConsole.module.css';
export default function TestConsole() { export default function TestConsole() {
const user = useSelector(state => state.user); const { user } = useUser();
const [website, setWebsite] = useState(); const [website, setWebsite] = useState();
const [show, setShow] = useState(true); const [show, setShow] = useState(true);
const { basePath } = useRouter(); const { basePath } = useRouter();
const { data } = useFetch('/api/websites'); const { data } = useFetch('/websites');
if (!data || !user?.is_admin) { if (!data || !user?.is_admin) {
return null; return null;
@ -68,7 +68,7 @@ export default function TestConsole() {
{show && ( {show && (
<div className={classNames(styles.test, 'row')}> <div className={classNames(styles.test, 'row')}>
<div className="col-4"> <div className="col-4">
<PageHeader>Page links</PageHeader> <PageHeader>Page links</PageHeader>Nmo
<div> <div>
<Link href={`?page=1`}> <Link href={`?page=1`}>
<a>page one</a> <a>page one</a>
@ -79,6 +79,11 @@ export default function TestConsole() {
<a>page two</a> <a>page two</a>
</Link> </Link>
</div> </div>
<div>
<Link href={`https://www.google.com`}>
<a className="umami--click--external-link">external link</a>
</Link>
</div>
</div> </div>
<div className="col-4"> <div className="col-4">
<PageHeader>CSS events</PageHeader> <PageHeader>CSS events</PageHeader>

View File

@ -9,20 +9,20 @@ import MenuLayout from 'components/layout/MenuLayout';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
import Loading from 'components/common/Loading'; import Loading from 'components/common/Loading';
import Arrow from 'assets/arrow-right.svg'; import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteDetails.module.css'; import PagesTable from 'components/metrics/PagesTable';
import PagesTable from '../metrics/PagesTable'; import ReferrersTable from 'components/metrics/ReferrersTable';
import ReferrersTable from '../metrics/ReferrersTable'; import BrowsersTable from 'components/metrics/BrowsersTable';
import BrowsersTable from '../metrics/BrowsersTable'; import OSTable from 'components/metrics/OSTable';
import OSTable from '../metrics/OSTable'; import DevicesTable from 'components/metrics/DevicesTable';
import DevicesTable from '../metrics/DevicesTable'; import CountriesTable from 'components/metrics/CountriesTable';
import CountriesTable from '../metrics/CountriesTable'; import LanguagesTable from 'components/metrics/LanguagesTable';
import LanguagesTable from '../metrics/LanguagesTable'; import EventsTable from 'components/metrics/EventsTable';
import EventsTable from '../metrics/EventsTable'; import EventsChart from 'components/metrics/EventsChart';
import EventsChart from '../metrics/EventsChart';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import useShareToken from 'hooks/useShareToken'; import useShareToken from 'hooks/useShareToken';
import { DEFAULT_ANIMATION_DURATION, TOKEN_HEADER } from 'lib/constants'; import { DEFAULT_ANIMATION_DURATION, TOKEN_HEADER } from 'lib/constants';
import styles from './WebsiteDetails.module.css';
const views = { const views = {
url: PagesTable, url: PagesTable,
@ -37,7 +37,7 @@ const views = {
export default function WebsiteDetails({ websiteId }) { export default function WebsiteDetails({ websiteId }) {
const shareToken = useShareToken(); const shareToken = useShareToken();
const { data } = useFetch(`/api/website/${websiteId}`, { const { data } = useFetch(`/website/${websiteId}`, {
headers: { [TOKEN_HEADER]: shareToken?.token }, headers: { [TOKEN_HEADER]: shareToken?.token },
}); });
const [chartLoaded, setChartLoaded] = useState(false); const [chartLoaded, setChartLoaded] = useState(false);

View File

@ -4,15 +4,23 @@ import Link from 'components/common/Link';
import WebsiteChart from 'components/metrics/WebsiteChart'; import WebsiteChart from 'components/metrics/WebsiteChart';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import Button from 'components/common/Button';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import DashboardSettingsButton from 'components/settings/DashboardSettingsButton';
import Button from 'components/common/Button';
import useStore from 'store/app';
import Arrow from 'assets/arrow-right.svg'; import Arrow from 'assets/arrow-right.svg';
import Chart from 'assets/chart-bar.svg';
import styles from './WebsiteList.module.css'; import styles from './WebsiteList.module.css';
const selector = state => state.dashboard;
export default function WebsiteList({ userId }) { export default function WebsiteList({ userId }) {
const { data } = useFetch('/api/websites', { params: { user_id: userId } }); const { data } = useFetch('/websites', { params: { user_id: userId } });
const [showCharts, setShowCharts] = useState(true); const { showCharts, limit } = useStore(selector);
const [max, setMax] = useState(limit);
function handleMore() {
setMax(max + limit);
}
if (!data) { if (!data) {
return null; return null;
@ -40,13 +48,10 @@ export default function WebsiteList({ userId }) {
return ( return (
<Page> <Page>
<div className={styles.menubar}> <div className={styles.menubar}>
<Button <DashboardSettingsButton />
tooltip={<FormattedMessage id="message.toggle-charts" defaultMessage="Toggle charts" />}
icon={<Chart />}
onClick={() => setShowCharts(!showCharts)}
/>
</div> </div>
{data.map(({ website_id, name, domain }) => ( {data.map(({ website_id, name, domain }, index) =>
index < max ? (
<div key={website_id} className={styles.website}> <div key={website_id} className={styles.website}>
<WebsiteChart <WebsiteChart
websiteId={website_id} websiteId={website_id}
@ -56,7 +61,13 @@ export default function WebsiteList({ userId }) {
showLink showLink
/> />
</div> </div>
))} ) : null,
)}
{max < data.length && (
<Button className={styles.button} onClick={handleMore}>
<FormattedMessage id="label.more" defaultMessage="More" />
</Button>
)}
</Page> </Page>
); );
} }

View File

@ -16,3 +16,8 @@
justify-content: flex-end; justify-content: flex-end;
padding-top: 10px; padding-top: 10px;
} }
.button {
align-self: center;
margin-bottom: 40px;
}

View File

@ -25,7 +25,7 @@ export default function AccountSettings() {
const [deleteAccount, setDeleteAccount] = useState(); const [deleteAccount, setDeleteAccount] = useState();
const [saved, setSaved] = useState(0); const [saved, setSaved] = useState(0);
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const { data } = useFetch(`/api/accounts`, {}, [saved]); const { data } = useFetch(`/accounts`, {}, [saved]);
const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null); const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null);
@ -53,23 +53,23 @@ export default function AccountSettings() {
{ {
key: 'username', key: 'username',
label: <FormattedMessage id="label.username" defaultMessage="Username" />, label: <FormattedMessage id="label.username" defaultMessage="Username" />,
className: 'col-4 col-md-3', className: 'col-12 col-lg-4',
}, },
{ {
key: 'is_admin', key: 'is_admin',
label: <FormattedMessage id="label.administrator" defaultMessage="Administrator" />, label: <FormattedMessage id="label.administrator" defaultMessage="Administrator" />,
className: 'col-4 col-md-3', className: 'col-12 col-lg-3',
render: Checkmark, render: Checkmark,
}, },
{ {
key: 'dashboard', key: 'dashboard',
label: <FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />, label: <FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />,
className: 'col-4 col-md-3', className: 'col-12 col-lg-3',
render: DashboardLink, render: DashboardLink,
}, },
{ {
key: 'actions', key: 'actions',
className: classNames(styles.buttons, 'col-12 col-md-3 pt-2 pt-md-0'), className: classNames(styles.buttons, 'col-12 col-lg-2 pt-2 pt-md-0'),
render: Buttons, render: Buttons,
}, },
]; ];

View File

@ -0,0 +1,27 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import MenuButton from 'components/common/MenuButton';
import Gear from 'assets/gear.svg';
import useStore, { setDashboard } from 'store/app';
const selector = state => state.dashboard;
export default function DashboardSettingsButton() {
const settings = useStore(selector);
const menuOptions = [
{
label: <FormattedMessage id="message.toggle-charts" defaultMessage="Toggle charts" />,
value: 'charts',
},
];
function handleSelect(value) {
if (value === 'charts') {
setDashboard({ showCharts: !settings.showCharts });
}
//setDashboard(value);
}
return <MenuButton icon={<Gear />} options={menuOptions} onSelect={handleSelect} hideLabel />;
}

View File

@ -4,18 +4,19 @@ import DateFilter, { filterOptions } from 'components/common/DateFilter';
import Button from 'components/common/Button'; import Button from 'components/common/Button';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import { DEFAULT_DATE_RANGE } from 'lib/constants'; import { DEFAULT_DATE_RANGE } from 'lib/constants';
import { getDateRange } from 'lib/date';
import styles from './DateRangeSetting.module.css'; import styles from './DateRangeSetting.module.css';
import useLocale from 'hooks/useLocale';
export default function DateRangeSetting() { export default function DateRangeSetting() {
const { locale } = useLocale();
const [dateRange, setDateRange] = useDateRange(); const [dateRange, setDateRange] = useDateRange();
const { startDate, endDate, value } = dateRange; const { startDate, endDate, value } = dateRange;
const options = filterOptions.filter(e => e.value !== 'all'); const options = filterOptions.filter(e => e.value !== 'all');
function handleChange(value) {
setDateRange(value);
}
function handleReset() { function handleReset() {
setDateRange(getDateRange(DEFAULT_DATE_RANGE, locale)); setDateRange(DEFAULT_DATE_RANGE);
} }
return ( return (
@ -25,7 +26,7 @@ export default function DateRangeSetting() {
value={value} value={value}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
onChange={setDateRange} onChange={handleChange}
/> />
<Button className={styles.button} size="small" onClick={handleReset}> <Button className={styles.button} size="small" onClick={handleReset}>
<FormattedMessage id="label.reset" defaultMessage="Reset" /> <FormattedMessage id="label.reset" defaultMessage="Reset" />

View File

@ -0,0 +1,31 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import DropDown from 'components/common/DropDown';
import Button from 'components/common/Button';
import useLocale from 'hooks/useLocale';
import { DEFAULT_LOCALE } from 'lib/constants';
import styles from './TimezoneSetting.module.css';
import { languages } from '../../lib/lang';
export default function LanguageSetting() {
const { locale, saveLocale } = useLocale();
const options = Object.keys(languages).map(key => ({ ...languages[key], value: key }));
function handleReset() {
saveLocale(DEFAULT_LOCALE);
}
return (
<>
<DropDown
menuClassName={styles.menu}
value={locale}
options={options}
onChange={saveLocale}
/>
<Button className={styles.button} size="small" onClick={handleReset}>
<FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button>
</>
);
}

View File

@ -1,6 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import Button from 'components/common/Button'; import Button from 'components/common/Button';
import Modal from 'components/common/Modal'; import Modal from 'components/common/Modal';
@ -11,12 +10,14 @@ import Dots from 'assets/ellipsis-h.svg';
import styles from './ProfileSettings.module.css'; import styles from './ProfileSettings.module.css';
import DateRangeSetting from './DateRangeSetting'; import DateRangeSetting from './DateRangeSetting';
import useEscapeKey from 'hooks/useEscapeKey'; import useEscapeKey from 'hooks/useEscapeKey';
import useUser from 'hooks/useUser';
import LanguageSetting from './LanguageSetting';
import ThemeSetting from './ThemeSetting';
export default function ProfileSettings() { export default function ProfileSettings() {
const user = useSelector(state => state.user); const { user } = useUser();
const [changePassword, setChangePassword] = useState(false); const [changePassword, setChangePassword] = useState(false);
const [message, setMessage] = useState(); const [message, setMessage] = useState(null);
const { user_id } = user;
function handleSave() { function handleSave() {
setChangePassword(false); setChangePassword(false);
@ -27,6 +28,12 @@ export default function ProfileSettings() {
setChangePassword(false); setChangePassword(false);
}); });
if (!user) {
return null;
}
const { user_id, username } = user;
return ( return (
<> <>
<PageHeader> <PageHeader>
@ -41,7 +48,7 @@ export default function ProfileSettings() {
<dt> <dt>
<FormattedMessage id="label.username" defaultMessage="Username" /> <FormattedMessage id="label.username" defaultMessage="Username" />
</dt> </dt>
<dd>{user.username}</dd> <dd>{username}</dd>
<dt> <dt>
<FormattedMessage id="label.timezone" defaultMessage="Timezone" /> <FormattedMessage id="label.timezone" defaultMessage="Timezone" />
</dt> </dt>
@ -54,6 +61,18 @@ export default function ProfileSettings() {
<dd> <dd>
<DateRangeSetting /> <DateRangeSetting />
</dd> </dd>
<dt>
<FormattedMessage id="label.language" defaultMessage="Language" />
</dt>
<dd>
<LanguageSetting />
</dd>
<dt>
<FormattedMessage id="label.theme" defaultMessage="Theme" />
</dt>
<dd>
<ThemeSetting />
</dd>
</dl> </dl>
{changePassword && ( {changePassword && (
<Modal <Modal

View File

@ -9,7 +9,8 @@ import Icon from '../common/Icon';
export default function ThemeButton() { export default function ThemeButton() {
const [theme, setTheme] = useTheme(); const [theme, setTheme] = useTheme();
const transitions = useTransition(theme, theme => theme, { const transitions = useTransition(theme, {
initial: { opacity: 1 },
from: { from: {
opacity: 0, opacity: 0,
transform: `translateY(${theme === 'light' ? '20px' : '-20px'}) scale(0.5)`, transform: `translateY(${theme === 'light' ? '20px' : '-20px'}) scale(0.5)`,
@ -27,17 +28,11 @@ export default function ThemeButton() {
return ( return (
<div className={styles.button} onClick={handleClick}> <div className={styles.button} onClick={handleClick}>
{transitions.map(({ item, key, props }) => {transitions((styles, item) => (
item === 'light' ? ( <animated.div key={item} style={styles}>
<animated.div key={key} style={props}> <Icon icon={item === 'light' ? <Sun /> : <Moon />} />
<Icon icon={<Sun />} />
</animated.div> </animated.div>
) : ( ))}
<animated.div key={key} style={props}>
<Icon icon={<Moon />} />
</animated.div>
),
)}
</div> </div>
); );
} }

View File

@ -0,0 +1,25 @@
import classNames from 'classnames';
import Button from 'components/common/Button';
import useTheme from 'hooks/useTheme';
import Sun from 'assets/sun.svg';
import Moon from 'assets/moon.svg';
import styles from './ThemeSetting.module.css';
export default function ThemeSetting() {
const [theme, setTheme] = useTheme();
return (
<div className={styles.buttons}>
<Button
className={classNames({ [styles.active]: theme === 'light' })}
icon={<Sun />}
onClick={() => setTheme('light')}
/>
<Button
className={classNames({ [styles.active]: theme === 'dark' })}
icon={<Moon />}
onClick={() => setTheme('dark')}
/>
</div>
);
}

View File

@ -0,0 +1,11 @@
.buttons {
display: flex;
}
.buttons button {
margin-right: 20px;
}
.active {
border: 1px solid var(--primary400);
}

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import MenuButton from 'components/common/MenuButton'; import MenuButton from 'components/common/MenuButton';
import Icon from 'components/common/Icon'; import Icon from 'components/common/Icon';
@ -9,9 +8,10 @@ import Chevron from 'assets/chevron-down.svg';
import styles from './UserButton.module.css'; import styles from './UserButton.module.css';
import { removeItem } from 'lib/web'; import { removeItem } from 'lib/web';
import { AUTH_TOKEN } from 'lib/constants'; import { AUTH_TOKEN } from 'lib/constants';
import useUser from 'hooks/useUser';
export default function UserButton() { export default function UserButton() {
const user = useSelector(state => state.user); const { user } = useUser();
const router = useRouter(); const router = useRouter();
const menuOptions = [ const menuOptions = [

View File

@ -1,10 +1,10 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useSelector } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
import Table from 'components/common/Table'; import Table from 'components/common/Table';
import Button from 'components/common/Button'; import Button from 'components/common/Button';
import OverflowText from 'components/common/OverflowText';
import PageHeader from 'components/layout/PageHeader'; import PageHeader from 'components/layout/PageHeader';
import Modal from 'components/common/Modal'; import Modal from 'components/common/Modal';
import WebsiteEditForm from 'components/forms/WebsiteEditForm'; import WebsiteEditForm from 'components/forms/WebsiteEditForm';
@ -23,10 +23,11 @@ import Plus from 'assets/plus.svg';
import Code from 'assets/code.svg'; import Code from 'assets/code.svg';
import LinkIcon from 'assets/link.svg'; import LinkIcon from 'assets/link.svg';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import useUser from 'hooks/useUser';
import styles from './WebsiteSettings.module.css'; import styles from './WebsiteSettings.module.css';
export default function WebsiteSettings() { export default function WebsiteSettings() {
const user = useSelector(state => state.user); const { user } = useUser();
const [editWebsite, setEditWebsite] = useState(); const [editWebsite, setEditWebsite] = useState();
const [resetWebsite, setResetWebsite] = useState(); const [resetWebsite, setResetWebsite] = useState();
const [deleteWebsite, setDeleteWebsite] = useState(); const [deleteWebsite, setDeleteWebsite] = useState();
@ -35,9 +36,7 @@ export default function WebsiteSettings() {
const [showUrl, setShowUrl] = useState(); const [showUrl, setShowUrl] = useState();
const [saved, setSaved] = useState(0); const [saved, setSaved] = useState(0);
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const { data } = useFetch(`/api/websites` + (user.is_admin ? '?include_all=true' : ''), {}, [ const { data } = useFetch('/websites', { params: { include_all: !!user?.is_admin } }, [saved]);
saved,
]);
const Buttons = row => ( const Buttons = row => (
<ButtonLayout align="right"> <ButtonLayout align="right">
@ -84,28 +83,37 @@ export default function WebsiteSettings() {
); );
const DetailsLink = ({ website_id, name, domain }) => ( const DetailsLink = ({ website_id, name, domain }) => (
<Link href="/website/[...id]" as={`/website/${website_id}/${name}`}> <Link
className={styles.detailLink}
href="/website/[...id]"
as={`/website/${website_id}/${name}`}
>
<Favicon domain={domain} /> <Favicon domain={domain} />
{name} <OverflowText tooltipId={`${website_id}-name`}>{name}</OverflowText>
</Link> </Link>
); );
const Domain = ({ domain, website_id }) => (
<OverflowText tooltipId={`${website_id}-domain`}>{domain}</OverflowText>
);
const adminColumns = [ const adminColumns = [
{ {
key: 'name', key: 'name',
label: <FormattedMessage id="label.name" defaultMessage="Name" />, label: <FormattedMessage id="label.name" defaultMessage="Name" />,
className: 'col-4 col-xl-3', className: 'col-12 col-lg-4 col-xl-3',
render: DetailsLink, render: DetailsLink,
}, },
{ {
key: 'domain', key: 'domain',
label: <FormattedMessage id="label.domain" defaultMessage="Domain" />, label: <FormattedMessage id="label.domain" defaultMessage="Domain" />,
className: 'col-4 col-xl-3', className: 'col-12 col-lg-4 col-xl-3',
render: Domain,
}, },
{ {
key: 'account', key: 'account',
label: <FormattedMessage id="label.owner" defaultMessage="Owner" />, label: <FormattedMessage id="label.owner" defaultMessage="Owner" />,
className: 'col-4 col-xl-1', className: 'col-12 col-lg-4 col-xl-1',
}, },
{ {
key: 'action', key: 'action',
@ -118,13 +126,14 @@ export default function WebsiteSettings() {
{ {
key: 'name', key: 'name',
label: <FormattedMessage id="label.name" defaultMessage="Name" />, label: <FormattedMessage id="label.name" defaultMessage="Name" />,
className: 'col-6 col-xl-4', className: 'col-12 col-lg-6 col-xl-4',
render: DetailsLink, render: DetailsLink,
}, },
{ {
key: 'domain', key: 'domain',
label: <FormattedMessage id="label.domain" defaultMessage="Domain" />, label: <FormattedMessage id="label.domain" defaultMessage="Domain" />,
className: 'col-6 col-xl-4', className: 'col-12 col-lg-6 col-xl-4',
render: Domain,
}, },
{ {
key: 'action', key: 'action',

View File

@ -3,5 +3,11 @@
} }
.buttons { .buttons {
display: flex;
justify-content: flex-end; justify-content: flex-end;
width: 100%;
}
.detailLink {
width: 100%;
} }

48
hooks/useApi.js Normal file
View File

@ -0,0 +1,48 @@
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import { get, post, put, del, getItem } from 'lib/web';
import { AUTH_TOKEN } from 'lib/constants';
function includeAuthToken(headers = {}) {
const authToken = getItem(AUTH_TOKEN);
if (authToken) {
headers.Authorization = `Bearer ${authToken}`;
}
return headers;
}
export default function useApi() {
const { basePath } = useRouter();
return {
get: useCallback(
async (url, params, headers) => {
return get(`${basePath}/api${url}`, params, includeAuthToken(headers));
},
[get],
),
post: useCallback(
async (url, params, headers) => {
return post(`${basePath}/api${url}`, params, includeAuthToken(headers));
},
[post],
),
put: useCallback(
async (url, params, headers) => {
return put(`${basePath}/api${url}`, params, includeAuthToken(headers));
},
[put],
),
del: useCallback(
async (url, params, headers) => {
return del(`${basePath}/api${url}`, params, includeAuthToken(headers));
},
[del],
),
};
}

View File

@ -1,17 +1,18 @@
import { useDispatch, useSelector } from 'react-redux'; import { useCallback, useMemo } from 'react';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { getDateRange } from 'lib/date'; import { getDateRange } from 'lib/date';
import { getItem, setItem } from 'lib/web'; import { getItem, setItem } from 'lib/web';
import { setDateRange } from '../redux/actions/websites';
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants'; import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
import useForceUpdate from './useForceUpdate'; import useForceUpdate from './useForceUpdate';
import useLocale from './useLocale'; import useLocale from './useLocale';
import useStore, { setDateRange } from 'store/websites';
export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_RANGE) { export default function useDateRange(websiteId) {
const dispatch = useDispatch();
const { locale } = useLocale(); const { locale } = useLocale();
const dateRange = useSelector(state => state.websites[websiteId]?.dateRange);
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
const selector = useCallback(state => state?.[websiteId]?.dateRange, [websiteId]);
const websiteDateRange = useStore(selector);
const defaultDateRange = useMemo(() => getDateRange(DEFAULT_DATE_RANGE, locale), [locale]);
const globalDefault = getItem(DATE_RANGE_CONFIG); const globalDefault = getItem(DATE_RANGE_CONFIG);
let globalDateRange; let globalDateRange;
@ -28,16 +29,14 @@ export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_
} }
} }
function saveDateRange(values) { function saveDateRange(dateRange) {
const { value } = values;
if (websiteId) { if (websiteId) {
dispatch(setDateRange(websiteId, values)); setDateRange(websiteId, dateRange);
} else { } else {
setItem(DATE_RANGE_CONFIG, value === 'custom' ? values : value); setItem(DATE_RANGE_CONFIG, dateRange);
forceUpdate(); forceUpdate();
} }
} }
return [dateRange || globalDateRange || getDateRange(defaultDateRange, locale), saveDateRange]; return [websiteDateRange || globalDateRange || defaultDateRange, saveDateRange];
} }

View File

@ -1,11 +0,0 @@
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import { del } from 'lib/web';
export default function useDelete() {
const { basePath } = useRouter();
return useCallback(async (url, params, headers) => {
return del(`${basePath}${url}`, params, headers);
}, []);
}

View File

@ -1,16 +1,13 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux'; import { saveQuery } from 'store/queries';
import { useRouter } from 'next/router'; import useApi from './useApi';
import { get } from 'lib/web';
import { updateQuery } from 'redux/actions/queries';
export default function useFetch(url, options = {}, update = []) { export default function useFetch(url, options = {}, update = []) {
const dispatch = useDispatch();
const [response, setResponse] = useState(); const [response, setResponse] = useState();
const [error, setError] = useState(); const [error, setError] = useState();
const [loading, setLoadiing] = useState(false); const [loading, setLoadiing] = useState(false);
const [count, setCount] = useState(0); const [count, setCount] = useState(0);
const { basePath } = useRouter(); const { get } = useApi();
const { params = {}, headers = {}, disabled, delay = 0, interval, onDataLoad } = options; const { params = {}, headers = {}, disabled, delay = 0, interval, onDataLoad } = options;
async function loadData(params) { async function loadData(params) {
@ -19,9 +16,9 @@ export default function useFetch(url, options = {}, update = []) {
setError(null); setError(null);
const time = performance.now(); const time = performance.now();
const { data, status, ok } = await get(`${basePath}${url}`, params, headers); const { data, status, ok } = await get(url, params, headers);
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() })); await saveQuery(url, { time: performance.now() - time, completed: Date.now() });
if (status >= 400) { if (status >= 400) {
setError(data); setError(data);

View File

@ -1,14 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
export default function useForceSSL(enabled) {
const router = useRouter();
useEffect(() => {
if (enabled && typeof window !== 'undefined' && /^http:\/\//.test(location.href)) {
router.push(location.href.replace(/^http:\/\//, 'https://'));
}
}, [enabled]);
return null;
}

View File

@ -1,10 +1,9 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setLocale } from 'redux/actions/app';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { get, setItem } from 'lib/web'; import { get, setItem } from 'lib/web';
import { LOCALE_CONFIG } from 'lib/constants'; import { LOCALE_CONFIG } from 'lib/constants';
import { getDateLocale, getTextDirection } from 'lib/lang'; import { getDateLocale, getTextDirection } from 'lib/lang';
import useStore, { setLocale } from 'store/app';
import useForceUpdate from 'hooks/useForceUpdate'; import useForceUpdate from 'hooks/useForceUpdate';
import enUS from 'public/messages/en-US.json'; import enUS from 'public/messages/en-US.json';
@ -12,9 +11,10 @@ const messages = {
'en-US': enUS, 'en-US': enUS,
}; };
const selector = state => state.locale;
export default function useLocale() { export default function useLocale() {
const locale = useSelector(state => state.app.locale); const locale = useStore(selector);
const dispatch = useDispatch();
const { basePath } = useRouter(); const { basePath } = useRouter();
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
const dir = getTextDirection(locale); const dir = getTextDirection(locale);
@ -36,7 +36,7 @@ export default function useLocale() {
setItem(LOCALE_CONFIG, value); setItem(LOCALE_CONFIG, value);
if (locale !== value) { if (locale !== value) {
dispatch(setLocale(value)); setLocale(value);
} else { } else {
forceUpdate(); forceUpdate();
} }

View File

@ -1,11 +0,0 @@
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import { post } from 'lib/web';
export default function usePost() {
const { basePath } = useRouter();
return useCallback(async (url, params, headers) => {
return post(`${basePath}${url}`, params, headers);
}, []);
}

View File

@ -1,38 +1,36 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { updateUser } from 'redux/actions/user';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { get } from 'lib/web'; import useUser from 'hooks/useUser';
import useApi from 'hooks/useApi';
export default function useRequireLogin() { export default function useRequireLogin() {
const router = useRouter(); const router = useRouter();
const dispatch = useDispatch(); const { get } = useApi();
const storeUser = useSelector(state => state.user); const { user, setUser } = useUser();
const [loading, setLoading] = useState(!storeUser); const [loading, setLoading] = useState(false);
const [user, setUser] = useState(storeUser);
async function loadUser() { async function loadUser() {
setLoading(true); setLoading(true);
const { ok, data } = await get(`${router.basePath}/api/auth/verify`); const { ok, data } = await get('/auth/verify');
if (!ok) { if (!ok) {
return router.push('/login'); await router.push('/login');
return null;
} }
await dispatch(updateUser(data)); setUser(data);
setUser(user);
setLoading(false); setLoading(false);
} }
useEffect(() => { useEffect(() => {
if (!loading && user) { if (loading || user) {
return; return;
} }
loadUser(); loadUser();
}, []); }, [user, loading]);
return { user, loading }; return { user, loading };
} }

View File

@ -1,19 +1,18 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import useStore, { setShareToken } from 'store/app';
import { useRouter } from 'next/router'; import useApi from './useApi';
import { get } from 'lib/web';
import { setShareToken } from 'redux/actions/app'; const selector = state => state.shareToken;
export default function useShareToken(shareId) { export default function useShareToken(shareId) {
const { basePath } = useRouter(); const shareToken = useStore(selector);
const dispatch = useDispatch(); const { get } = useApi();
const shareToken = useSelector(state => state.app.shareToken);
async function loadToken(id) { async function loadToken(id) {
const { data } = await get(`${basePath}/api/share/${id}`); const { data } = await get(`/share/${id}`);
if (data) { if (data) {
dispatch(setShareToken(data)); setShareToken(data);
} }
} }

View File

@ -1,8 +1,9 @@
import { useDispatch, useSelector } from 'react-redux'; import { useEffect } from 'react';
import { setTheme } from 'redux/actions/app'; import useStore, { setTheme } from 'store/app';
import { getItem, setItem } from 'lib/web'; import { getItem, setItem } from 'lib/web';
import { THEME_CONFIG } from 'lib/constants'; import { THEME_CONFIG } from 'lib/constants';
import { useEffect } from 'react';
const selector = state => state.theme;
export default function useTheme() { export default function useTheme() {
const defaultTheme = const defaultTheme =
@ -11,12 +12,11 @@ export default function useTheme() {
? 'dark' ? 'dark'
: 'light' : 'light'
: 'light'; : 'light';
const theme = useSelector(state => state.app.theme || getItem(THEME_CONFIG) || defaultTheme); const theme = useStore(selector) || getItem(THEME_CONFIG) || defaultTheme;
const dispatch = useDispatch();
function saveTheme(value) { function saveTheme(value) {
setItem(THEME_CONFIG, value); setItem(THEME_CONFIG, value);
dispatch(setTheme(value)); setTheme(value);
} }
useEffect(() => { useEffect(() => {

9
hooks/useUser.js Normal file
View File

@ -0,0 +1,9 @@
import useStore, { setUser } from 'store/app';
const selector = state => state.user;
export default function useUser() {
const user = useStore(selector);
return { user, setUser };
}

View File

@ -1,12 +1,10 @@
import { useEffect, useCallback } from 'react'; import { useEffect, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import useStore, { checkVersion } from 'store/version';
import { checkVersion } from 'redux/actions/app';
import { VERSION_CHECK } from 'lib/constants'; import { VERSION_CHECK } from 'lib/constants';
import { getItem, setItem } from 'lib/web'; import { getItem, setItem } from 'lib/web';
export default function useVersion(check) { export default function useVersion(check) {
const dispatch = useDispatch(); const versions = useStore();
const versions = useSelector(state => state.app.versions);
const checked = versions.latest === getItem(VERSION_CHECK)?.version; const checked = versions.latest === getItem(VERSION_CHECK)?.version;
const updateCheck = useCallback(() => { const updateCheck = useCallback(() => {
@ -15,7 +13,7 @@ export default function useVersion(check) {
useEffect(() => { useEffect(() => {
if (check && !versions.latest) { if (check && !versions.latest) {
dispatch(checkVersion()); checkVersion();
} }
}, [versions, check]); }, [versions, check]);

View File

@ -24,5 +24,13 @@
"metrics.device.desktop", "metrics.device.desktop",
"metrics.device.laptop", "metrics.device.laptop",
"metrics.device.tablet" "metrics.device.tablet"
] ],
"it-IT": [
"label.password",
"label.reset",
"message.powered-by",
"metrics.device.desktop",
"metrics.device.tablet",
"metrics.filter.raw",
],
} }

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "تفعيل مشاركة الرابط", "label.enable-share-url": "تفعيل مشاركة الرابط",
"label.invalid": "غير صحيح", "label.invalid": "غير صحيح",
"label.invalid-domain": "النطاق غير صحيح", "label.invalid-domain": "النطاق غير صحيح",
"label.language": "Language",
"label.last-days": "اخر {x} يوم/ايام", "label.last-days": "اخر {x} يوم/ايام",
"label.last-hours": "اخر {x} ساعة/ساعات", "label.last-hours": "اخر {x} ساعة/ساعات",
"label.logged-in-as": "تم تسجيل الدخول كـ {username}", "label.logged-in-as": "تم تسجيل الدخول كـ {username}",
@ -50,6 +51,7 @@
"label.settings": "اعدادات", "label.settings": "اعدادات",
"label.share-url": "مشاركة الرابط", "label.share-url": "مشاركة الرابط",
"label.single-day": "يوم واحد", "label.single-day": "يوم واحد",
"label.theme": "Theme",
"label.this-month": "الشهر الحالي", "label.this-month": "الشهر الحالي",
"label.this-week": "الاسبوع الحالي", "label.this-week": "الاسبوع الحالي",
"label.this-year": "السنة الحالية", "label.this-year": "السنة الحالية",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Activa l'enllaç per compartir", "label.enable-share-url": "Activa l'enllaç per compartir",
"label.invalid": "Invàlid", "label.invalid": "Invàlid",
"label.invalid-domain": "Domini invàlid", "label.invalid-domain": "Domini invàlid",
"label.language": "Language",
"label.last-days": "Últims {x} dies", "label.last-days": "Últims {x} dies",
"label.last-hours": "Últimes {x} hores", "label.last-hours": "Últimes {x} hores",
"label.logged-in-as": "Connectat com {username}", "label.logged-in-as": "Connectat com {username}",
@ -50,6 +51,7 @@
"label.settings": "Configuració", "label.settings": "Configuració",
"label.share-url": "Enllaç per compartir", "label.share-url": "Enllaç per compartir",
"label.single-day": "Un sol dia", "label.single-day": "Un sol dia",
"label.theme": "Theme",
"label.this-month": "Aquest mes", "label.this-month": "Aquest mes",
"label.this-week": "Aquesta setmana", "label.this-week": "Aquesta setmana",
"label.this-year": "Aquest any", "label.this-year": "Aquest any",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Povolit sdílení URL", "label.enable-share-url": "Povolit sdílení URL",
"label.invalid": "Neplatný", "label.invalid": "Neplatný",
"label.invalid-domain": "Neplatná doména", "label.invalid-domain": "Neplatná doména",
"label.language": "Language",
"label.last-days": "Posledních {x} dnů", "label.last-days": "Posledních {x} dnů",
"label.last-hours": "Posledních {x} hodin", "label.last-hours": "Posledních {x} hodin",
"label.logged-in-as": "Přihlášený jako {username}", "label.logged-in-as": "Přihlášený jako {username}",
@ -50,6 +51,7 @@
"label.settings": "Nastavení", "label.settings": "Nastavení",
"label.share-url": "Sdílet URL", "label.share-url": "Sdílet URL",
"label.single-day": "Jeden den", "label.single-day": "Jeden den",
"label.theme": "Theme",
"label.this-month": "Tento měsíc", "label.this-month": "Tento měsíc",
"label.this-week": "Tento týden", "label.this-week": "Tento týden",
"label.this-year": "Tento rok", "label.this-year": "Tento rok",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Aktivér delings-URL", "label.enable-share-url": "Aktivér delings-URL",
"label.invalid": "Ugyldig", "label.invalid": "Ugyldig",
"label.invalid-domain": "Ugyldigt domæne", "label.invalid-domain": "Ugyldigt domæne",
"label.language": "Language",
"label.last-days": "Sidste {x} dage", "label.last-days": "Sidste {x} dage",
"label.last-hours": "Sidste {x} timer", "label.last-hours": "Sidste {x} timer",
"label.logged-in-as": "Loggede ind som {username}", "label.logged-in-as": "Loggede ind som {username}",
@ -50,6 +51,7 @@
"label.settings": "Indstillinger", "label.settings": "Indstillinger",
"label.share-url": "Del URL", "label.share-url": "Del URL",
"label.single-day": "Enkelt dag", "label.single-day": "Enkelt dag",
"label.theme": "Theme",
"label.this-month": "Denne måned", "label.this-month": "Denne måned",
"label.this-week": "Denne uge", "label.this-week": "Denne uge",
"label.this-year": "Dette år", "label.this-year": "Dette år",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Freigabe-URL aktivieren", "label.enable-share-url": "Freigabe-URL aktivieren",
"label.invalid": "Ungültig", "label.invalid": "Ungültig",
"label.invalid-domain": "Ungültige Domain", "label.invalid-domain": "Ungültige Domain",
"label.language": "Language",
"label.last-days": "Letzten {x} Tage", "label.last-days": "Letzten {x} Tage",
"label.last-hours": "Letzten {x} Stunden", "label.last-hours": "Letzten {x} Stunden",
"label.logged-in-as": "Angemeldet als {username}", "label.logged-in-as": "Angemeldet als {username}",
@ -50,6 +51,7 @@
"label.settings": "Einstellungen", "label.settings": "Einstellungen",
"label.share-url": "Freigabe-URL", "label.share-url": "Freigabe-URL",
"label.single-day": "Ein Tag", "label.single-day": "Ein Tag",
"label.theme": "Theme",
"label.this-month": "Diesen Monat", "label.this-month": "Diesen Monat",
"label.this-week": "Diese Woche", "label.this-week": "Diese Woche",
"label.this-year": "Dieses Jahr", "label.this-year": "Dieses Jahr",
@ -76,7 +78,7 @@
"message.no-websites-configured": "Es ist keine Webseite vorhanden.", "message.no-websites-configured": "Es ist keine Webseite vorhanden.",
"message.page-not-found": "Seite nicht gefunden.", "message.page-not-found": "Seite nicht gefunden.",
"message.powered-by": "Betrieben durch {name}", "message.powered-by": "Betrieben durch {name}",
"message.reset-warning": "Alle Daten für diese Website werden gelöscht, jedoch bleibt der tracking code bestehen.", "message.reset-warning": "Alle Daten für diese Webseite werden gelöscht, jedoch bleibt der Tracking Code bestehen.",
"message.save-success": "Erfolgreich gespeichert.", "message.save-success": "Erfolgreich gespeichert.",
"message.share-url": "Dies ist die öffentliche URL zum Teilen für {target}.", "message.share-url": "Dies ist die öffentliche URL zum Teilen für {target}.",
"message.toggle-charts": "Schaubilder umschalten", "message.toggle-charts": "Schaubilder umschalten",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Ενεργοποίηση κοινής χρήσης URL", "label.enable-share-url": "Ενεργοποίηση κοινής χρήσης URL",
"label.invalid": "Μη έγκυρο", "label.invalid": "Μη έγκυρο",
"label.invalid-domain": "Μη έγκυρος τομέας", "label.invalid-domain": "Μη έγκυρος τομέας",
"label.language": "Language",
"label.last-days": "Τελευταίες {x} ημέρες", "label.last-days": "Τελευταίες {x} ημέρες",
"label.last-hours": "Τελευταίες {x} ώρες", "label.last-hours": "Τελευταίες {x} ώρες",
"label.logged-in-as": "Συνδεθήκατε ως {username}", "label.logged-in-as": "Συνδεθήκατε ως {username}",
@ -50,6 +51,7 @@
"label.settings": "Ρυθμίσεις", "label.settings": "Ρυθμίσεις",
"label.share-url": "Κοινοποίηση διεύθυνσης URL", "label.share-url": "Κοινοποίηση διεύθυνσης URL",
"label.single-day": "Ημερήσια", "label.single-day": "Ημερήσια",
"label.theme": "Theme",
"label.this-month": "Αυτο το μήνα", "label.this-month": "Αυτο το μήνα",
"label.this-week": "Αυτή την εβδομάδα", "label.this-week": "Αυτή την εβδομάδα",
"label.this-year": "Αυτή την χρονιά", "label.this-year": "Αυτή την χρονιά",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Enable share URL", "label.enable-share-url": "Enable share URL",
"label.invalid": "Invalid", "label.invalid": "Invalid",
"label.invalid-domain": "Invalid domain", "label.invalid-domain": "Invalid domain",
"label.language": "Language",
"label.last-days": "Last {x} days", "label.last-days": "Last {x} days",
"label.last-hours": "Last {x} hours", "label.last-hours": "Last {x} hours",
"label.logged-in-as": "Logged in as {username}", "label.logged-in-as": "Logged in as {username}",
@ -50,6 +51,7 @@
"label.settings": "Settings", "label.settings": "Settings",
"label.share-url": "Share URL", "label.share-url": "Share URL",
"label.single-day": "Single day", "label.single-day": "Single day",
"label.theme": "Theme",
"label.this-month": "This month", "label.this-month": "This month",
"label.this-week": "This week", "label.this-week": "This week",
"label.this-year": "This year", "label.this-year": "This year",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Enable share URL", "label.enable-share-url": "Enable share URL",
"label.invalid": "Invalid", "label.invalid": "Invalid",
"label.invalid-domain": "Invalid domain", "label.invalid-domain": "Invalid domain",
"label.language": "Language",
"label.last-days": "Last {x} days", "label.last-days": "Last {x} days",
"label.last-hours": "Last {x} hours", "label.last-hours": "Last {x} hours",
"label.logged-in-as": "Logged in as {username}", "label.logged-in-as": "Logged in as {username}",
@ -50,6 +51,7 @@
"label.settings": "Settings", "label.settings": "Settings",
"label.share-url": "Share URL", "label.share-url": "Share URL",
"label.single-day": "Single day", "label.single-day": "Single day",
"label.theme": "Theme",
"label.this-month": "This month", "label.this-month": "This month",
"label.this-week": "This week", "label.this-week": "This week",
"label.this-year": "This year", "label.this-year": "This year",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Habilitar compartir URL", "label.enable-share-url": "Habilitar compartir URL",
"label.invalid": "Inválido", "label.invalid": "Inválido",
"label.invalid-domain": "Dominio inválido", "label.invalid-domain": "Dominio inválido",
"label.language": "Language",
"label.last-days": "Últimos {x} días", "label.last-days": "Últimos {x} días",
"label.last-hours": "Últimas {x} horas", "label.last-hours": "Últimas {x} horas",
"label.logged-in-as": "Sesión iniciada como {username}", "label.logged-in-as": "Sesión iniciada como {username}",
@ -50,6 +51,7 @@
"label.settings": "Configuraciones", "label.settings": "Configuraciones",
"label.share-url": "Compartir URL", "label.share-url": "Compartir URL",
"label.single-day": "Dia", "label.single-day": "Dia",
"label.theme": "Theme",
"label.this-month": "Este mes", "label.this-month": "Este mes",
"label.this-week": "Esta semana", "label.this-week": "Esta semana",
"label.this-year": "Este año", "label.this-year": "Este año",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "فعال کردن اشتراک گذاری URL", "label.enable-share-url": "فعال کردن اشتراک گذاری URL",
"label.invalid": "نامعتبر", "label.invalid": "نامعتبر",
"label.invalid-domain": "دامنه‌ی نامعتبر", "label.invalid-domain": "دامنه‌ی نامعتبر",
"label.language": "Language",
"label.last-days": "لیست {x} روز", "label.last-days": "لیست {x} روز",
"label.last-hours": "لیست {x} ساعت", "label.last-hours": "لیست {x} ساعت",
"label.logged-in-as": "وارد شده به عنوان {username}", "label.logged-in-as": "وارد شده به عنوان {username}",
@ -50,6 +51,7 @@
"label.settings": "تنظیمات", "label.settings": "تنظیمات",
"label.share-url": "به اشتراک گذاری URL", "label.share-url": "به اشتراک گذاری URL",
"label.single-day": "یک روز", "label.single-day": "یک روز",
"label.theme": "Theme",
"label.this-month": "این ماه", "label.this-month": "این ماه",
"label.this-week": "این هفته", "label.this-week": "این هفته",
"label.this-year": "امسال", "label.this-year": "امسال",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Ota jakamisen URL-osoite käyttöön", "label.enable-share-url": "Ota jakamisen URL-osoite käyttöön",
"label.invalid": "Virheellinen", "label.invalid": "Virheellinen",
"label.invalid-domain": "Virheellinen verkkotunnus", "label.invalid-domain": "Virheellinen verkkotunnus",
"label.language": "Language",
"label.last-days": "Viimeisimmät {x} päivät", "label.last-days": "Viimeisimmät {x} päivät",
"label.last-hours": "Viimeisimmät {x} tunnit", "label.last-hours": "Viimeisimmät {x} tunnit",
"label.logged-in-as": "Kirjautuneena sisään nimellä {username}", "label.logged-in-as": "Kirjautuneena sisään nimellä {username}",
@ -50,6 +51,7 @@
"label.settings": "Asetukset", "label.settings": "Asetukset",
"label.share-url": "Jaa URL", "label.share-url": "Jaa URL",
"label.single-day": "Yksi päivä", "label.single-day": "Yksi päivä",
"label.theme": "Theme",
"label.this-month": "Tämä kuukausi", "label.this-month": "Tämä kuukausi",
"label.this-week": "Tämä viikko", "label.this-week": "Tämä viikko",
"label.this-year": "Tämä vuosi", "label.this-year": "Tämä vuosi",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Virkja deili leinki", "label.enable-share-url": "Virkja deili leinki",
"label.invalid": "Ógilda", "label.invalid": "Ógilda",
"label.invalid-domain": "Ógilt økisnavn", "label.invalid-domain": "Ógilt økisnavn",
"label.language": "Language",
"label.last-days": "Seinastu {x} dagarnar", "label.last-days": "Seinastu {x} dagarnar",
"label.last-hours": "Seinastu {x} tímarnar", "label.last-hours": "Seinastu {x} tímarnar",
"label.logged-in-as": "Ritaður inn sum {username}", "label.logged-in-as": "Ritaður inn sum {username}",
@ -50,6 +51,7 @@
"label.settings": "Stillingar", "label.settings": "Stillingar",
"label.share-url": "Deil leinku", "label.share-url": "Deil leinku",
"label.single-day": "Einkultur dagur", "label.single-day": "Einkultur dagur",
"label.theme": "Theme",
"label.this-month": "Hendan mánan", "label.this-month": "Hendan mánan",
"label.this-week": "Hesa vikuna", "label.this-week": "Hesa vikuna",
"label.this-year": "Hetta árið", "label.this-year": "Hetta árið",

View File

@ -5,7 +5,7 @@
"label.administrator": "Administrateur", "label.administrator": "Administrateur",
"label.all": "Tout", "label.all": "Tout",
"label.all-events": "Tous les événements", "label.all-events": "Tous les événements",
"label.all-time": "All time", "label.all-time": "Toutes périodes",
"label.all-websites": "Tous les sites web", "label.all-websites": "Tous les sites web",
"label.back": "Retour", "label.back": "Retour",
"label.cancel": "Annuler", "label.cancel": "Annuler",
@ -28,6 +28,7 @@
"label.enable-share-url": "Activer le partage d'URL", "label.enable-share-url": "Activer le partage d'URL",
"label.invalid": "Invalide", "label.invalid": "Invalide",
"label.invalid-domain": "Domaine invalide", "label.invalid-domain": "Domaine invalide",
"label.language": "Language",
"label.last-days": "{x} derniers jours", "label.last-days": "{x} derniers jours",
"label.last-hours": "{x} dernières heures", "label.last-hours": "{x} dernières heures",
"label.logged-in-as": "Connecté en tant que {username}", "label.logged-in-as": "Connecté en tant que {username}",
@ -36,7 +37,7 @@
"label.more": "Plus", "label.more": "Plus",
"label.name": "Nom", "label.name": "Nom",
"label.new-password": "Nouveau mot de passe", "label.new-password": "Nouveau mot de passe",
"label.owner": "Owner", "label.owner": "Propriétaire",
"label.password": "Mot de passe", "label.password": "Mot de passe",
"label.passwords-dont-match": "Les mots de passe ne correspondent pas", "label.passwords-dont-match": "Les mots de passe ne correspondent pas",
"label.profile": "Profil", "label.profile": "Profil",
@ -45,11 +46,12 @@
"label.refresh": "Rafraîchir", "label.refresh": "Rafraîchir",
"label.required": "Requis", "label.required": "Requis",
"label.reset": "Réinitialiser", "label.reset": "Réinitialiser",
"label.reset-website": "Reset statistics", "label.reset-website": "Réinitialiser les statistiques",
"label.save": "Sauvegarder", "label.save": "Sauvegarder",
"label.settings": "Paramètres", "label.settings": "Paramètres",
"label.share-url": "Partager l'URL", "label.share-url": "Partager l'URL",
"label.single-day": "Journée", "label.single-day": "Journée",
"label.theme": "Theme",
"label.this-month": "Ce mois ci", "label.this-month": "Ce mois ci",
"label.this-week": "Cette semaine", "label.this-week": "Cette semaine",
"label.this-year": "Cette année", "label.this-year": "Cette année",
@ -62,7 +64,7 @@
"label.websites": "Sites", "label.websites": "Sites",
"message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement", "message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement",
"message.confirm-delete": "Êtes-vous sûr de vouloir supprimer {target} ?", "message.confirm-delete": "Êtes-vous sûr de vouloir supprimer {target} ?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", "message.confirm-reset": "Êtes-vous sûr de vouloir réinistialiser les statistiques de {target} ?",
"message.copied": "Copié !", "message.copied": "Copié !",
"message.delete-warning": "Toutes les données associées seront également supprimées.", "message.delete-warning": "Toutes les données associées seront également supprimées.",
"message.failure": "Un problème est survenu.", "message.failure": "Un problème est survenu.",
@ -76,13 +78,13 @@
"message.no-websites-configured": "Vous n'avez configuré aucun site Web.", "message.no-websites-configured": "Vous n'avez configuré aucun site Web.",
"message.page-not-found": "Page non trouvée.", "message.page-not-found": "Page non trouvée.",
"message.powered-by": "Propulsé par {name}", "message.powered-by": "Propulsé par {name}",
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.", "message.reset-warning": "Toutes les statistiques pour ce site seront supprimés, mais votre code de suivi restera intact.",
"message.save-success": "Enregistré avec succès.", "message.save-success": "Enregistré avec succès.",
"message.share-url": "Ceci est l'URL partagée pour {target}.", "message.share-url": "Ceci est l'URL partagée pour {target}.",
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Changer les graphiques",
"message.track-stats": "Pour suivre les statistiques de {target}, placez le code suivant dans la section {head} de votre site Web.", "message.track-stats": "Pour suivre les statistiques de {target}, placez le code suivant dans la section {head} de votre site Web.",
"message.type-delete": "Tapez {delete} dans la case ci-dessous pour confirmer.", "message.type-delete": "Tapez {delete} dans la case ci-dessous pour confirmer.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Tapez {reset} dans la case ci-dessous pour confirmer.",
"metrics.actions": "Actions", "metrics.actions": "Actions",
"metrics.average-visit-time": "Temps de visite moyen", "metrics.average-visit-time": "Temps de visite moyen",
"metrics.bounce-rate": "Taux de rebond", "metrics.bounce-rate": "Taux de rebond",
@ -97,7 +99,7 @@
"metrics.filter.combined": "Combiné", "metrics.filter.combined": "Combiné",
"metrics.filter.domain-only": "Domaine uniquement", "metrics.filter.domain-only": "Domaine uniquement",
"metrics.filter.raw": "Brute", "metrics.filter.raw": "Brute",
"metrics.languages": "Languages", "metrics.languages": "Langages",
"metrics.operating-systems": "Systèmes d'exploitation", "metrics.operating-systems": "Systèmes d'exploitation",
"metrics.page-views": "Pages vues", "metrics.page-views": "Pages vues",
"metrics.pages": "Pages", "metrics.pages": "Pages",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "הפעלת URL שיתוף", "label.enable-share-url": "הפעלת URL שיתוף",
"label.invalid": "לא תקין", "label.invalid": "לא תקין",
"label.invalid-domain": "דומיין לא תקין", "label.invalid-domain": "דומיין לא תקין",
"label.language": "Language",
"label.last-days": "{x} ימים אחרונים", "label.last-days": "{x} ימים אחרונים",
"label.last-hours": "{x} שעות אחרונות", "label.last-hours": "{x} שעות אחרונות",
"label.logged-in-as": "מחובר כ-{username}", "label.logged-in-as": "מחובר כ-{username}",
@ -50,6 +51,7 @@
"label.settings": "הגדרות", "label.settings": "הגדרות",
"label.share-url": "שיתוף URL", "label.share-url": "שיתוף URL",
"label.single-day": "יום בודד", "label.single-day": "יום בודד",
"label.theme": "Theme",
"label.this-month": "החודש", "label.this-month": "החודש",
"label.this-week": "השבוע", "label.this-week": "השבוע",
"label.this-year": "השנה", "label.this-year": "השנה",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "शेयर URL सक्षम करें", "label.enable-share-url": "शेयर URL सक्षम करें",
"label.invalid": "अमान्य", "label.invalid": "अमान्य",
"label.invalid-domain": "अमान्य डोमेन", "label.invalid-domain": "अमान्य डोमेन",
"label.language": "Language",
"label.last-days": "पिछले {x} दिन", "label.last-days": "पिछले {x} दिन",
"label.last-hours": "पिछले {x} घंटे", "label.last-hours": "पिछले {x} घंटे",
"label.logged-in-as": "{x} के रूप में लॉग इन किया", "label.logged-in-as": "{x} के रूप में लॉग इन किया",
@ -50,6 +51,7 @@
"label.settings": "समायोजन", "label.settings": "समायोजन",
"label.share-url": "यूआरएल साझा करें", "label.share-url": "यूआरएल साझा करें",
"label.single-day": "एक दिन", "label.single-day": "एक दिन",
"label.theme": "Theme",
"label.this-month": "इस महीने", "label.this-month": "इस महीने",
"label.this-week": "इस सप्ताह", "label.this-week": "इस सप्ताह",
"label.this-year": "इस साल", "label.this-year": "इस साल",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "URL-megosztás engedélyezése", "label.enable-share-url": "URL-megosztás engedélyezése",
"label.invalid": "Érvénytelen", "label.invalid": "Érvénytelen",
"label.invalid-domain": "Érvénytelen domain", "label.invalid-domain": "Érvénytelen domain",
"label.language": "Language",
"label.last-days": "Legutóbbi {x} nap", "label.last-days": "Legutóbbi {x} nap",
"label.last-hours": "Legutóbbi {x} óra", "label.last-hours": "Legutóbbi {x} óra",
"label.logged-in-as": "Bejelentkezve, mint {username}", "label.logged-in-as": "Bejelentkezve, mint {username}",
@ -50,6 +51,7 @@
"label.settings": "Beállítások", "label.settings": "Beállítások",
"label.share-url": "URL megosztása", "label.share-url": "URL megosztása",
"label.single-day": "Egy nap", "label.single-day": "Egy nap",
"label.theme": "Theme",
"label.this-month": "Ezen hónap", "label.this-month": "Ezen hónap",
"label.this-week": "Ezen hét", "label.this-week": "Ezen hét",
"label.this-year": "Ezen év", "label.this-year": "Ezen év",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Aktifkan URL berbagi", "label.enable-share-url": "Aktifkan URL berbagi",
"label.invalid": "Tidak valid", "label.invalid": "Tidak valid",
"label.invalid-domain": "Domain tidak valid", "label.invalid-domain": "Domain tidak valid",
"label.language": "Language",
"label.last-days": "{x} hari terakhir", "label.last-days": "{x} hari terakhir",
"label.last-hours": "{x} jam terakhir", "label.last-hours": "{x} jam terakhir",
"label.logged-in-as": "Masuk sebagai {username}", "label.logged-in-as": "Masuk sebagai {username}",
@ -50,6 +51,7 @@
"label.settings": "Pengaturan", "label.settings": "Pengaturan",
"label.share-url": "Bagikan URL", "label.share-url": "Bagikan URL",
"label.single-day": "Sehari", "label.single-day": "Sehari",
"label.theme": "Theme",
"label.this-month": "Bulan ini", "label.this-month": "Bulan ini",
"label.this-week": "Minggu ini", "label.this-week": "Minggu ini",
"label.this-year": "Tahun ini", "label.this-year": "Tahun ini",

View File

@ -4,8 +4,8 @@
"label.add-website": "Aggiungi sito", "label.add-website": "Aggiungi sito",
"label.administrator": "Amministratore", "label.administrator": "Amministratore",
"label.all": "Tutto", "label.all": "Tutto",
"label.all-events": "All events", "label.all-events": "Tutti gli eventi",
"label.all-time": "All time", "label.all-time": "Sempre",
"label.all-websites": "Tutti i siti web", "label.all-websites": "Tutti i siti web",
"label.back": "Indietro", "label.back": "Indietro",
"label.cancel": "Annulla", "label.cancel": "Annulla",
@ -14,7 +14,7 @@
"label.copy-to-clipboard": "Copia", "label.copy-to-clipboard": "Copia",
"label.current-password": "Password corrente", "label.current-password": "Password corrente",
"label.custom-range": "Personalizzato", "label.custom-range": "Personalizzato",
"label.dashboard": "Dashboard", "label.dashboard": "Pannello di Controllo",
"label.date-range": "Periodo", "label.date-range": "Periodo",
"label.default-date-range": "Periodo standard", "label.default-date-range": "Periodo standard",
"label.delete": "Elimina", "label.delete": "Elimina",
@ -28,28 +28,30 @@
"label.enable-share-url": "Abilita URL di condivisione", "label.enable-share-url": "Abilita URL di condivisione",
"label.invalid": "Non valido", "label.invalid": "Non valido",
"label.invalid-domain": "Dominio non valido", "label.invalid-domain": "Dominio non valido",
"label.language": "Language",
"label.last-days": "Ultimi {x} giorni", "label.last-days": "Ultimi {x} giorni",
"label.last-hours": "Ultime {x} ore", "label.last-hours": "Ultime {x} ore",
"label.logged-in-as": "Ciao {username}", "label.logged-in-as": "Ciao {username}",
"label.login": "Login", "label.login": "Accedi",
"label.logout": "Logout", "label.logout": "Esci",
"label.more": "Dettagli", "label.more": "Dettagli",
"label.name": "Nome", "label.name": "Nome",
"label.new-password": "Nuova password", "label.new-password": "Nuova password",
"label.owner": "Owner", "label.owner": "Proprietario",
"label.password": "Password", "label.password": "Password",
"label.passwords-dont-match": "Le password non corrispondono", "label.passwords-dont-match": "Le password non corrispondono",
"label.profile": "Profilo", "label.profile": "Profilo",
"label.realtime": "Realtime", "label.realtime": "Tempo reale",
"label.realtime-logs": "Log in realtime", "label.realtime-logs": "Log in tempo reale",
"label.refresh": "Ricarica", "label.refresh": "Ricarica",
"label.required": "Obbligatorio", "label.required": "Obbligatorio",
"label.reset": "Reset", "label.reset": "Reset",
"label.reset-website": "Reset statistics", "label.reset-website": "Resetta le statistiche",
"label.save": "Salva", "label.save": "Salva",
"label.settings": "Impostazioni", "label.settings": "Impostazioni",
"label.share-url": "Share URL", "label.share-url": "Condividi link",
"label.single-day": "Singolo giorno", "label.single-day": "Singolo giorno",
"label.theme": "Theme",
"label.this-month": "Questo mese", "label.this-month": "Questo mese",
"label.this-week": "Questa settimana", "label.this-week": "Questa settimana",
"label.this-year": "Quest'anno", "label.this-year": "Quest'anno",
@ -57,7 +59,7 @@
"label.today": "Oggi", "label.today": "Oggi",
"label.tracking-code": "Codice di tracking", "label.tracking-code": "Codice di tracking",
"label.unknown": "Sconosciuto", "label.unknown": "Sconosciuto",
"label.username": "Username", "label.username": "Nome utente",
"label.view-details": "Vedi dettagli", "label.view-details": "Vedi dettagli",
"label.websites": "Siti web", "label.websites": "Siti web",
"message.active-users": "{x} {x, plural, one {visitatore} other {visitatori}} online", "message.active-users": "{x} {x, plural, one {visitatore} other {visitatori}} online",
@ -76,28 +78,28 @@
"message.no-websites-configured": "Non hai ancora configurato alcun sito.", "message.no-websites-configured": "Non hai ancora configurato alcun sito.",
"message.page-not-found": "Pagina non trovata", "message.page-not-found": "Pagina non trovata",
"message.powered-by": "Powered by {name}", "message.powered-by": "Powered by {name}",
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.", "message.reset-warning": "Tutte le statistiche verranno cancellate per questo sito, ma il tuo codice di tracciamento rimarrà invariato.",
"message.save-success": "Salvato!", "message.save-success": "Salvato!",
"message.share-url": "Questo è l'URL di condivisione per {target}.", "message.share-url": "Questo è l'URL di condivisione per {target}.",
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Apri/Chiudi i grafici",
"message.track-stats": "Per tracciare le statistiche di {target}, inserisci questo codice nella sezione {head} del tuo sito web.", "message.track-stats": "Per tracciare le statistiche di {target}, inserisci questo codice nella sezione {head} del tuo sito web.",
"message.type-delete": "Digita {delete} nel box qui sotto per confermare.", "message.type-delete": "Digita {delete} nel box qui sotto per confermare.",
"message.type-reset": "Type {reset} in the box below to confirm.", "message.type-reset": "Digita {reset} nel box qui sotto per confermare.",
"metrics.actions": "Azioni", "metrics.actions": "Azioni",
"metrics.average-visit-time": "Tempo medio di visita", "metrics.average-visit-time": "Tempo medio di visita",
"metrics.bounce-rate": "Frequenza di rimbalzo", "metrics.bounce-rate": "Frequenza di rimbalzo",
"metrics.browsers": "Browser", "metrics.browsers": "Browser",
"metrics.countries": "Nazioni", "metrics.countries": "Nazioni",
"metrics.device.desktop": "Desktop", "metrics.device.desktop": "Desktop",
"metrics.device.laptop": "Laptop", "metrics.device.laptop": "Portatile",
"metrics.device.mobile": "Mobile", "metrics.device.mobile": "Cellulare",
"metrics.device.tablet": "Tablet", "metrics.device.tablet": "Tablet",
"metrics.devices": "Dispositivi", "metrics.devices": "Dispositivi",
"metrics.events": "Eventi", "metrics.events": "Eventi",
"metrics.filter.combined": "Aggregati", "metrics.filter.combined": "Aggregati",
"metrics.filter.domain-only": "Solo dominio", "metrics.filter.domain-only": "Solo dominio",
"metrics.filter.raw": "Raw", "metrics.filter.raw": "Raw",
"metrics.languages": "Languages", "metrics.languages": "Lingue",
"metrics.operating-systems": "Sistemi operativi", "metrics.operating-systems": "Sistemi operativi",
"metrics.page-views": "Visualizzazioni di pagina", "metrics.page-views": "Visualizzazioni di pagina",
"metrics.pages": "Pagine", "metrics.pages": "Pagine",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "共有リンクを有効にする", "label.enable-share-url": "共有リンクを有効にする",
"label.invalid": "無効", "label.invalid": "無効",
"label.invalid-domain": "無効なドメイン", "label.invalid-domain": "無効なドメイン",
"label.language": "Language",
"label.last-days": "過去{x}日間", "label.last-days": "過去{x}日間",
"label.last-hours": "過去{x}時間", "label.last-hours": "過去{x}時間",
"label.logged-in-as": "{username}でログイン中", "label.logged-in-as": "{username}でログイン中",
@ -50,6 +51,7 @@
"label.settings": "設定", "label.settings": "設定",
"label.share-url": "共有リンク", "label.share-url": "共有リンク",
"label.single-day": "一日のみ", "label.single-day": "一日のみ",
"label.theme": "Theme",
"label.this-month": "今月", "label.this-month": "今月",
"label.this-week": "今週", "label.this-week": "今週",
"label.this-year": "今年", "label.this-year": "今年",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "URL 공유 활성화", "label.enable-share-url": "URL 공유 활성화",
"label.invalid": "무효", "label.invalid": "무효",
"label.invalid-domain": "잘못된 도메인", "label.invalid-domain": "잘못된 도메인",
"label.language": "Language",
"label.last-days": "최근 {x} 일간", "label.last-days": "최근 {x} 일간",
"label.last-hours": "최근 {x} 시간", "label.last-hours": "최근 {x} 시간",
"label.logged-in-as": "{username}(으)로 로그인됨", "label.logged-in-as": "{username}(으)로 로그인됨",
@ -50,6 +51,7 @@
"label.settings": "설정", "label.settings": "설정",
"label.share-url": "공유 URL", "label.share-url": "공유 URL",
"label.single-day": "하루", "label.single-day": "하루",
"label.theme": "Theme",
"label.this-month": "이번 달", "label.this-month": "이번 달",
"label.this-week": "이번 주", "label.this-week": "이번 주",
"label.this-year": "올해", "label.this-year": "올해",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Įjungti bendrinimą su nuoroda", "label.enable-share-url": "Įjungti bendrinimą su nuoroda",
"label.invalid": "Neteisingai", "label.invalid": "Neteisingai",
"label.invalid-domain": "Klaidingas domenas", "label.invalid-domain": "Klaidingas domenas",
"label.language": "Language",
"label.last-days": "{x, plural, =0 {Paskutinės # dienų} zero {Paskutinės # dienų} one {Paskutinė diena} other {Paskutinės # dienos}}", "label.last-days": "{x, plural, =0 {Paskutinės # dienų} zero {Paskutinės # dienų} one {Paskutinė diena} other {Paskutinės # dienos}}",
"label.last-hours": "{x, plural, =0 {Paskutinės # valandų} zero {Paskutinės # valandų} one {Paskutinė # valanda} other {Paskutinės # valandos}}", "label.last-hours": "{x, plural, =0 {Paskutinės # valandų} zero {Paskutinės # valandų} one {Paskutinė # valanda} other {Paskutinės # valandos}}",
"label.logged-in-as": "Prisijungęs kaip {username}", "label.logged-in-as": "Prisijungęs kaip {username}",
@ -50,6 +51,7 @@
"label.settings": "Nustatymai", "label.settings": "Nustatymai",
"label.share-url": "Pasidalinti nuoroda", "label.share-url": "Pasidalinti nuoroda",
"label.single-day": "Viena diena", "label.single-day": "Viena diena",
"label.theme": "Theme",
"label.this-month": "Šis mėnuo", "label.this-month": "Šis mėnuo",
"label.this-week": "Ši savaitė", "label.this-week": "Ši savaitė",
"label.this-year": "Šie metai", "label.this-year": "Šie metai",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх", "label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх",
"label.invalid": "Буруу", "label.invalid": "Буруу",
"label.invalid-domain": "Буруу домэйн", "label.invalid-domain": "Буруу домэйн",
"label.language": "Language",
"label.last-days": "Сүүлийн {x} хоног", "label.last-days": "Сүүлийн {x} хоног",
"label.last-hours": "Сүүлийн {x} цаг", "label.last-hours": "Сүүлийн {x} цаг",
"label.logged-in-as": "{username}-р нэвтэрсэн", "label.logged-in-as": "{username}-р нэвтэрсэн",
@ -50,6 +51,7 @@
"label.settings": "Тохиргоо", "label.settings": "Тохиргоо",
"label.share-url": "Хуваалцах холбоос", "label.share-url": "Хуваалцах холбоос",
"label.single-day": "Нэг өдөр", "label.single-day": "Нэг өдөр",
"label.theme": "Theme",
"label.this-month": "Энэ сар", "label.this-month": "Энэ сар",
"label.this-week": "Энэ долоо хоног", "label.this-week": "Энэ долоо хоног",
"label.this-year": "Энэ жил", "label.this-year": "Энэ жил",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Aktifkan url berkongsi", "label.enable-share-url": "Aktifkan url berkongsi",
"label.invalid": "Tidak sah", "label.invalid": "Tidak sah",
"label.invalid-domain": "Domain tidak sah", "label.invalid-domain": "Domain tidak sah",
"label.language": "Language",
"label.last-days": "{x} hari lepas", "label.last-days": "{x} hari lepas",
"label.last-hours": "{x} jam lepas", "label.last-hours": "{x} jam lepas",
"label.logged-in-as": "Log masuk sebagai {username}", "label.logged-in-as": "Log masuk sebagai {username}",
@ -50,6 +51,7 @@
"label.settings": "Tetapan", "label.settings": "Tetapan",
"label.share-url": "Kongsikan URL", "label.share-url": "Kongsikan URL",
"label.single-day": "Satu hari", "label.single-day": "Satu hari",
"label.theme": "Theme",
"label.this-month": "Bulan ini", "label.this-month": "Bulan ini",
"label.this-week": "Minggu ini", "label.this-week": "Minggu ini",
"label.this-year": "Tahun ini", "label.this-year": "Tahun ini",

View File

@ -28,6 +28,7 @@
"label.enable-share-url": "Aktiver delings-URL", "label.enable-share-url": "Aktiver delings-URL",
"label.invalid": "Ugyldig", "label.invalid": "Ugyldig",
"label.invalid-domain": "Ugyldig domene", "label.invalid-domain": "Ugyldig domene",
"label.language": "Language",
"label.last-days": "Siste {x} dager", "label.last-days": "Siste {x} dager",
"label.last-hours": "Siste {x} timer", "label.last-hours": "Siste {x} timer",
"label.logged-in-as": "Logget på som {brukernavn}", "label.logged-in-as": "Logget på som {brukernavn}",
@ -50,6 +51,7 @@
"label.settings": "Innstillinger", "label.settings": "Innstillinger",
"label.share-url": "Del URL", "label.share-url": "Del URL",
"label.single-day": "Enkelt dag", "label.single-day": "Enkelt dag",
"label.theme": "Theme",
"label.this-month": "Denne måneden", "label.this-month": "Denne måneden",
"label.this-week": "Denne uka", "label.this-week": "Denne uka",
"label.this-year": "I år", "label.this-year": "I år",

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