fetched v.0.80.0
commit
4d11d39278
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M270.2 160h35.5c3.4 0 6.1 2.8 6 6.2l-7.5 196c-.1 3.2-2.8 5.8-6 5.8h-20.5c-3.2 0-5.9-2.5-6-5.8l-7.5-196c-.1-3.4 2.6-6.2 6-6.2zM288 388c-15.5 0-28 12.5-28 28s12.5 28 28 28 28-12.5 28-28-12.5-28-28-28zm281.5 52L329.6 24c-18.4-32-64.7-32-83.2 0L6.5 440c-18.4 31.9 4.6 72 41.6 72H528c36.8 0 60-40 41.5-72zM528 480H48c-12.3 0-20-13.3-13.9-24l240-416c6.1-10.6 21.6-10.7 27.7 0l240 416c6.2 10.6-1.5 24-13.8 24z"/></svg>
|
After Width: | Height: | Size: 482 B |
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Icon from './Icon';
|
||||||
|
import Exclamation from 'assets/exclamation-triangle.svg';
|
||||||
|
import styles from './ErrorMessage.module.css';
|
||||||
|
|
||||||
|
export default function ErrorMessage() {
|
||||||
|
return (
|
||||||
|
<div className={styles.error}>
|
||||||
|
<Icon icon={<Exclamation />} className={styles.icon} size="large" />
|
||||||
|
<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
.error {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
|
@ -12,6 +12,8 @@
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
border: 1px solid var(--gray500);
|
border: 1px solid var(--gray500);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 2;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option {
|
.option {
|
||||||
|
|
|
@ -9,7 +9,8 @@ export default function MenuButton({
|
||||||
icon,
|
icon,
|
||||||
value,
|
value,
|
||||||
options,
|
options,
|
||||||
menuClassname,
|
buttonClassName,
|
||||||
|
menuClassName,
|
||||||
menuPosition = 'bottom',
|
menuPosition = 'bottom',
|
||||||
menuAlign = 'right',
|
menuAlign = 'right',
|
||||||
onSelect,
|
onSelect,
|
||||||
|
@ -38,7 +39,7 @@ export default function MenuButton({
|
||||||
<div className={styles.container} ref={ref}>
|
<div className={styles.container} ref={ref}>
|
||||||
<Button
|
<Button
|
||||||
icon={icon}
|
icon={icon}
|
||||||
className={classNames(styles.button, { [styles.open]: showMenu })}
|
className={classNames(styles.button, buttonClassName, { [styles.open]: showMenu })}
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
variant="light"
|
variant="light"
|
||||||
>
|
>
|
||||||
|
@ -46,7 +47,7 @@ export default function MenuButton({
|
||||||
</Button>
|
</Button>
|
||||||
{showMenu && (
|
{showMenu && (
|
||||||
<Menu
|
<Menu
|
||||||
className={classNames(styles.menu, menuClassname)}
|
className={menuClassName}
|
||||||
options={options}
|
options={options}
|
||||||
selectedOption={selectedOption}
|
selectedOption={selectedOption}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
|
|
|
@ -9,10 +9,6 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu {
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
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 styles from './Footer.module.css';
|
import styles from './Footer.module.css';
|
||||||
|
@ -8,9 +9,9 @@ export default function Footer() {
|
||||||
const { current } = useVersion();
|
const { current } = useVersion();
|
||||||
return (
|
return (
|
||||||
<footer className="container">
|
<footer className="container">
|
||||||
<div className={styles.footer}>
|
<div className={classNames(styles.footer, 'row')}>
|
||||||
<div />
|
<div className="col-12 col-md-4" />
|
||||||
<div>
|
<div className="col-12 col-md-4">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="message.powered-by"
|
id="message.powered-by"
|
||||||
defaultMessage="Powered by {name}"
|
defaultMessage="Powered by {name}"
|
||||||
|
@ -23,7 +24,7 @@ export default function Footer() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>{`v${current}`}</div>
|
<div className={classNames(styles.version, 'col-12 col-md-4')}>{`v${current}`}</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,4 +4,15 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
.version {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Loading from 'components/common/Loading';
|
import Loading from 'components/common/Loading';
|
||||||
|
import ErrorMessage from 'components/common/ErrorMessage';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import useDateRange from 'hooks/useDateRange';
|
import useDateRange from 'hooks/useDateRange';
|
||||||
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
||||||
|
@ -17,7 +18,7 @@ export default function MetricsBar({ websiteId, token, className }) {
|
||||||
query: { url },
|
query: { url },
|
||||||
} = usePageQuery();
|
} = usePageQuery();
|
||||||
|
|
||||||
const { data } = useFetch(
|
const { data, error, loading } = useFetch(
|
||||||
`/api/website/${websiteId}/metrics`,
|
`/api/website/${websiteId}/metrics`,
|
||||||
{
|
{
|
||||||
start_at: +startDate,
|
start_at: +startDate,
|
||||||
|
@ -40,9 +41,9 @@ export default function MetricsBar({ websiteId, token, className }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>
|
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>
|
||||||
{!data ? (
|
{!data && loading && <Loading />}
|
||||||
<Loading />
|
{error && <ErrorMessage />}
|
||||||
) : (
|
{data && !error && (
|
||||||
<>
|
<>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
label={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
.bar {
|
.bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
min-height: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar > div + div {
|
.bar > div + div {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { formatNumber, formatLongNumber } from 'lib/format';
|
||||||
import useDateRange from 'hooks/useDateRange';
|
import useDateRange from 'hooks/useDateRange';
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
import styles from './MetricsTable.module.css';
|
import styles from './MetricsTable.module.css';
|
||||||
|
import ErrorMessage from '../common/ErrorMessage';
|
||||||
|
|
||||||
export default function MetricsTable({
|
export default function MetricsTable({
|
||||||
websiteId,
|
websiteId,
|
||||||
|
@ -36,7 +37,7 @@ export default function MetricsTable({
|
||||||
query: { url },
|
query: { url },
|
||||||
} = usePageQuery();
|
} = usePageQuery();
|
||||||
|
|
||||||
const { data } = useFetch(
|
const { data, loading, error } = useFetch(
|
||||||
`/api/website/${websiteId}/rankings`,
|
`/api/website/${websiteId}/rankings`,
|
||||||
{
|
{
|
||||||
type,
|
type,
|
||||||
|
@ -61,7 +62,7 @@ export default function MetricsTable({
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [data, dataFilter, filterOptions]);
|
}, [data, error, dataFilter, filterOptions]);
|
||||||
|
|
||||||
const handleSetFormat = () => setFormat(state => !state);
|
const handleSetFormat = () => setFormat(state => !state);
|
||||||
|
|
||||||
|
@ -86,8 +87,9 @@ export default function MetricsTable({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.container, className)}>
|
<div className={classNames(styles.container, className)}>
|
||||||
{!data && <Loading />}
|
{!data && loading && <Loading />}
|
||||||
{data && (
|
{error && <ErrorMessage />}
|
||||||
|
{data && !error && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div className={styles.title}>{title}</div>
|
<div className={styles.title}>{title}</div>
|
||||||
|
|
|
@ -13,6 +13,7 @@ import usePageQuery from 'hooks/usePageQuery';
|
||||||
import { getDateArray, getDateLength } from 'lib/date';
|
import { getDateArray, getDateLength } from 'lib/date';
|
||||||
import Times from 'assets/times.svg';
|
import Times from 'assets/times.svg';
|
||||||
import styles from './WebsiteChart.module.css';
|
import styles from './WebsiteChart.module.css';
|
||||||
|
import ErrorMessage from '../common/ErrorMessage';
|
||||||
|
|
||||||
export default function WebsiteChart({
|
export default function WebsiteChart({
|
||||||
websiteId,
|
websiteId,
|
||||||
|
@ -31,7 +32,7 @@ export default function WebsiteChart({
|
||||||
query: { url },
|
query: { url },
|
||||||
} = usePageQuery();
|
} = usePageQuery();
|
||||||
|
|
||||||
const { data, loading } = useFetch(
|
const { data, loading, error } = useFetch(
|
||||||
`/api/website/${websiteId}/pageviews`,
|
`/api/website/${websiteId}/pageviews`,
|
||||||
{
|
{
|
||||||
start_at: +startDate,
|
start_at: +startDate,
|
||||||
|
@ -83,6 +84,7 @@ export default function WebsiteChart({
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
|
{error && <ErrorMessage />}
|
||||||
<PageviewsChart
|
<PageviewsChart
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
data={{ pageviews, uniques }}
|
data={{ pageviews, uniques }}
|
||||||
|
|
|
@ -33,7 +33,7 @@ export default function LanguageButton() {
|
||||||
icon={<Globe />}
|
icon={<Globe />}
|
||||||
options={menuOptions}
|
options={menuOptions}
|
||||||
value={locale}
|
value={locale}
|
||||||
menuClassname={styles.menu}
|
menuClassName={styles.menu}
|
||||||
renderValue={option => option?.display}
|
renderValue={option => option?.display}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -8,5 +8,18 @@
|
||||||
|
|
||||||
.menu div {
|
.menu div {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
min-width: 33%;
|
min-width: calc(100% / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 992px) {
|
||||||
|
.menu {
|
||||||
|
min-width: 90vw;
|
||||||
|
transform: translateX(calc(40vw));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
.menu div {
|
||||||
|
min-width: 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,13 @@ export default function useFetch(url, params = {}, options = {}) {
|
||||||
|
|
||||||
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
|
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
|
||||||
|
|
||||||
|
if (status >= 400) {
|
||||||
|
setError(data);
|
||||||
|
setData(null);
|
||||||
|
} else {
|
||||||
setData(data);
|
setData(data);
|
||||||
|
}
|
||||||
|
|
||||||
setStatus(status);
|
setStatus(status);
|
||||||
onDataLoad(data);
|
onDataLoad(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
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;
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
{
|
||||||
|
"button.add-account": "Tambah akun",
|
||||||
|
"button.add-website": "Tambah situs web",
|
||||||
|
"button.back": "Kembali",
|
||||||
|
"button.cancel": "Batal",
|
||||||
|
"button.change-password": "Ganti password",
|
||||||
|
"button.copy-to-clipboard": "Salin ke papan klip",
|
||||||
|
"button.date-range": "Rentang tanggal",
|
||||||
|
"button.delete": "Hapus",
|
||||||
|
"button.dismiss": "Tutup",
|
||||||
|
"button.edit": "Sunting",
|
||||||
|
"button.login": "Masuk",
|
||||||
|
"button.more": "Lebih banyak",
|
||||||
|
"button.refresh": "Segarkan",
|
||||||
|
"button.reset": "Atur ulang",
|
||||||
|
"button.save": "Simpan",
|
||||||
|
"button.single-day": "Sehari",
|
||||||
|
"button.view-details": "Lihat Detil",
|
||||||
|
"label.accounts": "Akun",
|
||||||
|
"label.administrator": "Pengelola",
|
||||||
|
"label.confirm-password": "Konfirmasi kata sandi",
|
||||||
|
"label.current-password": "Kata sandi sekarang",
|
||||||
|
"label.custom-range": "Rentang khusus",
|
||||||
|
"label.dashboard": "Dasbor",
|
||||||
|
"label.default-date-range": "Rentang tanggal default",
|
||||||
|
"label.domain": "Domain",
|
||||||
|
"label.enable-share-url": "Aktifkan URL berbagi",
|
||||||
|
"label.invalid": "Tidak valid",
|
||||||
|
"label.invalid-domain": "Domain tidak valid",
|
||||||
|
"label.last-days": "{x} hari terakhir",
|
||||||
|
"label.last-hours": "{x} jam terakhir",
|
||||||
|
"label.logged-in-as": "Masuk sebagai {username}",
|
||||||
|
"label.logout": "Keluar",
|
||||||
|
"label.name": "Nama",
|
||||||
|
"label.new-password": "Kata sandi baru",
|
||||||
|
"label.password": "Kata sandi",
|
||||||
|
"label.passwords-dont-match": "Kata sandi tidak cocok",
|
||||||
|
"label.profile": "Profil",
|
||||||
|
"label.required": "Wajib",
|
||||||
|
"label.settings": "Pengaturan",
|
||||||
|
"label.this-month": "Bulan ini",
|
||||||
|
"label.this-week": "Minggu ini",
|
||||||
|
"label.this-year": "Tahun ini",
|
||||||
|
"label.timezone": "Zona waktu",
|
||||||
|
"label.today": "Hari ini",
|
||||||
|
"label.unknown": "Tidak diketahui",
|
||||||
|
"label.username": "Nama pengguna",
|
||||||
|
"label.websites": "Situs web",
|
||||||
|
"message.active-users": "{x} pengunjung saat ini",
|
||||||
|
"message.confirm-delete": "Apakah kamu yakin ingin menghapus {target}?",
|
||||||
|
"message.copied": "Tersalin!",
|
||||||
|
"message.delete-warning": "Semua data terkait juga akan dihapus.",
|
||||||
|
"message.failure": "Ada yang salah.",
|
||||||
|
"message.get-share-url": "Dapatkan URL berbagi",
|
||||||
|
"message.get-tracking-code": "Dapatkan kode pelacakan",
|
||||||
|
"message.go-to-settings": "Pergi ke pengaturan",
|
||||||
|
"message.incorrect-username-password": "Nama pengguna/kata sandi salah.",
|
||||||
|
"message.new-version-available": "Versi terbaru umami {version} telah tersedia!",
|
||||||
|
"message.no-data-available": "Tidak ada data.",
|
||||||
|
"message.no-websites-configured": "Anda tidak memiliki situs web yang dikonfigurasi.",
|
||||||
|
"message.page-not-found": "Halaman tidak ditemukan.",
|
||||||
|
"message.powered-by": "Didukung oleh {name}",
|
||||||
|
"message.save-success": "Berhasil disimpan.",
|
||||||
|
"message.share-url": "Ini adalah URL yang dibagikan secara publik untuk {target}.",
|
||||||
|
"message.track-stats": "Untuk melacak statistik {target}, tempatkan kode berikut di bagian {head} situs web anda.",
|
||||||
|
"message.type-delete": "Ketikkan {delete} pada kotak di bawah untuk konfirmasi.",
|
||||||
|
"metrics.actions": "Aksi",
|
||||||
|
"metrics.average-visit-time": "Waktu kunjungan rata-rata",
|
||||||
|
"metrics.bounce-rate": "Tingkat bouncing",
|
||||||
|
"metrics.browsers": "Browser",
|
||||||
|
"metrics.countries": "Negara",
|
||||||
|
"metrics.device.desktop": "Desktop",
|
||||||
|
"metrics.device.laptop": "Laptop",
|
||||||
|
"metrics.device.mobile": "Ponsel",
|
||||||
|
"metrics.device.tablet": "Tablet",
|
||||||
|
"metrics.devices": "Perangkat",
|
||||||
|
"metrics.events": "Perihal",
|
||||||
|
"metrics.filter.combined": "Gabungan",
|
||||||
|
"metrics.filter.domain-only": "Hanya domain",
|
||||||
|
"metrics.filter.raw": "Raw",
|
||||||
|
"metrics.operating-systems": "Sistem Operasi",
|
||||||
|
"metrics.page-views": "Tampilan halaman",
|
||||||
|
"metrics.pages": "Halaman",
|
||||||
|
"metrics.referrers": "Perujuk",
|
||||||
|
"metrics.unique-visitors": "Pengunjung unik",
|
||||||
|
"metrics.views": "Tampilan",
|
||||||
|
"metrics.visitors": "Pengunjung",
|
||||||
|
"title.add-account": "Tambah akun",
|
||||||
|
"title.add-website": "Tambah situs web",
|
||||||
|
"title.change-password": "Ganti kata sandi",
|
||||||
|
"title.delete-account": "Hapus akun",
|
||||||
|
"title.delete-website": "Hapus situs web",
|
||||||
|
"title.edit-account": "Sunting akun",
|
||||||
|
"title.edit-website": "Sunting situs web",
|
||||||
|
"title.share-url": "Bagikan URL",
|
||||||
|
"title.tracking-code": "Kode lacak"
|
||||||
|
}
|
|
@ -7,22 +7,22 @@
|
||||||
"button.copy-to-clipboard": "Kopiëer naar klembord",
|
"button.copy-to-clipboard": "Kopiëer naar klembord",
|
||||||
"button.date-range": "Datumbereik",
|
"button.date-range": "Datumbereik",
|
||||||
"button.delete": "Verwijderen",
|
"button.delete": "Verwijderen",
|
||||||
"button.dismiss": "Dismiss",
|
"button.dismiss": "Negeren",
|
||||||
"button.edit": "Bewerken",
|
"button.edit": "Bewerken",
|
||||||
"button.login": "Inloggen",
|
"button.login": "Inloggen",
|
||||||
"button.more": "Toon meer",
|
"button.more": "Toon meer",
|
||||||
"button.refresh": "Vernieuwen",
|
"button.refresh": "Vernieuwen",
|
||||||
"button.reset": "Reset",
|
"button.reset": "Resetten",
|
||||||
"button.save": "Opslaan",
|
"button.save": "Opslaan",
|
||||||
"button.single-day": "Enkele dag",
|
"button.single-day": "Enkele dag",
|
||||||
"button.view-details": "Meer details",
|
"button.view-details": "Meer details",
|
||||||
"label.accounts": "Accounts",
|
"label.accounts": "Gebruikers",
|
||||||
"label.administrator": "Administrator",
|
"label.administrator": "Administrator",
|
||||||
"label.confirm-password": "Wachtwoord bevestigen",
|
"label.confirm-password": "Wachtwoord bevestigen",
|
||||||
"label.current-password": "Huidig wachtwoord",
|
"label.current-password": "Huidig wachtwoord",
|
||||||
"label.custom-range": "Aangepast bereik",
|
"label.custom-range": "Aangepast bereik",
|
||||||
"label.dashboard": "Dashboard",
|
"label.dashboard": "Overzicht",
|
||||||
"label.default-date-range": "Default date range",
|
"label.default-date-range": "Standaard bereik",
|
||||||
"label.domain": "Domein",
|
"label.domain": "Domein",
|
||||||
"label.enable-share-url": "Sta delen via openbare URL toe",
|
"label.enable-share-url": "Sta delen via openbare URL toe",
|
||||||
"label.invalid": "Ongeldig",
|
"label.invalid": "Ongeldig",
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
"label.this-month": "Deze maand",
|
"label.this-month": "Deze maand",
|
||||||
"label.this-week": "Deze week",
|
"label.this-week": "Deze week",
|
||||||
"label.this-year": "Dit jaar",
|
"label.this-year": "Dit jaar",
|
||||||
"label.timezone": "Timezone",
|
"label.timezone": "Tijdzone",
|
||||||
"label.today": "Vandaag",
|
"label.today": "Vandaag",
|
||||||
"label.unknown": "Onbekend",
|
"label.unknown": "Onbekend",
|
||||||
"label.username": "Gebruikersnaam",
|
"label.username": "Gebruikersnaam",
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
"message.get-tracking-code": "Tracking code",
|
"message.get-tracking-code": "Tracking code",
|
||||||
"message.go-to-settings": "Naar instellingen",
|
"message.go-to-settings": "Naar instellingen",
|
||||||
"message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.",
|
"message.incorrect-username-password": "Incorrecte gebruikersnaam/wachtwoord.",
|
||||||
"message.new-version-available": "A new version of umami {version} is available!",
|
"message.new-version-available": "Een nieuwe versie van umami {version} is beschikbaar!",
|
||||||
"message.no-data-available": "Geen gegevens beschikbaar.",
|
"message.no-data-available": "Geen gegevens beschikbaar.",
|
||||||
"message.no-websites-configured": "Je hebt geen websites ingesteld.",
|
"message.no-websites-configured": "Je hebt geen websites ingesteld.",
|
||||||
"message.page-not-found": "Pagina niet gevonden.",
|
"message.page-not-found": "Pagina niet gevonden.",
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
{
|
||||||
|
"button.add-account": "Додати обліковий запис",
|
||||||
|
"button.add-website": "Додати веб-сайт",
|
||||||
|
"button.back": "Назад",
|
||||||
|
"button.cancel": "Відмінити",
|
||||||
|
"button.change-password": "Змінити пароль",
|
||||||
|
"button.copy-to-clipboard": "Копіювати до буферу обміну",
|
||||||
|
"button.date-range": "Діапазон дат",
|
||||||
|
"button.delete": "Видалити",
|
||||||
|
"button.dismiss": "Відхилити",
|
||||||
|
"button.edit": "Редагувати",
|
||||||
|
"button.login": "Увійти",
|
||||||
|
"button.more": "Більше",
|
||||||
|
"button.refresh": "Оновити",
|
||||||
|
"button.reset": "Скинути",
|
||||||
|
"button.save": "Зберегти",
|
||||||
|
"button.single-day": "Один день",
|
||||||
|
"button.view-details": "Переглянути деталі",
|
||||||
|
"label.accounts": "Облікові записи",
|
||||||
|
"label.administrator": "Адміністратор",
|
||||||
|
"label.confirm-password": "Підтвердити пароль",
|
||||||
|
"label.current-password": "Поточний пароль",
|
||||||
|
"label.custom-range": "Довільний період",
|
||||||
|
"label.dashboard": "Інформаційна панель",
|
||||||
|
"label.default-date-range": "Діапазон дат за умовчанням",
|
||||||
|
"label.domain": "Домен",
|
||||||
|
"label.enable-share-url": "Дозволити ділитися посиланням",
|
||||||
|
"label.invalid": "Некоректний",
|
||||||
|
"label.invalid-domain": "Некоректний домен",
|
||||||
|
"label.last-days": "Останні {x} днів",
|
||||||
|
"label.last-hours": "Останні {x} годин",
|
||||||
|
"label.logged-in-as": "Ви увійшли як {username}",
|
||||||
|
"label.logout": "Вийти",
|
||||||
|
"label.name": "Ім'я",
|
||||||
|
"label.new-password": "Новий пароль",
|
||||||
|
"label.password": "Пароль",
|
||||||
|
"label.passwords-dont-match": "Паролі не співпадають",
|
||||||
|
"label.profile": "Профіль",
|
||||||
|
"label.required": "Обов'язкове",
|
||||||
|
"label.settings": "Налаштування",
|
||||||
|
"label.this-month": "Поточний місяць",
|
||||||
|
"label.this-week": "Поточний тиждень",
|
||||||
|
"label.this-year": "Поточний рік",
|
||||||
|
"label.timezone": "Часовий пояс",
|
||||||
|
"label.today": "Сьогодні",
|
||||||
|
"label.unknown": "Невідомо",
|
||||||
|
"label.username": "Ім'я користувача",
|
||||||
|
"label.websites": "Веб-сайти",
|
||||||
|
"message.active-users": "{x} поточних відвідувачів",
|
||||||
|
"message.confirm-delete": "Ви впевнені, що бажаєте видалити {target}?",
|
||||||
|
"message.copied": "Скопійовано!",
|
||||||
|
"message.delete-warning": "Усі пов'язані дані будуть видалені також.",
|
||||||
|
"message.failure": "Щось пішло не так.",
|
||||||
|
"message.get-share-url": "Отримати публічне посилання",
|
||||||
|
"message.get-tracking-code": "Отримати код для відслідковування",
|
||||||
|
"message.go-to-settings": "Перейти до налаштувань",
|
||||||
|
"message.incorrect-username-password": "Невірне ім'я користувача або пароль.",
|
||||||
|
"message.new-version-available": "Нова версія umami {version} доступна!",
|
||||||
|
"message.no-data-available": "Немає даних.",
|
||||||
|
"message.no-websites-configured": "У вас немає налаштованих веб-сайтів.",
|
||||||
|
"message.page-not-found": "Сторінку не знайдено.",
|
||||||
|
"message.powered-by": "На базі {name}",
|
||||||
|
"message.save-success": "Збережено успішно.",
|
||||||
|
"message.share-url": "Це публічне посилання для {target}.",
|
||||||
|
"message.track-stats": "Або відслідковувати статистику для {target}, розмістіть наступний код у {head} секції вашого веб-сайту.",
|
||||||
|
"message.type-delete": "Введіть {delete} у полі нижче щоб підтвердити.",
|
||||||
|
"metrics.actions": "Дії",
|
||||||
|
"metrics.average-visit-time": "Середній час візиту",
|
||||||
|
"metrics.bounce-rate": "Показник відмов",
|
||||||
|
"metrics.browsers": "Браузери",
|
||||||
|
"metrics.countries": "Країни",
|
||||||
|
"metrics.device.desktop": "Настільний комп'ютер",
|
||||||
|
"metrics.device.laptop": "Ноутбук",
|
||||||
|
"metrics.device.mobile": "Мобільний",
|
||||||
|
"metrics.device.tablet": "Планшет",
|
||||||
|
"metrics.devices": "Пристрої",
|
||||||
|
"metrics.events": "Події",
|
||||||
|
"metrics.filter.combined": "Об'єднані",
|
||||||
|
"metrics.filter.domain-only": "Лише домен",
|
||||||
|
"metrics.filter.raw": "Сирі дані",
|
||||||
|
"metrics.operating-systems": "Операційна система",
|
||||||
|
"metrics.page-views": "Перегляди сторінок",
|
||||||
|
"metrics.pages": "Сторінки",
|
||||||
|
"metrics.referrers": "Джерела",
|
||||||
|
"metrics.unique-visitors": "Унікальні відвідувачі",
|
||||||
|
"metrics.views": "Перегляди",
|
||||||
|
"metrics.visitors": "Відвідувачі",
|
||||||
|
"title.add-account": "Додати обліковий запис",
|
||||||
|
"title.add-website": "Додати website",
|
||||||
|
"title.change-password": "Змінити пароль",
|
||||||
|
"title.delete-account": "Видалити обліковий запис",
|
||||||
|
"title.delete-website": "Видалити веб-сайт",
|
||||||
|
"title.edit-account": "Редагувати обліковий запис",
|
||||||
|
"title.edit-website": "Редагувати веб-сайт",
|
||||||
|
"title.share-url": "Поділитися посилання",
|
||||||
|
"title.tracking-code": "Код для відслідковування"
|
||||||
|
}
|
28
lib/lang.js
28
lib/lang.js
|
@ -1,5 +1,23 @@
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { enUS, nl, zhCN, tr, ru, de, ja, es, fr, da, sv, el, pt, ro, nb } from 'date-fns/locale';
|
import {
|
||||||
|
enUS,
|
||||||
|
nl,
|
||||||
|
zhCN,
|
||||||
|
tr,
|
||||||
|
ru,
|
||||||
|
de,
|
||||||
|
ja,
|
||||||
|
es,
|
||||||
|
fr,
|
||||||
|
da,
|
||||||
|
sv,
|
||||||
|
el,
|
||||||
|
pt,
|
||||||
|
ro,
|
||||||
|
nb,
|
||||||
|
id,
|
||||||
|
uk,
|
||||||
|
} from 'date-fns/locale';
|
||||||
import enMessages from 'lang-compiled/en-US.json';
|
import enMessages from 'lang-compiled/en-US.json';
|
||||||
import nlMessages from 'lang-compiled/nl-NL.json';
|
import nlMessages from 'lang-compiled/nl-NL.json';
|
||||||
import zhCNMessages from 'lang-compiled/zh-CN.json';
|
import zhCNMessages from 'lang-compiled/zh-CN.json';
|
||||||
|
@ -17,6 +35,8 @@ import foMessages from 'lang-compiled/fo-FO.json';
|
||||||
import ptMessages from 'lang-compiled/pt-PT.json';
|
import ptMessages from 'lang-compiled/pt-PT.json';
|
||||||
import roMessages from 'lang-compiled/ro-RO.json';
|
import roMessages from 'lang-compiled/ro-RO.json';
|
||||||
import nbNOMessages from 'lang-compiled/nb-NO.json';
|
import nbNOMessages from 'lang-compiled/nb-NO.json';
|
||||||
|
import idMessages from 'lang-compiled/id-ID.json';
|
||||||
|
import ukMessages from 'lang-compiled/uk-UA.json';
|
||||||
|
|
||||||
export const messages = {
|
export const messages = {
|
||||||
'en-US': enMessages,
|
'en-US': enMessages,
|
||||||
|
@ -36,6 +56,8 @@ export const messages = {
|
||||||
'pt-PT': ptMessages,
|
'pt-PT': ptMessages,
|
||||||
'ro-RO': roMessages,
|
'ro-RO': roMessages,
|
||||||
'nb-NO': nbNOMessages,
|
'nb-NO': nbNOMessages,
|
||||||
|
'id-ID': idMessages,
|
||||||
|
'uk-UA': ukMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dateLocales = {
|
export const dateLocales = {
|
||||||
|
@ -56,6 +78,8 @@ export const dateLocales = {
|
||||||
'pt-PT': pt,
|
'pt-PT': pt,
|
||||||
'ro-RO': ro,
|
'ro-RO': ro,
|
||||||
'nb-NO': nb,
|
'nb-NO': nb,
|
||||||
|
'id-ID': id,
|
||||||
|
'uk-UA': uk,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const menuOptions = [
|
export const menuOptions = [
|
||||||
|
@ -67,6 +91,7 @@ export const menuOptions = [
|
||||||
{ label: 'Føroyskt', value: 'fo-FO', display: 'fo' },
|
{ label: 'Føroyskt', value: 'fo-FO', display: 'fo' },
|
||||||
{ label: 'Français', value: 'fr-FR', display: 'fr' },
|
{ label: 'Français', value: 'fr-FR', display: 'fr' },
|
||||||
{ label: 'Ελληνικά', value: 'el-GR', display: 'el' },
|
{ label: 'Ελληνικά', value: 'el-GR', display: 'el' },
|
||||||
|
{ label: 'Bahasa Indonesia', value: 'id-ID', display: 'id' },
|
||||||
{ label: '日本語', value: 'ja-JP', display: 'ja' },
|
{ label: '日本語', value: 'ja-JP', display: 'ja' },
|
||||||
{ label: 'Монгол', value: 'mn-MN', display: 'mn' },
|
{ label: 'Монгол', value: 'mn-MN', display: 'mn' },
|
||||||
{ label: 'Nederlands', value: 'nl-NL', display: 'nl' },
|
{ label: 'Nederlands', value: 'nl-NL', display: 'nl' },
|
||||||
|
@ -76,6 +101,7 @@ export const menuOptions = [
|
||||||
{ label: 'Română', value: 'ro-RO', display: 'ro' },
|
{ label: 'Română', value: 'ro-RO', display: 'ro' },
|
||||||
{ label: 'Svenska', value: 'sv-SE', display: 'sv' },
|
{ label: 'Svenska', value: 'sv-SE', display: 'sv' },
|
||||||
{ label: 'Türkçe', value: 'tr-TR', display: 'tr' },
|
{ label: 'Türkçe', value: 'tr-TR', display: 'tr' },
|
||||||
|
{ label: 'українська', value: 'uk-UA', display: 'uk' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function dateFormat(date, str, locale) {
|
export function dateFormat(date, str, locale) {
|
||||||
|
|
|
@ -16,12 +16,8 @@ export function getDatabase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runQuery(query) {
|
export async function runQuery(query) {
|
||||||
return query
|
return query.catch(e => {
|
||||||
.catch(e => {
|
|
||||||
throw e;
|
throw e;
|
||||||
})
|
|
||||||
.finally(async () => {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { getWebsiteByUuid, getSessionByUuid, createSession } from 'lib/queries';
|
import { getWebsiteByUuid, getSessionByUuid, createSession } from 'lib/queries';
|
||||||
import { getClientInfo } from 'lib/request';
|
import { getClientInfo } from 'lib/request';
|
||||||
import { uuid, isValidUuid } from 'lib/crypto';
|
import { uuid, isValidUuid, parseToken } from 'lib/crypto';
|
||||||
|
|
||||||
export async function getSession(req) {
|
export async function getSession(req) {
|
||||||
const { payload } = req.body;
|
const { payload } = req.body;
|
||||||
|
@ -9,7 +9,15 @@ export async function getSession(req) {
|
||||||
throw new Error('Invalid request');
|
throw new Error('Invalid request');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { website: website_uuid, hostname, screen, language } = payload;
|
const { website: website_uuid, hostname, screen, language, cache } = payload;
|
||||||
|
|
||||||
|
if (cache) {
|
||||||
|
const result = await parseToken(cache);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isValidUuid(website_uuid)) {
|
if (!isValidUuid(website_uuid)) {
|
||||||
throw new Error(`Invalid website: ${website_uuid}`);
|
throw new Error(`Invalid website: ${website_uuid}`);
|
||||||
|
|
|
@ -3,9 +3,7 @@ const pkg = require('./package.json');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
env: {
|
env: {
|
||||||
VERSION: pkg.version,
|
VERSION: pkg.version,
|
||||||
},
|
FORCE_SSL: !!process.env.FORCE_SSL,
|
||||||
serverRuntimeConfig: {
|
|
||||||
PROJECT_ROOT: __dirname,
|
|
||||||
},
|
},
|
||||||
webpack(config) {
|
webpack(config) {
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
|
@ -21,16 +19,11 @@ module.exports = {
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
// mathching all API routes
|
source: '/umami.js',
|
||||||
source: '/api/:path*',
|
|
||||||
headers: [
|
headers: [
|
||||||
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
|
|
||||||
{ key: 'Access-Control-Allow-Origin', value: '*' },
|
|
||||||
{ key: 'Access-Control-Allow-Methods', value: 'GET,OPTIONS,PATCH,DELETE,POST,PUT' },
|
|
||||||
{
|
{
|
||||||
key: 'Access-Control-Allow-Headers',
|
key: 'Cache-Control',
|
||||||
value:
|
value: 'public, max-age=2592000', // 30 days
|
||||||
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "0.72.0",
|
"version": "0.80.0",
|
||||||
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
||||||
"author": "Mike Cao <mike@mikecao.com>",
|
"author": "Mike Cao <mike@mikecao.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -32,6 +32,7 @@
|
||||||
"format-lang": "node scripts/format-lang.js",
|
"format-lang": "node scripts/format-lang.js",
|
||||||
"compile-lang": "formatjs compile-folder --ast build lang-compiled",
|
"compile-lang": "formatjs compile-folder --ast build lang-compiled",
|
||||||
"check-lang": "node scripts/check-lang.js",
|
"check-lang": "node scripts/check-lang.js",
|
||||||
|
"download-country-names": "node scripts/download-country-names.js",
|
||||||
"loadtest": "node scripts/loadtest.js",
|
"loadtest": "node scripts/loadtest.js",
|
||||||
"loadtest:medium": "node scripts/loadtest.js --weight=medium",
|
"loadtest:medium": "node scripts/loadtest.js --weight=medium",
|
||||||
"loadtest:heavy": "node scripts/loadtest.js --weight=heavy --verbose"
|
"loadtest:heavy": "node scripts/loadtest.js --weight=heavy --verbose"
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { IntlProvider } from 'react-intl';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { useStore } from 'redux/store';
|
import { useStore } from 'redux/store';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
|
import useForceSSL from 'hooks/useForceSSL';
|
||||||
import { messages } from 'lib/lang';
|
import { messages } from 'lib/lang';
|
||||||
import 'styles/variables.css';
|
import 'styles/variables.css';
|
||||||
import 'styles/bootstrap-grid.css';
|
import 'styles/bootstrap-grid.css';
|
||||||
|
@ -21,6 +22,7 @@ const Intl = ({ children }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function App({ Component, pageProps }) {
|
export default function App({ Component, pageProps }) {
|
||||||
|
useForceSSL(process.env.FORCE_SSL);
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,13 +1,24 @@
|
||||||
|
import isBot from 'isbot-fast';
|
||||||
import { savePageView, saveEvent } from 'lib/queries';
|
import { savePageView, saveEvent } from 'lib/queries';
|
||||||
import { useCors, useSession } from 'lib/middleware';
|
import { useCors, useSession } from 'lib/middleware';
|
||||||
import { ok, badRequest } from 'lib/response';
|
import { ok, badRequest } from 'lib/response';
|
||||||
import isBot from 'isbot-fast';
|
import { createToken } from 'lib/crypto';
|
||||||
|
import { getIpAddress } from '../../lib/request';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
if (isBot(req.headers['user-agent'])) {
|
if (isBot(req.headers['user-agent'])) {
|
||||||
return ok(res);
|
return ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.IGNORE_IP) {
|
||||||
|
const ips = process.env.IGNORE_IP.split(',').map(n => n.trim());
|
||||||
|
const ip = getIpAddress(req);
|
||||||
|
|
||||||
|
if (ips.includes(ip)) {
|
||||||
|
return ok(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await useCors(req, res);
|
await useCors(req, res);
|
||||||
await useSession(req, res);
|
await useSession(req, res);
|
||||||
|
|
||||||
|
@ -28,5 +39,7 @@ export default async (req, res) => {
|
||||||
return badRequest(res);
|
return badRequest(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ok(res);
|
const token = await createToken({ website_id, session_id });
|
||||||
|
|
||||||
|
return ok(res, token);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{"AF":"Afganistan","ZA":"Afrika Selatan","AL":"Albania","DZ":"Aljazair","US":"Amerika Serikat","AD":"Andorra","AO":"Angola","AI":"Anguilla","AQ":"Antarktika","AG":"Antigua dan Barbuda","SA":"Arab Saudi","AR":"Argentina","AM":"Armenia","AW":"Aruba","AU":"Australia","AT":"Austria","AZ":"Azerbaijan","BS":"Bahama","BH":"Bahrain","BD":"Bangladesh","BB":"Barbados","NL":"Belanda","BQ":"Belanda Karibia","BY":"Belarus","BE":"Belgia","BZ":"Belize","BJ":"Benin","BM":"Bermuda","BT":"Bhutan","BO":"Bolivia","BA":"Bosnia dan Herzegovina","BW":"Botswana","BR":"Brasil","BN":"Brunei","BG":"Bulgaria","BF":"Burkina Faso","BI":"Burundi","TD":"Cad","CZ":"Ceko","CL":"Cile","CI":"C\u00f4te d\u2019Ivoire","CW":"Cura\u00e7ao","DK":"Denmark","DM":"Dominika","EC":"Ekuador","SV":"El Salvador","ER":"Eritrea","EE":"Estonia","SZ":"eSwatini","ET":"Etiopia","FJ":"Fiji","PH":"Filipina","FI":"Finlandia","GA":"Gabon","GM":"Gambia","GE":"Georgia","GS":"Georgia Selatan & Kep. Sandwich Selatan","GH":"Ghana","GI":"Gibraltar","GD":"Grenada","GL":"Grinlandia","GP":"Guadeloupe","GU":"Guam","GT":"Guatemala","GG":"Guernsey","GN":"Guinea","GQ":"Guinea Ekuatorial","GW":"Guinea-Bissau","GY":"Guyana","GF":"Guyana Prancis","HT":"Haiti","HN":"Honduras","HK":"Hong Kong DAK Tiongkok","HU":"Hungaria","IN":"India","ID":"Indonesia","GB":"Inggris Raya","IQ":"Irak","IR":"Iran","IE":"Irlandia","IS":"Islandia","IL":"Israel","IT":"Italia","JM":"Jamaika","JP":"Jepang","DE":"Jerman","JE":"Jersey","DJ":"Jibuti","NC":"Kaledonia Baru","KH":"Kamboja","CM":"Kamerun","CA":"Kanada","KZ":"Kazakstan","KE":"Kenya","AX":"Kepulauan Aland","KY":"Kepulauan Cayman","CC":"Kepulauan Cocos (Keeling)","CK":"Kepulauan Cook","FK":"Kepulauan Falkland","FO":"Kepulauan Faroe","MP":"Kepulauan Mariana Utara","MH":"Kepulauan Marshall","NF":"Kepulauan Norfolk","PN":"Kepulauan Pitcairn","SB":"Kepulauan Solomon","SJ":"Kepulauan Svalbard dan Jan Mayen","UM":"Kepulauan Terluar A.S.","TC":"Kepulauan Turks dan Caicos","VI":"Kepulauan Virgin Amerika Serikat","VG":"Kepulauan Virgin Britania Raya","WF":"Kepulauan Wallis dan Futuna","KG":"Kirgistan","KI":"Kiribati","CO":"Kolombia","KM":"Komoro","CG":"Kongo - Brazzaville","CD":"Kongo - Kinshasa","KR":"Korea Selatan","KP":"Korea Utara","CR":"Kosta Rika","HR":"Kroasia","CU":"Kuba","KW":"Kuwait","LA":"Laos","LV":"Latvia","LB":"Lebanon","LS":"Lesotho","LR":"Liberia","LY":"Libia","LI":"Liechtenstein","LT":"Lituania","LU":"Luksemburg","MG":"Madagaskar","MO":"Makau DAK Tiongkok","MK":"Makedonia Utara","MV":"Maladewa","MW":"Malawi","MY":"Malaysia","ML":"Mali","MT":"Malta","MA":"Maroko","MQ":"Martinik","MR":"Mauritania","MU":"Mauritius","YT":"Mayotte","MX":"Meksiko","EG":"Mesir","FM":"Mikronesia","MD":"Moldova","MC":"Monako","MN":"Mongolia","ME":"Montenegro","MS":"Montserrat","MZ":"Mozambik","MM":"Myanmar (Burma)","NA":"Namibia","NR":"Nauru","NP":"Nepal","NE":"Niger","NG":"Nigeria","NI":"Nikaragua","NU":"Niue","NO":"Norwegia","OM":"Oman","PK":"Pakistan","PW":"Palau","PA":"Panama","PG":"Papua Nugini","PY":"Paraguay","PE":"Peru","PL":"Polandia","PF":"Polinesia Prancis","PT":"Portugal","FR":"Prancis","PR":"Puerto Riko","BV":"Pulau Bouvet","HM":"Pulau Heard dan Kepulauan McDonald","IM":"Pulau Man","CX":"Pulau Natal","QA":"Qatar","CF":"Republik Afrika Tengah","DO":"Republik Dominika","RE":"R\u00e9union","RO":"Rumania","RU":"Rusia","RW":"Rwanda","EH":"Sahara Barat","BL":"Saint Barth\u00e9lemy","SH":"Saint Helena","KN":"Saint Kitts dan Nevis","LC":"Saint Lucia","MF":"Saint Martin","PM":"Saint Pierre dan Miquelon","VC":"Saint Vincent dan Grenadine","WS":"Samoa","AS":"Samoa Amerika","SM":"San Marino","ST":"Sao Tome dan Principe","NZ":"Selandia Baru","SN":"Senegal","RS":"Serbia","SC":"Seychelles","SL":"Sierra Leone","SG":"Singapura","SX":"Sint Maarten","CY":"Siprus","SK":"Slovakia","SI":"Slovenia","SO":"Somalia","ES":"Spanyol","LK":"Sri Lanka","SD":"Sudan","SS":"Sudan Selatan","SY":"Suriah","SR":"Suriname","SE":"Swedia","CH":"Swiss","TW":"Taiwan","TJ":"Tajikistan","CV":"Tanjung Verde","TZ":"Tanzania","TH":"Thailand","TL":"Timor Leste","CN":"Tiongkok","TG":"Togo","TK":"Tokelau","TO":"Tonga","TT":"Trinidad dan Tobago","TN":"Tunisia","TR":"Turki","TM":"Turkimenistan","TV":"Tuvalu","UG":"Uganda","UA":"Ukraina","AE":"Uni Emirat Arab","UY":"Uruguay","UZ":"Uzbekistan","VU":"Vanuatu","VA":"Vatikan","VE":"Venezuela","VN":"Vietnam","IO":"Wilayah Inggris di Samudra Hindia","PS":"Wilayah Palestina","TF":"Wilayah Selatan Perancis","YE":"Yaman","JO":"Yordania","GR":"Yunani","ZM":"Zambia","ZW":"Zimbabwe"}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,39 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const https = require('https');
|
||||||
|
const chalk = require('chalk');
|
||||||
|
|
||||||
|
const src = path.resolve(__dirname, '../lang');
|
||||||
|
const dest = path.resolve(__dirname, '../public/country');
|
||||||
|
const files = fs.readdirSync(src);
|
||||||
|
|
||||||
|
const getUrl = locale =>
|
||||||
|
`https://raw.githubusercontent.com/umpirsky/country-list/master/data/${locale}/country.json`;
|
||||||
|
|
||||||
|
const asyncForEach = async (array, callback) => {
|
||||||
|
for (let index = 0; index < array.length; index++) {
|
||||||
|
await callback(array[index], index, array);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!fs.existsSync(dest)) {
|
||||||
|
fs.mkdirSync(dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
const download = async files => {
|
||||||
|
await asyncForEach(files, async file => {
|
||||||
|
const locale = file.replace('-', '_').replace('.json', '');
|
||||||
|
|
||||||
|
const filename = path.join(dest, file);
|
||||||
|
if (!fs.existsSync(filename)) {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
https.get(getUrl(locale), res => {
|
||||||
|
console.log('Downloaded', chalk.greenBright('->'), filename);
|
||||||
|
resolve(res.pipe(fs.createWriteStream(filename)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
download(files);
|
|
@ -6,6 +6,7 @@ import { removeTrailingSlash } from '../lib/url';
|
||||||
screen: { width, height },
|
screen: { width, height },
|
||||||
navigator: { language },
|
navigator: { language },
|
||||||
location: { hostname, pathname, search },
|
location: { hostname, pathname, search },
|
||||||
|
sessionStorage,
|
||||||
document,
|
document,
|
||||||
history,
|
history,
|
||||||
} = window;
|
} = window;
|
||||||
|
@ -16,9 +17,21 @@ import { removeTrailingSlash } from '../lib/url';
|
||||||
const website = attr('data-website-id');
|
const website = attr('data-website-id');
|
||||||
const hostUrl = attr('data-host-url');
|
const hostUrl = attr('data-host-url');
|
||||||
const autoTrack = attr('data-auto-track') !== 'false';
|
const autoTrack = attr('data-auto-track') !== 'false';
|
||||||
const dnt = attr('data-do-not-track') === 'true';
|
const dnt = attr('data-do-not-track');
|
||||||
|
const useCache = attr('data-cache');
|
||||||
|
const domains = attr('data-domains');
|
||||||
|
|
||||||
if (!script || (dnt && doNotTrack())) return;
|
if (
|
||||||
|
!script ||
|
||||||
|
(dnt && doNotTrack()) ||
|
||||||
|
(domains &&
|
||||||
|
!domains
|
||||||
|
.split(',')
|
||||||
|
.map(n => n.trim())
|
||||||
|
.includes(hostname))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const root = hostUrl
|
const root = hostUrl
|
||||||
? removeTrailingSlash(hostUrl)
|
? removeTrailingSlash(hostUrl)
|
||||||
|
@ -37,7 +50,7 @@ import { removeTrailingSlash } from '../lib/url';
|
||||||
|
|
||||||
req.onreadystatechange = () => {
|
req.onreadystatechange = () => {
|
||||||
if (req.readyState === 4) {
|
if (req.readyState === 4) {
|
||||||
callback && callback();
|
callback && callback(req.response);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,11 +58,14 @@ import { removeTrailingSlash } from '../lib/url';
|
||||||
};
|
};
|
||||||
|
|
||||||
const collect = (type, params, uuid) => {
|
const collect = (type, params, uuid) => {
|
||||||
|
const key = 'umami.cache';
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
website: uuid,
|
website: uuid,
|
||||||
hostname,
|
hostname,
|
||||||
screen,
|
screen,
|
||||||
language,
|
language,
|
||||||
|
cache: useCache && sessionStorage.getItem(key),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
|
@ -58,10 +74,14 @@ import { removeTrailingSlash } from '../lib/url';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return post(`${root}/api/collect`, {
|
return post(
|
||||||
|
`${root}/api/collect`,
|
||||||
|
{
|
||||||
type,
|
type,
|
||||||
payload,
|
payload,
|
||||||
});
|
},
|
||||||
|
res => useCache && sessionStorage.setItem(key, res),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackView = (url = currentUrl, referrer = currentRef, uuid = website) =>
|
const trackView = (url = currentUrl, referrer = currentRef, uuid = website) =>
|
||||||
|
|
Loading…
Reference in New Issue