Merge branch 'mikecao:master' into master
commit
c015880a4e
|
@ -4,7 +4,7 @@
|
||||||
"es2020": true,
|
"es2020": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"extends": ["eslint:recommended", "plugin:react/recommended", "prettier", "prettier/react"],
|
"extends": ["eslint:recommended", "plugin:react/recommended", "prettier"],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaFeatures": {
|
"ecmaFeatures": {
|
||||||
"jsx": true
|
"jsx": true
|
||||||
|
|
|
@ -28,9 +28,9 @@ jobs:
|
||||||
.
|
.
|
||||||
|
|
||||||
- name: Docker login
|
- name: Docker login
|
||||||
env:
|
run: >-
|
||||||
CR_PAT: ${{ secrets.CR_PAT }}
|
echo "${{ secrets.GITHUB_TOKEN }}"
|
||||||
run: docker login -u $GITHUB_ACTOR -p $CR_PAT ghcr.io
|
| docker login -u "${{ github.actor }}" --password-stdin ghcr.io
|
||||||
|
|
||||||
- name: Push image to GitHub
|
- name: Push image to GitHub
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M396.8 352h22.4c6.4 0 12.8-6.4 12.8-12.8V108.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v230.4c0 6.4 6.4 12.8 12.8 12.8zm-192 0h22.4c6.4 0 12.8-6.4 12.8-12.8V140.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v198.4c0 6.4 6.4 12.8 12.8 12.8zm96 0h22.4c6.4 0 12.8-6.4 12.8-12.8V204.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v134.4c0 6.4 6.4 12.8 12.8 12.8zM496 400H48V80c0-8.84-7.16-16-16-16H16C7.16 64 0 71.16 0 80v336c0 17.67 14.33 32 32 32h464c8.84 0 16-7.16 16-16v-16c0-8.84-7.16-16-16-16zm-387.2-48h22.4c6.4 0 12.8-6.4 12.8-12.8v-70.4c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v70.4c0 6.4 6.4 12.8 12.8 12.8z"/></svg>
|
After Width: | Height: | Size: 885 B |
|
@ -20,6 +20,7 @@ import Button from './Button';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import { dateFormat } from 'lib/date';
|
import { dateFormat } from 'lib/date';
|
||||||
import { chunk } from 'lib/array';
|
import { chunk } from 'lib/array';
|
||||||
|
import { dateLocales } from 'lib/lang';
|
||||||
import Chevron from 'assets/chevron-down.svg';
|
import Chevron from 'assets/chevron-down.svg';
|
||||||
import Cross from 'assets/times.svg';
|
import Cross from 'assets/times.svg';
|
||||||
import styles from './Calendar.module.css';
|
import styles from './Calendar.module.css';
|
||||||
|
@ -105,8 +106,8 @@ export default function Calendar({ date, minDate, maxDate, onChange }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => {
|
const DaySelector = ({ date, minDate, maxDate, locale, onSelect }) => {
|
||||||
const startWeek = startOfWeek(date);
|
const startWeek = startOfWeek(date, { locale: dateLocales[locale] });
|
||||||
const startMonth = startOfMonth(date);
|
const startMonth = startOfMonth(date, { locale: dateLocales[locale] });
|
||||||
const startDay = subDays(startMonth, startMonth.getDay());
|
const startDay = subDays(startMonth, startMonth.getDay());
|
||||||
const month = date.getMonth();
|
const month = date.getMonth();
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
|
|
|
@ -7,12 +7,14 @@ import styles from './Checkbox.module.css';
|
||||||
function Checkbox({ name, value, label, onChange }) {
|
function Checkbox({ name, value, label, onChange }) {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
|
|
||||||
|
const onClick = () => ref.current.click();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.checkbox} onClick={() => ref.current.click()}>
|
<div className={styles.checkbox} onClick={onClick}>
|
||||||
{value && <Icon icon={<Check />} size="small" />}
|
{value && <Icon icon={<Check />} size="small" />}
|
||||||
</div>
|
</div>
|
||||||
<label className={styles.label} htmlFor={name}>
|
<label className={styles.label} htmlFor={name} onClick={onClick}>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
@ -20,7 +22,7 @@ function Checkbox({ name, value, label, onChange }) {
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name={name}
|
name={name}
|
||||||
value={value}
|
defaultChecked={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
user-select: none; /* disable text selection when clicking to toggle the checkbox */
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
|
|
|
@ -55,6 +55,7 @@ const filterOptions = [
|
||||||
];
|
];
|
||||||
|
|
||||||
function DateFilter({ value, startDate, endDate, onChange, className }) {
|
function DateFilter({ value, startDate, endDate, onChange, className }) {
|
||||||
|
const [locale] = useLocale();
|
||||||
const [showPicker, setShowPicker] = useState(false);
|
const [showPicker, setShowPicker] = useState(false);
|
||||||
const displayValue =
|
const displayValue =
|
||||||
value === 'custom' ? (
|
value === 'custom' ? (
|
||||||
|
@ -68,7 +69,7 @@ function DateFilter({ value, startDate, endDate, onChange, className }) {
|
||||||
setShowPicker(true);
|
setShowPicker(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onChange(getDateRange(value));
|
onChange(getDateRange(value, locale));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePickerChange(value) {
|
function handlePickerChange(value) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ function EmptyPlaceholder({ msg, children }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
<Icon className={styles.icon} icon={<Logo />} size="xlarge" />
|
<Icon className={styles.icon} icon={<Logo />} size="xlarge" />
|
||||||
<h2>{msg}</h2>
|
<h2 className={styles.msg}>{msg}</h2>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,3 +9,7 @@
|
||||||
.icon {
|
.icon {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
|
@ -8,9 +8,11 @@ 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';
|
||||||
import { getDateRange } from '../../lib/date';
|
import { getDateRange } from '../../lib/date';
|
||||||
|
import useLocale from 'hooks/useLocale';
|
||||||
|
|
||||||
function RefreshButton({ websiteId }) {
|
function RefreshButton({ websiteId }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const [locale] = useLocale();
|
||||||
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 completed = useSelector(state => state.queries[`/api/website/${websiteId}/stats`]);
|
||||||
|
@ -18,7 +20,7 @@ function RefreshButton({ websiteId }) {
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (dateRange) {
|
if (dateRange) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
dispatch(setDateRange(websiteId, getDateRange(dateRange.value)));
|
dispatch(setDateRange(websiteId, getDateRange(dateRange.value, locale)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ReactTooltip from 'react-tooltip';
|
import ReactTooltip from 'react-tooltip';
|
||||||
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import useTheme from 'hooks/useTheme';
|
import useTheme from 'hooks/useTheme';
|
||||||
import { THEME_COLORS } from 'lib/constants';
|
import { ISO_COUNTRIES, THEME_COLORS, MAP_FILE } from 'lib/constants';
|
||||||
import styles from './WorldMap.module.css';
|
import styles from './WorldMap.module.css';
|
||||||
import useCountryNames from 'hooks/useCountryNames';
|
import useCountryNames from 'hooks/useCountryNames';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
|
|
||||||
const geoUrl = '/world-110m.json';
|
|
||||||
|
|
||||||
function WorldMap({ data, className }) {
|
function WorldMap({ data, className }) {
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
|
@ -60,10 +58,10 @@ function WorldMap({ data, className }) {
|
||||||
>
|
>
|
||||||
<ComposableMap projection="geoMercator">
|
<ComposableMap projection="geoMercator">
|
||||||
<ZoomableGroup zoom={0.8} minZoom={0.7} center={[0, 40]}>
|
<ZoomableGroup zoom={0.8} minZoom={0.7} center={[0, 40]}>
|
||||||
<Geographies geography={`${basePath}${geoUrl}`}>
|
<Geographies geography={`${basePath}${MAP_FILE}`}>
|
||||||
{({ geographies }) => {
|
{({ geographies }) => {
|
||||||
return geographies.map(geo => {
|
return geographies.map(geo => {
|
||||||
const code = geo.properties.ISO_A2;
|
const code = ISO_COUNTRIES[geo.id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Geography
|
<Geography
|
||||||
|
|
|
@ -72,12 +72,12 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
||||||
<FormattedMessage id="label.domain" defaultMessage="Domain" />
|
<FormattedMessage id="label.domain" defaultMessage="Domain" />
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<Field name="domain" type="text" />
|
<Field name="domain" type="text" placeholder="example.com" />
|
||||||
<FormError name="domain" />
|
<FormError name="domain" />
|
||||||
</div>
|
</div>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow>
|
<FormRow>
|
||||||
<label></label>
|
<label />
|
||||||
<Field name="enable_share_url">
|
<Field name="enable_share_url">
|
||||||
{({ field }) => (
|
{({ field }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
|
@ -11,14 +11,22 @@
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row > div {
|
.row > div {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
flex: 1 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row > div > input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -13,20 +13,43 @@ import styles from './Header.module.css';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const user = useSelector(state => state.user);
|
const user = useSelector(state => state.user);
|
||||||
|
const [active, setActive] = useState(false);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
setActive(state => !state);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="container">
|
<nav className="container">
|
||||||
{user?.is_admin && <UpdateNotice />}
|
{user?.is_admin && <UpdateNotice />}
|
||||||
<div className={classNames(styles.header, 'row align-items-center')}>
|
<div className={classNames(styles.header, 'row align-items-center')}>
|
||||||
<div className="col-6 col-lg-3 order-1 order-lg-1">
|
<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 ? '/' : 'https://umami.is'}>umami</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-lg-6 order-3 order-lg-2">
|
<button
|
||||||
|
onClick={handleClick}
|
||||||
|
role="button"
|
||||||
|
className={styles.burger}
|
||||||
|
aria-label="menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
{active ? (
|
||||||
|
<div> X </div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
{user && (
|
{user && (
|
||||||
<div className={styles.nav}>
|
<div className={styles.items}>
|
||||||
|
<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>
|
||||||
|
@ -37,9 +60,10 @@ export default function Header() {
|
||||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-6 col-lg-3 order-2 order-lg-3">
|
)}
|
||||||
|
<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" />
|
||||||
|
@ -47,6 +71,8 @@ export default function Header() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,19 @@
|
||||||
|
.navbar {
|
||||||
|
align-items: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
@ -15,6 +28,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
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;
|
||||||
|
@ -35,16 +57,83 @@
|
||||||
@media only screen and (max-width: 992px) {
|
@media only screen and (max-width: 992px) {
|
||||||
.nav {
|
.nav {
|
||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
padding: 20px 0;
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.items {
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 576px) {
|
@media only screen and (max-width: 768px) {
|
||||||
.header {
|
.header {
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
font-size: var(--font-size-normal);
|
font-size: var(--font-size-normal);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
display: flex;
|
||||||
|
justify-content: unset;
|
||||||
|
align-items: left;
|
||||||
|
font-size: var(--font-size-normal);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items > div {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .active {
|
||||||
|
display: inherit;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items a {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger {
|
||||||
|
display: block;
|
||||||
|
/* color: #4a4a4a; */
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--gray900);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 3.25rem;
|
||||||
|
width: 3.25rem;
|
||||||
|
margin-left: auto;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger span {
|
||||||
|
transform: translateX(25%);
|
||||||
|
padding: 1px 0px;
|
||||||
|
margin: 6px 0;
|
||||||
|
width: 65%;
|
||||||
|
display: block;
|
||||||
|
background-color: var(--gray900);
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger div {
|
||||||
|
/* height: 100%; */
|
||||||
|
color: var(--gray900);
|
||||||
|
text-align: center;
|
||||||
|
margin: auto;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
/* transform: translateX(-50%); */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,6 @@ export default function Layout({ title, children, header = true, footer = true }
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>umami{title && ` - ${title}`}</title>
|
<title>umami{title && ` - ${title}`}</title>
|
||||||
<link rel="icon" href="favicon.ico" />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
</Head>
|
</Head>
|
||||||
{header && <Header />}
|
{header && <Header />}
|
||||||
<main className="container">{children}</main>
|
<main className="container">{children}</main>
|
||||||
|
|
|
@ -40,7 +40,7 @@ export default function BarChart({
|
||||||
function renderXLabel(label, index, values) {
|
function renderXLabel(label, index, values) {
|
||||||
if (loading) return '';
|
if (loading) return '';
|
||||||
const d = new Date(values[index].value);
|
const d = new Date(values[index].value);
|
||||||
const w = canvas.current.width;
|
const sw = canvas.current.width / window.devicePixelRatio;
|
||||||
|
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case 'minute':
|
case 'minute':
|
||||||
|
@ -48,18 +48,27 @@ export default function BarChart({
|
||||||
case 'hour':
|
case 'hour':
|
||||||
return dateFormat(d, 'p', locale);
|
return dateFormat(d, 'p', locale);
|
||||||
case 'day':
|
case 'day':
|
||||||
if (records > 31) {
|
if (records > 25) {
|
||||||
if (w <= 500) {
|
if (sw <= 275) {
|
||||||
return index % 10 === 0 ? dateFormat(d, 'M/d', locale) : '';
|
return index % 10 === 0 ? dateFormat(d, 'M/d', locale) : '';
|
||||||
}
|
}
|
||||||
|
if (sw <= 550) {
|
||||||
return index % 5 === 0 ? dateFormat(d, 'M/d', locale) : '';
|
return index % 5 === 0 ? dateFormat(d, 'M/d', locale) : '';
|
||||||
}
|
}
|
||||||
if (w <= 500) {
|
if (sw <= 700) {
|
||||||
|
return index % 2 === 0 ? dateFormat(d, 'M/d', locale) : '';
|
||||||
|
}
|
||||||
|
return dateFormat(d, 'MMM d', locale);
|
||||||
|
}
|
||||||
|
if (sw <= 375) {
|
||||||
return index % 2 === 0 ? dateFormat(d, 'MMM d', locale) : '';
|
return index % 2 === 0 ? dateFormat(d, 'MMM d', locale) : '';
|
||||||
}
|
}
|
||||||
|
if (sw <= 425) {
|
||||||
|
return dateFormat(d, 'MMM d', locale);
|
||||||
|
}
|
||||||
return dateFormat(d, 'EEE M/d', locale);
|
return dateFormat(d, 'EEE M/d', locale);
|
||||||
case 'month':
|
case 'month':
|
||||||
if (w <= 660) {
|
if (sw <= 330) {
|
||||||
return index % 2 === 0 ? dateFormat(d, 'MMM', locale) : '';
|
return index % 2 === 0 ? dateFormat(d, 'MMM', locale) : '';
|
||||||
}
|
}
|
||||||
return dateFormat(d, 'MMM', locale);
|
return dateFormat(d, 'MMM', locale);
|
||||||
|
|
|
@ -23,6 +23,7 @@ export default function WebsiteChart({
|
||||||
domain,
|
domain,
|
||||||
stickyHeader = false,
|
stickyHeader = false,
|
||||||
showLink = false,
|
showLink = false,
|
||||||
|
hideChart = false,
|
||||||
onDataLoad = () => {},
|
onDataLoad = () => {},
|
||||||
}) {
|
}) {
|
||||||
const shareToken = useShareToken();
|
const shareToken = useShareToken();
|
||||||
|
@ -91,6 +92,7 @@ export default function WebsiteChart({
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
{error && <ErrorMessage />}
|
{error && <ErrorMessage />}
|
||||||
|
{!hideChart && (
|
||||||
<PageviewsChart
|
<PageviewsChart
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
|
@ -98,6 +100,7 @@ export default function WebsiteChart({
|
||||||
records={getDateLength(startDate, endDate, unit)}
|
records={getDateLength(startDate, endDate, unit)}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,28 +1,26 @@
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import Link from 'components/common/Link';
|
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 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';
|
||||||
|
|
||||||
export default function WebsiteList({ userId }) {
|
export default function WebsiteList({ userId }) {
|
||||||
const { data } = useFetch('/api/websites', { params: { user_id: userId } });
|
const { data } = useFetch('/api/websites', { params: { user_id: userId } });
|
||||||
|
const [hideCharts, setHideCharts] = useState(false);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
{data.map(({ website_id, name, domain }) => (
|
|
||||||
<div key={website_id} className={styles.website}>
|
|
||||||
<WebsiteChart websiteId={website_id} title={name} domain={domain} showLink />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{data.length === 0 && (
|
|
||||||
<EmptyPlaceholder
|
<EmptyPlaceholder
|
||||||
msg={
|
msg={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -35,7 +33,26 @@ export default function WebsiteList({ userId }) {
|
||||||
<FormattedMessage id="message.go-to-settings" defaultMessage="Go to settings" />
|
<FormattedMessage id="message.go-to-settings" defaultMessage="Go to settings" />
|
||||||
</Link>
|
</Link>
|
||||||
</EmptyPlaceholder>
|
</EmptyPlaceholder>
|
||||||
)}
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className={styles.menubar}>
|
||||||
|
<Button icon={<Chart />} onClick={() => setHideCharts(!hideCharts)} />
|
||||||
|
</div>
|
||||||
|
{data.map(({ website_id, name, domain }) => (
|
||||||
|
<div key={website_id} className={styles.website}>
|
||||||
|
<WebsiteChart
|
||||||
|
websiteId={website_id}
|
||||||
|
title={name}
|
||||||
|
domain={domain}
|
||||||
|
hideChart={hideCharts}
|
||||||
|
showLink
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,3 +9,10 @@
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menubar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
|
@ -6,13 +6,15 @@ 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 { 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;
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
setDateRange(getDateRange(DEFAULT_DATE_RANGE));
|
setDateRange(getDateRange(DEFAULT_DATE_RANGE, locale));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -16,12 +16,18 @@ export default function LanguageButton() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
{(locale === 'zh-CN' || locale === 'zh-TW') && (
|
{locale === 'zh-CN' && (
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{locale === 'zh-TW' && (
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{locale === 'ja-JP' && (
|
{locale === 'ja-JP' && (
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap"
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
padding-bottom: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button svg {
|
.button svg {
|
||||||
|
|
|
@ -5,9 +5,11 @@ import { getItem, setItem } from 'lib/web';
|
||||||
import { setDateRange } from '../redux/actions/websites';
|
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';
|
||||||
|
|
||||||
export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_RANGE) {
|
export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_RANGE) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const [locale] = useLocale();
|
||||||
const dateRange = useSelector(state => state.websites[websiteId]?.dateRange);
|
const dateRange = useSelector(state => state.websites[websiteId]?.dateRange);
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
|
@ -16,7 +18,7 @@ export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_
|
||||||
|
|
||||||
if (globalDefault) {
|
if (globalDefault) {
|
||||||
if (typeof globalDefault === 'string') {
|
if (typeof globalDefault === 'string') {
|
||||||
globalDateRange = getDateRange(globalDefault);
|
globalDateRange = getDateRange(globalDefault, locale);
|
||||||
} else if (typeof globalDefault === 'object') {
|
} else if (typeof globalDefault === 'object') {
|
||||||
globalDateRange = {
|
globalDateRange = {
|
||||||
...globalDefault,
|
...globalDefault,
|
||||||
|
@ -37,5 +39,5 @@ export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [dateRange || globalDateRange || getDateRange(defaultDateRange), saveDateRange];
|
return [dateRange || globalDateRange || getDateRange(defaultDateRange, locale), saveDateRange];
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,13 @@ import { THEME_CONFIG } from 'lib/constants';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export default function useTheme() {
|
export default function useTheme() {
|
||||||
const theme = useSelector(state => state.app.theme || getItem(THEME_CONFIG) || 'light');
|
const defaultTheme =
|
||||||
|
typeof window !== 'undefined'
|
||||||
|
? window?.matchMedia('prefers-color-scheme: dark')?.matches
|
||||||
|
? 'dark'
|
||||||
|
: 'light'
|
||||||
|
: 'light';
|
||||||
|
const theme = useSelector(state => state.app.theme || getItem(THEME_CONFIG) || defaultTheme);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
function saveTheme(value) {
|
function saveTheme(value) {
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
{
|
||||||
|
"label.accounts": "حساب ها",
|
||||||
|
"label.add-account": "افزودن حساب",
|
||||||
|
"label.add-website": "افزودن وب سایت",
|
||||||
|
"label.administrator": "مدیر",
|
||||||
|
"label.all": "همه",
|
||||||
|
"label.all-websites": "همه وب سایت ها",
|
||||||
|
"label.all-events": "همه رویداد ها",
|
||||||
|
"label.back": "برگشت",
|
||||||
|
"label.cancel": "انصراف",
|
||||||
|
"label.change-password": "تغییر رمز",
|
||||||
|
"label.confirm-password": "تایید رمز",
|
||||||
|
"label.copy-to-clipboard": "کپی به حافظه",
|
||||||
|
"label.current-password": "رمز فعلی",
|
||||||
|
"label.custom-range": "محدوده دلخواه",
|
||||||
|
"label.dashboard": "داشبورد",
|
||||||
|
"label.date-range": "محدوده تاریخ",
|
||||||
|
"label.default-date-range": "محدوده پیشفرض تاریخ",
|
||||||
|
"label.delete": "حذف",
|
||||||
|
"label.delete-account": "حذف حساب",
|
||||||
|
"label.delete-website": "حذف وب سایت",
|
||||||
|
"label.dismiss": "رد کردن",
|
||||||
|
"label.domain": "دامنه",
|
||||||
|
"label.edit": "ویرایش",
|
||||||
|
"label.edit-account": "ویرایش حساب",
|
||||||
|
"label.edit-website": "ویرایش وب سایت",
|
||||||
|
"label.enable-share-url": "فعال کردن اشتراک گذاری URL",
|
||||||
|
"label.invalid": "نامعتبر",
|
||||||
|
"label.invalid-domain": "دامنه نامعتبر",
|
||||||
|
"label.last-days": "لیست {x} روز",
|
||||||
|
"label.last-hours": "لیست {x} ساعت",
|
||||||
|
"label.logged-in-as": "وارد شده به عنوان {username}",
|
||||||
|
"label.login": "ورود",
|
||||||
|
"label.logout": "خروج",
|
||||||
|
"label.more": "بیشتر",
|
||||||
|
"label.name": "نام",
|
||||||
|
"label.new-password": "رمز جدید",
|
||||||
|
"label.password": "رمز",
|
||||||
|
"label.passwords-dont-match": "رمز ها یکسان نیستند",
|
||||||
|
"label.profile": "پروفایل",
|
||||||
|
"label.realtime": "آمار هم اکنون",
|
||||||
|
"label.realtime-logs": "لاگ های هم اکنون",
|
||||||
|
"label.refresh": "تازه کردن",
|
||||||
|
"label.required": "لازم",
|
||||||
|
"label.reset": "ریست",
|
||||||
|
"label.save": "ذخیره",
|
||||||
|
"label.settings": "تنظیمات",
|
||||||
|
"label.share-url": "به اشتراک گذاری URL",
|
||||||
|
"label.single-day": "یک روز",
|
||||||
|
"label.this-month": "این ماه",
|
||||||
|
"label.this-week": "این هفته",
|
||||||
|
"label.this-year": "امسال",
|
||||||
|
"label.timezone": "منطقه زمانی",
|
||||||
|
"label.today": "امروز",
|
||||||
|
"label.tracking-code": "کد رهگیری",
|
||||||
|
"label.unknown": "ناشناخته",
|
||||||
|
"label.username": "نام کاربری",
|
||||||
|
"label.view-details": "مشاهده جزئیات",
|
||||||
|
"label.websites": "وب سایت ها",
|
||||||
|
"message.active-users": "{x} هم اکنون {x, plural, one {یک} other {از میان}}",
|
||||||
|
"message.confirm-delete": "آیا مطمئن هستید می خواهید {target} را حذف کنید?",
|
||||||
|
"message.copied": "کپی شد!",
|
||||||
|
"message.delete-warning": "همه داده های مرتبط هم حذف خواهد شد.",
|
||||||
|
"message.failure": "مشکلی پیش آمده است.",
|
||||||
|
"message.get-share-url": "دریافت URL برای اشتراک گذاری",
|
||||||
|
"message.get-tracking-code": "گرفتن کد رهگیری",
|
||||||
|
"message.go-to-settings": "رفتن به تنظیمات",
|
||||||
|
"message.incorrect-username-password": "نام کاربری / رمز نادرست است.",
|
||||||
|
"message.log.visitor": "بازدید کننده از کشور {country} با مروگر {browser} در {os} {device}",
|
||||||
|
"message.new-version-available": "نسخه جدید umami ({version}) وجود است!",
|
||||||
|
"message.no-data-available": "اطلاعاتی موجود نیست.",
|
||||||
|
"message.no-websites-configured": "شما هیچ وب سایتی را پیکر بندی نکرده اید.",
|
||||||
|
"message.page-not-found": "صفحه یافت نشد.",
|
||||||
|
"message.powered-by": "قدرت گرفته توسط {name}",
|
||||||
|
"message.save-success": "با موفقیت ذخیره شد.",
|
||||||
|
"message.share-url": "این URL به اشتراک گذاشته شده عمومی برای {target} است.",
|
||||||
|
"message.track-stats": "برای ردیابی آمار {target}, کد روبرو را در قسمت {head} وب سایت قرار دهید.",
|
||||||
|
"message.type-delete": "جهت اطمینان '{delete}' را در کادر زیر بنویسید.",
|
||||||
|
"metrics.actions": "اقدامات",
|
||||||
|
"metrics.average-visit-time": "میانگین زمان بازدید",
|
||||||
|
"metrics.bounce-rate": "نرخ Bounce",
|
||||||
|
"metrics.browsers": "مروگر ها",
|
||||||
|
"metrics.countries": "کشور ها",
|
||||||
|
"metrics.device.desktop": "دسکتاپ",
|
||||||
|
"metrics.device.laptop": "لپ تاپ",
|
||||||
|
"metrics.device.mobile": "موبایل",
|
||||||
|
"metrics.device.tablet": "تبلت",
|
||||||
|
"metrics.devices": "دستگاه ها",
|
||||||
|
"metrics.events": "رویداد ها",
|
||||||
|
"metrics.filter.combined": "ترکیب شده",
|
||||||
|
"metrics.filter.domain-only": "فقط دامنه",
|
||||||
|
"metrics.filter.raw": "خام",
|
||||||
|
"metrics.operating-systems": "سیستم عامل ها",
|
||||||
|
"metrics.page-views": "بازدید صفحه",
|
||||||
|
"metrics.pages": "صفحه ها",
|
||||||
|
"metrics.referrers": "ارجاع دهندگان",
|
||||||
|
"metrics.unique-visitors": "بازدید کننده خالص",
|
||||||
|
"metrics.views": "بازدید",
|
||||||
|
"metrics.visitors": "بازدید کننده"
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
{
|
||||||
|
"label.accounts": "Akaun",
|
||||||
|
"label.add-account": "Tambah akaun",
|
||||||
|
"label.add-website": "Tambah laman web",
|
||||||
|
"label.administrator": "Pentadbir",
|
||||||
|
"label.all": "Semua",
|
||||||
|
"label.all-websites": "Semua laman web",
|
||||||
|
"label.all-events": "Semua peristiwa",
|
||||||
|
"label.back": "Kembali",
|
||||||
|
"label.cancel": "Batal",
|
||||||
|
"label.change-password": "Tukar kata laluan",
|
||||||
|
"label.confirm-password": "Sahkan kata laluan",
|
||||||
|
"label.copy-to-clipboard": "Salin ke papan keratan",
|
||||||
|
"label.current-password": "Kata laluan semasa",
|
||||||
|
"label.custom-range": "Julat khas",
|
||||||
|
"label.dashboard": "Papan pemuka",
|
||||||
|
"label.date-range": "Julat tarikh",
|
||||||
|
"label.default-date-range": "Julat tarikh lalai",
|
||||||
|
"label.delete": "Padam",
|
||||||
|
"label.delete-account": "Padam akaun",
|
||||||
|
"label.delete-website": "Padam laman web",
|
||||||
|
"label.dismiss": "Ketepikan",
|
||||||
|
"label.domain": "Domain",
|
||||||
|
"label.edit": "Edit",
|
||||||
|
"label.edit-account": "Edit akaun",
|
||||||
|
"label.edit-website": "Edit laman web",
|
||||||
|
"label.enable-share-url": "Aktifkan url berkongsi",
|
||||||
|
"label.invalid": "Tidak sah",
|
||||||
|
"label.invalid-domain": "Domain tidak sah",
|
||||||
|
"label.last-days": "{x} hari lepas",
|
||||||
|
"label.last-hours": "{x} jam lepas",
|
||||||
|
"label.logged-in-as": "Log masuk sebagai {username}",
|
||||||
|
"label.login": "Log masuk",
|
||||||
|
"label.logout": "Log keluar",
|
||||||
|
"label.more": "Lebih banyak lagi",
|
||||||
|
"label.name": "Nama",
|
||||||
|
"label.new-password": "Kata laluan baru",
|
||||||
|
"label.password": "Kata laluan",
|
||||||
|
"label.passwords-dont-match": "Kata laluan tidak sepadan",
|
||||||
|
"label.profile": "Profil",
|
||||||
|
"label.realtime": "Siaran langsung",
|
||||||
|
"label.realtime-logs": "Log secara siaran langsung",
|
||||||
|
"label.refresh": "Muat semula",
|
||||||
|
"label.required": "Diperlukan",
|
||||||
|
"label.reset": "Tetapkan semula",
|
||||||
|
"label.save": "Simpan",
|
||||||
|
"label.settings": "Tetapan",
|
||||||
|
"label.share-url": "Kongsikan URL",
|
||||||
|
"label.single-day": "Satu hari",
|
||||||
|
"label.this-month": "Bulan ini",
|
||||||
|
"label.this-week": "Minggu ini",
|
||||||
|
"label.this-year": "Tahun ini",
|
||||||
|
"label.timezone": "Zon masa",
|
||||||
|
"label.today": "Hari ini",
|
||||||
|
"label.tracking-code": "Kod penjejakan",
|
||||||
|
"label.unknown": "Tidak diketahui",
|
||||||
|
"label.username": "Nama pengguna",
|
||||||
|
"label.view-details": "Lihat butiran",
|
||||||
|
"label.websites": "Laman web",
|
||||||
|
"message.active-users": "{x} semasa {x, plural, one {pelawat} other {pelawat}}",
|
||||||
|
"message.confirm-delete": "Pastikah anda ingin memadam {target}?",
|
||||||
|
"message.copied": "Disalin!",
|
||||||
|
"message.delete-warning": "Semua data yang berkaitan juga akan dihapuskan.",
|
||||||
|
"message.failure": "Ada yang tidak kena.",
|
||||||
|
"message.get-share-url": "Dapatkan URL berkongsi",
|
||||||
|
"message.get-tracking-code": "Dapatkan kod penjejakan",
|
||||||
|
"message.go-to-settings": "Pergi ke tetapan",
|
||||||
|
"message.incorrect-username-password": "Pengguna/kata laluan tidak betul.",
|
||||||
|
"message.log.visitor": "Pelawat dari {country} mengguna {browser} pada {os} {device}",
|
||||||
|
"message.new-version-available": "Versi baru umami {version} boleh didapati!",
|
||||||
|
"message.no-data-available": "Tiada data yang boleh didapati.",
|
||||||
|
"message.no-websites-configured": "Anda tidak ada sebarang laman web yang telah dikonfigurasikan.",
|
||||||
|
"message.page-not-found": "Halaman tidak dijumpai.",
|
||||||
|
"message.powered-by": "Disediakan oleh {name}",
|
||||||
|
"message.save-success": "Berjaya disimpan.",
|
||||||
|
"message.share-url": "Ini adalah URL berkongsi untuk {target}.",
|
||||||
|
"message.track-stats": "Untuk menjejak statistik bagi {target}, letakkan kod berikut di bahagian {head} laman web anda.",
|
||||||
|
"message.type-delete": "Taip {delete} di dalam kotak di bawah untuk pengesahan.",
|
||||||
|
"metrics.actions": "Aksi",
|
||||||
|
"metrics.average-visit-time": "Purata tempoh masa lawatan",
|
||||||
|
"metrics.bounce-rate": "Kadar lantunan",
|
||||||
|
"metrics.browsers": "Pelayar web",
|
||||||
|
"metrics.countries": "Negara",
|
||||||
|
"metrics.device.desktop": "Desktop",
|
||||||
|
"metrics.device.laptop": "Laptop",
|
||||||
|
"metrics.device.mobile": "Telefon bimbit",
|
||||||
|
"metrics.device.tablet": "Tablet",
|
||||||
|
"metrics.devices": "Peranti",
|
||||||
|
"metrics.events": "Peristiwa",
|
||||||
|
"metrics.filter.combined": "Digabungkan",
|
||||||
|
"metrics.filter.domain-only": "Domain sahaja",
|
||||||
|
"metrics.filter.raw": "Mentah",
|
||||||
|
"metrics.operating-systems": "Sistem operasi",
|
||||||
|
"metrics.page-views": "Paparan halaman",
|
||||||
|
"metrics.pages": "Halaman",
|
||||||
|
"metrics.referrers": "Perujuk",
|
||||||
|
"metrics.unique-visitors": "Pelawat unik",
|
||||||
|
"metrics.views": "Lawatan",
|
||||||
|
"metrics.visitors": "Pelawat"
|
||||||
|
}
|
|
@ -5,6 +5,7 @@
|
||||||
"label.administrator": "Administrator",
|
"label.administrator": "Administrator",
|
||||||
"label.all": "Alles",
|
"label.all": "Alles",
|
||||||
"label.all-websites": "Alle websites",
|
"label.all-websites": "Alle websites",
|
||||||
|
"label.all-events": "Alle gebeurtenissen",
|
||||||
"label.back": "Terug",
|
"label.back": "Terug",
|
||||||
"label.cancel": "Annuleren",
|
"label.cancel": "Annuleren",
|
||||||
"label.change-password": "Wachtwoord wijzigen",
|
"label.change-password": "Wachtwoord wijzigen",
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
"label.last-days": "Ostatnie {x} dni",
|
"label.last-days": "Ostatnie {x} dni",
|
||||||
"label.last-hours": "Ostatnie {x} godzin",
|
"label.last-hours": "Ostatnie {x} godzin",
|
||||||
"label.logged-in-as": "Zalogowano jako {username}",
|
"label.logged-in-as": "Zalogowano jako {username}",
|
||||||
"label.login": "Zaloguj sie",
|
"label.login": "Zaloguj się",
|
||||||
"label.logout": "Wyloguj",
|
"label.logout": "Wyloguj",
|
||||||
"label.more": "Więcej",
|
"label.more": "Więcej",
|
||||||
"label.name": "Nazwa",
|
"label.name": "Nazwa",
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"label.administrator": "Администратор",
|
"label.administrator": "Администратор",
|
||||||
"label.all": "Все",
|
"label.all": "Все",
|
||||||
"label.all-websites": "Все сайты",
|
"label.all-websites": "Все сайты",
|
||||||
|
"label.all-events": "Все события",
|
||||||
"label.back": "Назад",
|
"label.back": "Назад",
|
||||||
"label.cancel": "Отменить",
|
"label.cancel": "Отменить",
|
||||||
"label.change-password": "Изменить пароль",
|
"label.change-password": "Изменить пароль",
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
{
|
||||||
|
"label.accounts": "Účty",
|
||||||
|
"label.add-account": "Pridať účet",
|
||||||
|
"label.add-website": "Pridať web",
|
||||||
|
"label.administrator": "Administrátor",
|
||||||
|
"label.all": "Všetko",
|
||||||
|
"label.all-websites": "Všetky weby",
|
||||||
|
"label.back": "Späť",
|
||||||
|
"label.cancel": "Zrušiť",
|
||||||
|
"label.change-password": "Zmeniť heslo",
|
||||||
|
"label.confirm-password": "Potvrdiť heslo",
|
||||||
|
"label.copy-to-clipboard": "Kopírovať do schránky",
|
||||||
|
"label.current-password": "Aktuálne heslo",
|
||||||
|
"label.custom-range": "Vlastný rozsah",
|
||||||
|
"label.dashboard": "Prehlad",
|
||||||
|
"label.date-range": "Obdobie",
|
||||||
|
"label.default-date-range": "Predvolené obdobie",
|
||||||
|
"label.delete": "Zmazať",
|
||||||
|
"label.delete-account": "Zmazať účet",
|
||||||
|
"label.delete-website": "Zmazať web",
|
||||||
|
"label.dismiss": "Odísť",
|
||||||
|
"label.domain": "Doména",
|
||||||
|
"label.edit": "Upraviť",
|
||||||
|
"label.edit-account": "Upraviť účet",
|
||||||
|
"label.edit-website": "Upraviť web",
|
||||||
|
"label.enable-share-url": "Povoliť zdielanie URL",
|
||||||
|
"label.invalid": "Neplatný",
|
||||||
|
"label.invalid-domain": "Neplatná doména",
|
||||||
|
"label.last-days": "Posledných {x} dní",
|
||||||
|
"label.last-hours": "Posledných {x} hodín",
|
||||||
|
"label.logged-in-as": "Prihlásený ako {username}",
|
||||||
|
"label.login": "Prihlásiť",
|
||||||
|
"label.logout": "Odhlásiť",
|
||||||
|
"label.more": "Viac",
|
||||||
|
"label.name": "Meno",
|
||||||
|
"label.new-password": "Nové heslo",
|
||||||
|
"label.password": "Heslo",
|
||||||
|
"label.passwords-dont-match": "Hesla se nezhodujú",
|
||||||
|
"label.profile": "Profil",
|
||||||
|
"label.realtime": "Aktuálne",
|
||||||
|
"label.realtime-logs": "Aktuálne záznamy",
|
||||||
|
"label.refresh": "Obnoviť",
|
||||||
|
"label.required": "Povinné",
|
||||||
|
"label.reset": "Reset",
|
||||||
|
"label.save": "Uložiť",
|
||||||
|
"label.settings": "Nastavenia",
|
||||||
|
"label.share-url": "Zdielanie URL",
|
||||||
|
"label.single-day": "Jeden deň",
|
||||||
|
"label.this-month": "Tento mesiac",
|
||||||
|
"label.this-week": "Tento týždeň",
|
||||||
|
"label.this-year": "Tento rok",
|
||||||
|
"label.timezone": "Časová zóna",
|
||||||
|
"label.today": "Dnes",
|
||||||
|
"label.tracking-code": "Sledovací kód",
|
||||||
|
"label.unknown": "Neznámý",
|
||||||
|
"label.username": "Užívateľské meno",
|
||||||
|
"label.view-details": "Zobraziť detaily",
|
||||||
|
"label.websites": "Weby",
|
||||||
|
"message.active-users": "{x} aktuálne {x, plural, one {návštevník} other {návštěvníci}}",
|
||||||
|
"message.confirm-delete": "Naozaj zmazať {target}?",
|
||||||
|
"message.copied": "Skopírované!",
|
||||||
|
"message.delete-warning": "Všetky príbuzné data budu tiež zmazané.",
|
||||||
|
"message.failure": "Niečo sa pokazilo.",
|
||||||
|
"message.get-share-url": "Získať zdielané URL",
|
||||||
|
"message.get-tracking-code": "Získať tracking kód",
|
||||||
|
"message.go-to-settings": "Ísť do nastavení",
|
||||||
|
"message.incorrect-username-password": "Nesprávné meno/heslo.",
|
||||||
|
"message.log.visitor": "Návštevník z {country} s prehliadačom {browser} na {os} {device}",
|
||||||
|
"message.new-version-available": "Nová verzia umami {version} je k dispozícii!",
|
||||||
|
"message.no-data-available": "Žiadne data.",
|
||||||
|
"message.no-websites-configured": "Nemáte nastavený žiadny web.",
|
||||||
|
"message.page-not-found": "Stránka sa nenašla.",
|
||||||
|
"message.powered-by": "Powered by {name}",
|
||||||
|
"message.save-success": "Úspešne uložené.",
|
||||||
|
"message.share-url": "Toto je zdielané URL pre {target}.",
|
||||||
|
"message.track-stats": "Pre sledovanie návštev na {target}, pridajte následujúci kód do {head} časti vašeho webu.",
|
||||||
|
"message.type-delete": "Napíšte {delete} pre potvrdenie.",
|
||||||
|
"metrics.actions": "Akcie",
|
||||||
|
"metrics.average-visit-time": "Priemerný čas návštevy",
|
||||||
|
"metrics.bounce-rate": "Okamžité opustenie",
|
||||||
|
"metrics.browsers": "Prehliadač",
|
||||||
|
"metrics.countries": "Zem",
|
||||||
|
"metrics.device.desktop": "Stolný počítač",
|
||||||
|
"metrics.device.laptop": "Prenosný počítač",
|
||||||
|
"metrics.device.mobile": "Mobilný telefon",
|
||||||
|
"metrics.device.tablet": "Tablet",
|
||||||
|
"metrics.devices": "Zariadenie",
|
||||||
|
"metrics.events": "Udalosti",
|
||||||
|
"metrics.filter.combined": "Kombinácie",
|
||||||
|
"metrics.filter.domain-only": "Domény",
|
||||||
|
"metrics.filter.raw": "Nezpracované",
|
||||||
|
"metrics.operating-systems": "Operačný systém",
|
||||||
|
"metrics.page-views": "Zobrazenie stánok",
|
||||||
|
"metrics.pages": "Stránky",
|
||||||
|
"metrics.referrers": "Odkazy",
|
||||||
|
"metrics.unique-visitors": "Jedinečné návštevy",
|
||||||
|
"metrics.views": "Zobrazení",
|
||||||
|
"metrics.visitors": "Návštevy"
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
"label.add-website": "வலைத்தளத்தைச் சேர்க்க",
|
"label.add-website": "வலைத்தளத்தைச் சேர்க்க",
|
||||||
"label.administrator": "நிர்வாகியைச் சேர்க்க",
|
"label.administrator": "நிர்வாகியைச் சேர்க்க",
|
||||||
"label.all": "எல்லாம்",
|
"label.all": "எல்லாம்",
|
||||||
|
"label.all-events": "அனைத்து நிகழ்வுகளும்",
|
||||||
"label.all-websites": "அனைத்து வலைத்தளங்களும்",
|
"label.all-websites": "அனைத்து வலைத்தளங்களும்",
|
||||||
"label.back": "பின்னால்",
|
"label.back": "பின்னால்",
|
||||||
"label.cancel": "ரத்துசெய்",
|
"label.cancel": "ரத்துசெய்",
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"label.administrator": "Адміністратор",
|
"label.administrator": "Адміністратор",
|
||||||
"label.all": "Всі",
|
"label.all": "Всі",
|
||||||
"label.all-websites": "Всі сайти",
|
"label.all-websites": "Всі сайти",
|
||||||
|
"label.all-events": "Всі події",
|
||||||
"label.back": "Назад",
|
"label.back": "Назад",
|
||||||
"label.cancel": "Відмінити",
|
"label.cancel": "Відмінити",
|
||||||
"label.change-password": "Змінити пароль",
|
"label.change-password": "Змінити пароль",
|
||||||
|
|
255
lib/constants.js
255
lib/constants.js
|
@ -86,6 +86,8 @@ export const DESKTOP_SCREEN_WIDTH = 1920;
|
||||||
export const LAPTOP_SCREEN_WIDTH = 1024;
|
export const LAPTOP_SCREEN_WIDTH = 1024;
|
||||||
export const MOBILE_SCREEN_WIDTH = 479;
|
export const MOBILE_SCREEN_WIDTH = 479;
|
||||||
|
|
||||||
|
export const URL_LENGTH = 500;
|
||||||
|
|
||||||
export const DESKTOP_OS = [
|
export const DESKTOP_OS = [
|
||||||
'Windows 3.11',
|
'Windows 3.11',
|
||||||
'Windows 95',
|
'Windows 95',
|
||||||
|
@ -140,3 +142,256 @@ export const BROWSERS = {
|
||||||
'ios-webview': 'iOS (webview)',
|
'ios-webview': 'iOS (webview)',
|
||||||
searchbot: 'Searchbot',
|
searchbot: 'Searchbot',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MAP_FILE = '/datamaps.world.json';
|
||||||
|
|
||||||
|
export const ISO_COUNTRIES = {
|
||||||
|
AFG: 'AF',
|
||||||
|
ALA: 'AX',
|
||||||
|
ALB: 'AL',
|
||||||
|
DZA: 'DZ',
|
||||||
|
ASM: 'AS',
|
||||||
|
AND: 'AD',
|
||||||
|
AGO: 'AO',
|
||||||
|
AIA: 'AI',
|
||||||
|
ATA: 'AQ',
|
||||||
|
ATG: 'AG',
|
||||||
|
ARG: 'AR',
|
||||||
|
ARM: 'AM',
|
||||||
|
ABW: 'AW',
|
||||||
|
AUS: 'AU',
|
||||||
|
AUT: 'AT',
|
||||||
|
AZE: 'AZ',
|
||||||
|
BHS: 'BS',
|
||||||
|
BHR: 'BH',
|
||||||
|
BGD: 'BD',
|
||||||
|
BRB: 'BB',
|
||||||
|
BLR: 'BY',
|
||||||
|
BEL: 'BE',
|
||||||
|
BLZ: 'BZ',
|
||||||
|
BEN: 'BJ',
|
||||||
|
BMU: 'BM',
|
||||||
|
BTN: 'BT',
|
||||||
|
BOL: 'BO',
|
||||||
|
BIH: 'BA',
|
||||||
|
BWA: 'BW',
|
||||||
|
BVT: 'BV',
|
||||||
|
BRA: 'BR',
|
||||||
|
VGB: 'VG',
|
||||||
|
IOT: 'IO',
|
||||||
|
BRN: 'BN',
|
||||||
|
BGR: 'BG',
|
||||||
|
BFA: 'BF',
|
||||||
|
BDI: 'BI',
|
||||||
|
KHM: 'KH',
|
||||||
|
CMR: 'CM',
|
||||||
|
CAN: 'CA',
|
||||||
|
CPV: 'CV',
|
||||||
|
CYM: 'KY',
|
||||||
|
CAF: 'CF',
|
||||||
|
TCD: 'TD',
|
||||||
|
CHL: 'CL',
|
||||||
|
CHN: 'CN',
|
||||||
|
HKG: 'HK',
|
||||||
|
MAC: 'MO',
|
||||||
|
CXR: 'CX',
|
||||||
|
CCK: 'CC',
|
||||||
|
COL: 'CO',
|
||||||
|
COM: 'KM',
|
||||||
|
COG: 'CG',
|
||||||
|
COD: 'CD',
|
||||||
|
COK: 'CK',
|
||||||
|
CRI: 'CR',
|
||||||
|
CIV: 'CI',
|
||||||
|
HRV: 'HR',
|
||||||
|
CUB: 'CU',
|
||||||
|
CYP: 'CY',
|
||||||
|
CZE: 'CZ',
|
||||||
|
DNK: 'DK',
|
||||||
|
DJI: 'DJ',
|
||||||
|
DMA: 'DM',
|
||||||
|
DOM: 'DO',
|
||||||
|
ECU: 'EC',
|
||||||
|
EGY: 'EG',
|
||||||
|
SLV: 'SV',
|
||||||
|
GNQ: 'GQ',
|
||||||
|
ERI: 'ER',
|
||||||
|
EST: 'EE',
|
||||||
|
ETH: 'ET',
|
||||||
|
FLK: 'FK',
|
||||||
|
FRO: 'FO',
|
||||||
|
FJI: 'FJ',
|
||||||
|
FIN: 'FI',
|
||||||
|
FRA: 'FR',
|
||||||
|
GUF: 'GF',
|
||||||
|
PYF: 'PF',
|
||||||
|
ATF: 'TF',
|
||||||
|
GAB: 'GA',
|
||||||
|
GMB: 'GM',
|
||||||
|
GEO: 'GE',
|
||||||
|
DEU: 'DE',
|
||||||
|
GHA: 'GH',
|
||||||
|
GIB: 'GI',
|
||||||
|
GRC: 'GR',
|
||||||
|
GRL: 'GL',
|
||||||
|
GRD: 'GD',
|
||||||
|
GLP: 'GP',
|
||||||
|
GUM: 'GU',
|
||||||
|
GTM: 'GT',
|
||||||
|
GGY: 'GG',
|
||||||
|
GIN: 'GN',
|
||||||
|
GNB: 'GW',
|
||||||
|
GUY: 'GY',
|
||||||
|
HTI: 'HT',
|
||||||
|
HMD: 'HM',
|
||||||
|
VAT: 'VA',
|
||||||
|
HND: 'HN',
|
||||||
|
HUN: 'HU',
|
||||||
|
ISL: 'IS',
|
||||||
|
IND: 'IN',
|
||||||
|
IDN: 'ID',
|
||||||
|
IRN: 'IR',
|
||||||
|
IRQ: 'IQ',
|
||||||
|
IRL: 'IE',
|
||||||
|
IMN: 'IM',
|
||||||
|
ISR: 'IL',
|
||||||
|
ITA: 'IT',
|
||||||
|
JAM: 'JM',
|
||||||
|
JPN: 'JP',
|
||||||
|
JEY: 'JE',
|
||||||
|
JOR: 'JO',
|
||||||
|
KAZ: 'KZ',
|
||||||
|
KEN: 'KE',
|
||||||
|
KIR: 'KI',
|
||||||
|
PRK: 'KP',
|
||||||
|
KOR: 'KR',
|
||||||
|
KWT: 'KW',
|
||||||
|
KGZ: 'KG',
|
||||||
|
LAO: 'LA',
|
||||||
|
LVA: 'LV',
|
||||||
|
LBN: 'LB',
|
||||||
|
LSO: 'LS',
|
||||||
|
LBR: 'LR',
|
||||||
|
LBY: 'LY',
|
||||||
|
LIE: 'LI',
|
||||||
|
LTU: 'LT',
|
||||||
|
LUX: 'LU',
|
||||||
|
MKD: 'MK',
|
||||||
|
MDG: 'MG',
|
||||||
|
MWI: 'MW',
|
||||||
|
MYS: 'MY',
|
||||||
|
MDV: 'MV',
|
||||||
|
MLI: 'ML',
|
||||||
|
MLT: 'MT',
|
||||||
|
MHL: 'MH',
|
||||||
|
MTQ: 'MQ',
|
||||||
|
MRT: 'MR',
|
||||||
|
MUS: 'MU',
|
||||||
|
MYT: 'YT',
|
||||||
|
MEX: 'MX',
|
||||||
|
FSM: 'FM',
|
||||||
|
MDA: 'MD',
|
||||||
|
MCO: 'MC',
|
||||||
|
MNG: 'MN',
|
||||||
|
MNE: 'ME',
|
||||||
|
MSR: 'MS',
|
||||||
|
MAR: 'MA',
|
||||||
|
MOZ: 'MZ',
|
||||||
|
MMR: 'MM',
|
||||||
|
NAM: 'NA',
|
||||||
|
NRU: 'NR',
|
||||||
|
NPL: 'NP',
|
||||||
|
NLD: 'NL',
|
||||||
|
ANT: 'AN',
|
||||||
|
NCL: 'NC',
|
||||||
|
NZL: 'NZ',
|
||||||
|
NIC: 'NI',
|
||||||
|
NER: 'NE',
|
||||||
|
NGA: 'NG',
|
||||||
|
NIU: 'NU',
|
||||||
|
NFK: 'NF',
|
||||||
|
MNP: 'MP',
|
||||||
|
NOR: 'NO',
|
||||||
|
OMN: 'OM',
|
||||||
|
PAK: 'PK',
|
||||||
|
PLW: 'PW',
|
||||||
|
PSE: 'PS',
|
||||||
|
PAN: 'PA',
|
||||||
|
PNG: 'PG',
|
||||||
|
PRY: 'PY',
|
||||||
|
PER: 'PE',
|
||||||
|
PHL: 'PH',
|
||||||
|
PCN: 'PN',
|
||||||
|
POL: 'PL',
|
||||||
|
PRT: 'PT',
|
||||||
|
PRI: 'PR',
|
||||||
|
QAT: 'QA',
|
||||||
|
REU: 'RE',
|
||||||
|
ROU: 'RO',
|
||||||
|
RUS: 'RU',
|
||||||
|
RWA: 'RW',
|
||||||
|
BLM: 'BL',
|
||||||
|
SHN: 'SH',
|
||||||
|
KNA: 'KN',
|
||||||
|
LCA: 'LC',
|
||||||
|
MAF: 'MF',
|
||||||
|
SPM: 'PM',
|
||||||
|
VCT: 'VC',
|
||||||
|
WSM: 'WS',
|
||||||
|
SMR: 'SM',
|
||||||
|
STP: 'ST',
|
||||||
|
SAU: 'SA',
|
||||||
|
SEN: 'SN',
|
||||||
|
SRB: 'RS',
|
||||||
|
SYC: 'SC',
|
||||||
|
SLE: 'SL',
|
||||||
|
SGP: 'SG',
|
||||||
|
SVK: 'SK',
|
||||||
|
SVN: 'SI',
|
||||||
|
SLB: 'SB',
|
||||||
|
SOM: 'SO',
|
||||||
|
ZAF: 'ZA',
|
||||||
|
SGS: 'GS',
|
||||||
|
SSD: 'SS',
|
||||||
|
ESP: 'ES',
|
||||||
|
LKA: 'LK',
|
||||||
|
SDN: 'SD',
|
||||||
|
SUR: 'SR',
|
||||||
|
SJM: 'SJ',
|
||||||
|
SWZ: 'SZ',
|
||||||
|
SWE: 'SE',
|
||||||
|
CHE: 'CH',
|
||||||
|
SYR: 'SY',
|
||||||
|
TWN: 'TW',
|
||||||
|
TJK: 'TJ',
|
||||||
|
TZA: 'TZ',
|
||||||
|
THA: 'TH',
|
||||||
|
TLS: 'TL',
|
||||||
|
TGO: 'TG',
|
||||||
|
TKL: 'TK',
|
||||||
|
TON: 'TO',
|
||||||
|
TTO: 'TT',
|
||||||
|
TUN: 'TN',
|
||||||
|
TUR: 'TR',
|
||||||
|
TKM: 'TM',
|
||||||
|
TCA: 'TC',
|
||||||
|
TUV: 'TV',
|
||||||
|
UGA: 'UG',
|
||||||
|
UKR: 'UA',
|
||||||
|
ARE: 'AE',
|
||||||
|
GBR: 'GB',
|
||||||
|
USA: 'US',
|
||||||
|
UMI: 'UM',
|
||||||
|
URY: 'UY',
|
||||||
|
UZB: 'UZ',
|
||||||
|
VUT: 'VU',
|
||||||
|
VEN: 'VE',
|
||||||
|
VNM: 'VN',
|
||||||
|
VIR: 'VI',
|
||||||
|
WLF: 'WF',
|
||||||
|
ESH: 'EH',
|
||||||
|
YEM: 'YE',
|
||||||
|
ZMB: 'ZM',
|
||||||
|
ZWE: 'ZW',
|
||||||
|
XKX: 'XK',
|
||||||
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { v4, v5, validate } from 'uuid';
|
import { v4, v5, validate } from 'uuid';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcryptjs';
|
||||||
import { JWT, JWE, JWK } from 'jose';
|
import { JWT, JWE, JWK } from 'jose';
|
||||||
import { startOfMonth } from 'date-fns';
|
import { startOfMonth } from 'date-fns';
|
||||||
|
|
||||||
|
@ -40,11 +40,11 @@ export function getRandomChars(n) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hashPassword(password) {
|
export async function hashPassword(password) {
|
||||||
return bcrypt.hash(password, SALT_ROUNDS);
|
return bcrypt.hashSync(password, SALT_ROUNDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkPassword(password, hash) {
|
export async function checkPassword(password, hash) {
|
||||||
return bcrypt.compare(password, hash);
|
return bcrypt.compareSync(password, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createToken(payload) {
|
export async function createToken(payload) {
|
||||||
|
|
|
@ -36,8 +36,9 @@ export function getLocalTime(t) {
|
||||||
return addMinutes(new Date(t), new Date().getTimezoneOffset());
|
return addMinutes(new Date(t), new Date().getTimezoneOffset());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDateRange(value) {
|
export function getDateRange(value, locale = 'en-US') {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const localeOptions = dateLocales[locale];
|
||||||
|
|
||||||
const { num, unit } = value.match(/^(?<num>[0-9]+)(?<unit>hour|day|week|month|year)$/).groups;
|
const { num, unit } = value.match(/^(?<num>[0-9]+)(?<unit>hour|day|week|month|year)$/).groups;
|
||||||
|
|
||||||
|
@ -52,8 +53,8 @@ export function getDateRange(value) {
|
||||||
};
|
};
|
||||||
case 'week':
|
case 'week':
|
||||||
return {
|
return {
|
||||||
startDate: startOfWeek(now),
|
startDate: startOfWeek(now, { locale: localeOptions }),
|
||||||
endDate: endOfWeek(now),
|
endDate: endOfWeek(now, { locale: localeOptions }),
|
||||||
unit: 'day',
|
unit: 'day',
|
||||||
value,
|
value,
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,7 +18,7 @@ export const urlFilter = (data, { raw }) => {
|
||||||
return `${pathname}${search}`;
|
return `${pathname}${search}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return removeTrailingSlash(pathname);
|
return pathname;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
15
lib/lang.js
15
lib/lang.js
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
cs,
|
cs,
|
||||||
|
sk,
|
||||||
da,
|
da,
|
||||||
de,
|
de,
|
||||||
el,
|
el,
|
||||||
|
@ -7,11 +8,13 @@ import {
|
||||||
es,
|
es,
|
||||||
fi,
|
fi,
|
||||||
fr,
|
fr,
|
||||||
|
faIR,
|
||||||
he,
|
he,
|
||||||
hi,
|
hi,
|
||||||
id,
|
id,
|
||||||
it,
|
it,
|
||||||
ja,
|
ja,
|
||||||
|
ms,
|
||||||
nb,
|
nb,
|
||||||
nl,
|
nl,
|
||||||
pl,
|
pl,
|
||||||
|
@ -49,11 +52,14 @@ import idMessages from 'lang-compiled/id-ID.json';
|
||||||
import ukMessages from 'lang-compiled/uk-UA.json';
|
import ukMessages from 'lang-compiled/uk-UA.json';
|
||||||
import fiMessages from 'lang-compiled/fi-FI.json';
|
import fiMessages from 'lang-compiled/fi-FI.json';
|
||||||
import csMessages from 'lang-compiled/cs-CZ.json';
|
import csMessages from 'lang-compiled/cs-CZ.json';
|
||||||
|
import skMessages from 'lang-compiled/sk-SK.json';
|
||||||
import plMessages from 'lang-compiled/pl-PL.json';
|
import plMessages from 'lang-compiled/pl-PL.json';
|
||||||
import taMessages from 'lang-compiled/ta-IN.json';
|
import taMessages from 'lang-compiled/ta-IN.json';
|
||||||
import hiMessages from 'lang-compiled/hi-IN.json';
|
import hiMessages from 'lang-compiled/hi-IN.json';
|
||||||
import heMessages from 'lang-compiled/he-IL.json';
|
import heMessages from 'lang-compiled/he-IL.json';
|
||||||
import itMessages from 'lang-compiled/it-IT.json';
|
import itMessages from 'lang-compiled/it-IT.json';
|
||||||
|
import faIRMessages from 'lang-compiled/fa-IR.json';
|
||||||
|
import msMYMessages from 'lang-compiled/ms-MY.json';
|
||||||
|
|
||||||
export const messages = {
|
export const messages = {
|
||||||
'en-US': enMessages,
|
'en-US': enMessages,
|
||||||
|
@ -79,11 +85,14 @@ export const messages = {
|
||||||
'uk-UA': ukMessages,
|
'uk-UA': ukMessages,
|
||||||
'fi-FI': fiMessages,
|
'fi-FI': fiMessages,
|
||||||
'cs-CZ': csMessages,
|
'cs-CZ': csMessages,
|
||||||
|
'sk-SK': skMessages,
|
||||||
'pl-PL': plMessages,
|
'pl-PL': plMessages,
|
||||||
'ta-IN': taMessages,
|
'ta-IN': taMessages,
|
||||||
'hi-IN': hiMessages,
|
'hi-IN': hiMessages,
|
||||||
'he-IL': heMessages,
|
'he-IL': heMessages,
|
||||||
'it-IT': itMessages,
|
'it-IT': itMessages,
|
||||||
|
'fa-IR': faIRMessages,
|
||||||
|
'ms-MY': msMYMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dateLocales = {
|
export const dateLocales = {
|
||||||
|
@ -110,11 +119,14 @@ export const dateLocales = {
|
||||||
'uk-UA': uk,
|
'uk-UA': uk,
|
||||||
'fi-FI': fi,
|
'fi-FI': fi,
|
||||||
'cs-CZ': cs,
|
'cs-CZ': cs,
|
||||||
|
'sk-SK': sk,
|
||||||
'pl-PL': pl,
|
'pl-PL': pl,
|
||||||
'ta-In': ta,
|
'ta-In': ta,
|
||||||
'hi-IN': hi,
|
'hi-IN': hi,
|
||||||
'he-IL': he,
|
'he-IL': he,
|
||||||
'it-IT': it,
|
'it-IT': it,
|
||||||
|
'fa-IR': faIR,
|
||||||
|
'ms-MY': ms,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const menuOptions = [
|
export const menuOptions = [
|
||||||
|
@ -125,6 +137,7 @@ export const menuOptions = [
|
||||||
{ label: 'Deutsch', value: 'de-DE', display: 'de' },
|
{ label: 'Deutsch', value: 'de-DE', display: 'de' },
|
||||||
{ label: 'English', value: 'en-US', display: 'en' },
|
{ label: 'English', value: 'en-US', display: 'en' },
|
||||||
{ label: 'Español', value: 'es-MX', display: 'es' },
|
{ label: 'Español', value: 'es-MX', display: 'es' },
|
||||||
|
{ label: 'فارسی', value: 'fa-IR', display: 'fa' },
|
||||||
{ 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' },
|
||||||
|
@ -133,6 +146,7 @@ export const menuOptions = [
|
||||||
{ label: 'Italiano', value: 'it-IT', display: 'it' },
|
{ label: 'Italiano', value: 'it-IT', display: 'it' },
|
||||||
{ label: 'Bahasa Indonesia', value: 'id-ID', display: 'id' },
|
{ label: 'Bahasa Indonesia', value: 'id-ID', display: 'id' },
|
||||||
{ label: '日本語', value: 'ja-JP', display: 'ja' },
|
{ label: '日本語', value: 'ja-JP', display: 'ja' },
|
||||||
|
{ label: 'Malay', value: 'ms-MY', display: 'ms' },
|
||||||
{ 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' },
|
||||||
{ label: 'Norsk Bokmål', value: 'nb-NO', display: 'nb' },
|
{ label: 'Norsk Bokmål', value: 'nb-NO', display: 'nb' },
|
||||||
|
@ -141,6 +155,7 @@ export const menuOptions = [
|
||||||
{ label: 'Português do Brasil', value: 'pt-BR', display: 'pt-BR' },
|
{ label: 'Português do Brasil', value: 'pt-BR', display: 'pt-BR' },
|
||||||
{ label: 'Русский', value: 'ru-RU', display: 'ru' },
|
{ label: 'Русский', value: 'ru-RU', display: 'ru' },
|
||||||
{ label: 'Română', value: 'ro-RO', display: 'ro' },
|
{ label: 'Română', value: 'ro-RO', display: 'ro' },
|
||||||
|
{ label: 'Slovenčina', value: 'sk-SK', display: 'sk' },
|
||||||
{ label: 'Suomi', value: 'fi-FI', display: 'fi' },
|
{ label: 'Suomi', value: 'fi-FI', display: 'fi' },
|
||||||
{ label: 'Svenska', value: 'sv-SE', display: 'sv' },
|
{ label: 'Svenska', value: 'sv-SE', display: 'sv' },
|
||||||
{ label: 'தமிழ்', value: 'ta-IN', display: 'ta' },
|
{ label: 'தமிழ்', value: 'ta-IN', display: 'ta' },
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import prisma from 'lib/db';
|
import prisma from 'lib/db';
|
||||||
import { subMinutes } from 'date-fns';
|
import { subMinutes } from 'date-fns';
|
||||||
import { MYSQL, POSTGRESQL, MYSQL_DATE_FORMATS, POSTGRESQL_DATE_FORMATS } from 'lib/constants';
|
import {
|
||||||
|
MYSQL,
|
||||||
|
POSTGRESQL,
|
||||||
|
MYSQL_DATE_FORMATS,
|
||||||
|
POSTGRESQL_DATE_FORMATS,
|
||||||
|
URL_LENGTH,
|
||||||
|
} from 'lib/constants';
|
||||||
|
|
||||||
export function getDatabase() {
|
export function getDatabase() {
|
||||||
const type =
|
const type =
|
||||||
|
@ -152,11 +158,7 @@ export async function createSession(website_id, data) {
|
||||||
return runQuery(
|
return runQuery(
|
||||||
prisma.session.create({
|
prisma.session.create({
|
||||||
data: {
|
data: {
|
||||||
website: {
|
|
||||||
connect: {
|
|
||||||
website_id,
|
website_id,
|
||||||
},
|
|
||||||
},
|
|
||||||
...data,
|
...data,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
|
@ -180,18 +182,10 @@ export async function savePageView(website_id, session_id, url, referrer) {
|
||||||
return runQuery(
|
return runQuery(
|
||||||
prisma.pageview.create({
|
prisma.pageview.create({
|
||||||
data: {
|
data: {
|
||||||
website: {
|
|
||||||
connect: {
|
|
||||||
website_id,
|
website_id,
|
||||||
},
|
|
||||||
},
|
|
||||||
session: {
|
|
||||||
connect: {
|
|
||||||
session_id,
|
session_id,
|
||||||
},
|
url: url?.substr(0, URL_LENGTH),
|
||||||
},
|
referrer: referrer?.substr(0, URL_LENGTH),
|
||||||
url,
|
|
||||||
referrer,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -201,19 +195,11 @@ export async function saveEvent(website_id, session_id, url, event_type, event_v
|
||||||
return runQuery(
|
return runQuery(
|
||||||
prisma.event.create({
|
prisma.event.create({
|
||||||
data: {
|
data: {
|
||||||
website: {
|
|
||||||
connect: {
|
|
||||||
website_id,
|
website_id,
|
||||||
},
|
|
||||||
},
|
|
||||||
session: {
|
|
||||||
connect: {
|
|
||||||
session_id,
|
session_id,
|
||||||
},
|
url: url?.substr(0, URL_LENGTH),
|
||||||
},
|
event_type: event_type?.substr(0, 50),
|
||||||
url,
|
event_value: event_value?.substr(0, 50),
|
||||||
event_type,
|
|
||||||
event_value,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,6 +12,8 @@ import {
|
||||||
MOBILE_SCREEN_WIDTH,
|
MOBILE_SCREEN_WIDTH,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
|
let lookup;
|
||||||
|
|
||||||
export function getIpAddress(req) {
|
export function getIpAddress(req) {
|
||||||
// Cloudflare
|
// Cloudflare
|
||||||
if (req.headers['cf-connecting-ip']) {
|
if (req.headers['cf-connecting-ip']) {
|
||||||
|
@ -61,7 +63,9 @@ export async function getCountry(req, ip) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Database lookup
|
// Database lookup
|
||||||
const lookup = await maxmind.open(path.resolve('./public/geo/GeoLite2-Country.mmdb'));
|
if (!lookup) {
|
||||||
|
lookup = await maxmind.open(path.resolve('./public/geo/GeoLite2-Country.mmdb'));
|
||||||
|
}
|
||||||
|
|
||||||
const result = lookup.get(ip);
|
const result = lookup.get(ip);
|
||||||
|
|
||||||
|
|
59
package.json
59
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "1.14.0",
|
"version": "1.17.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",
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
"build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma",
|
"build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma",
|
||||||
"copy-db-schema": "node scripts/copy-db-schema.js",
|
"copy-db-schema": "node scripts/copy-db-schema.js",
|
||||||
"generate-lang": "npm-run-all extract-lang merge-lang",
|
"generate-lang": "npm-run-all extract-lang merge-lang",
|
||||||
"extract-lang": "formatjs extract {pages,components}/**/*.js --out-file build/messages.json",
|
"extract-lang": "formatjs extract '{pages,components}/**/*.js' --out-file build/messages.json",
|
||||||
"merge-lang": "node scripts/merge-lang.js",
|
"merge-lang": "node scripts/merge-lang.js",
|
||||||
"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",
|
||||||
|
@ -56,12 +56,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "2.17.0",
|
"@prisma/client": "2.21.2",
|
||||||
"@reduxjs/toolkit": "^1.5.0",
|
"@reduxjs/toolkit": "^1.5.1",
|
||||||
"bcrypt": "^5.0.0",
|
"bcryptjs": "^2.4.3",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.1",
|
||||||
"chart.js": "^2.9.4",
|
"chart.js": "^2.9.4",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.3.1",
|
||||||
"cookie": "^0.4.1",
|
"cookie": "^0.4.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"date-fns": "^2.16.1",
|
"date-fns": "^2.16.1",
|
||||||
|
@ -70,27 +70,28 @@
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"formik": "^2.2.6",
|
"formik": "^2.2.6",
|
||||||
"immer": "^8.0.1",
|
"immer": "^8.0.1",
|
||||||
|
"ipaddr.js": "^2.0.0",
|
||||||
"is-localhost-ip": "^1.4.0",
|
"is-localhost-ip": "^1.4.0",
|
||||||
"isbot-fast": "^1.2.0",
|
"isbot": "^3.0.26",
|
||||||
"jose": "2.0.3",
|
"jose": "2.0.5",
|
||||||
"maxmind": "^4.3.1",
|
"maxmind": "^4.3.1",
|
||||||
"moment-timezone": "^0.5.32",
|
"moment-timezone": "^0.5.33",
|
||||||
"next": "^10.0.7",
|
"next": "^10.1.3",
|
||||||
"prompts": "2.4.0",
|
"prompts": "2.4.1",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.2",
|
||||||
"react-intl": "^5.12.3",
|
"react-intl": "^5.16.0",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.4",
|
||||||
"react-simple-maps": "^2.3.0",
|
"react-simple-maps": "^2.3.0",
|
||||||
"react-spring": "^8.0.27",
|
"react-spring": "^8.0.27",
|
||||||
"react-tooltip": "^4.2.14",
|
"react-tooltip": "^4.2.18",
|
||||||
"react-use-measure": "^2.0.3",
|
"react-use-measure": "^2.0.4",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.1.0",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"request-ip": "^2.1.3",
|
"request-ip": "^2.1.3",
|
||||||
"semver": "^7.3.4",
|
"semver": "^7.3.5",
|
||||||
"thenby": "^1.3.4",
|
"thenby": "^1.3.4",
|
||||||
"timezone-support": "^2.0.2",
|
"timezone-support": "^2.0.2",
|
||||||
"tinycolor2": "^1.4.2",
|
"tinycolor2": "^1.4.2",
|
||||||
|
@ -98,18 +99,17 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formatjs/cli": "^2.13.16",
|
"@formatjs/cli": "^2.13.16",
|
||||||
"@prisma/cli": "2.17.0",
|
|
||||||
"@rollup/plugin-buble": "^0.21.3",
|
"@rollup/plugin-buble": "^0.21.3",
|
||||||
"@rollup/plugin-node-resolve": "^11.1.1",
|
"@rollup/plugin-node-resolve": "^11.2.1",
|
||||||
"@rollup/plugin-replace": "^2.3.4",
|
"@rollup/plugin-replace": "^2.3.4",
|
||||||
"@svgr/webpack": "^5.5.0",
|
"@svgr/webpack": "^5.5.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"del": "^6.0.0",
|
"del": "^6.0.0",
|
||||||
"dotenv-cli": "^4.0.0",
|
"dotenv-cli": "^4.0.0",
|
||||||
"eslint": "^7.20.0",
|
"eslint": "^7.25.0",
|
||||||
"eslint-config-prettier": "^7.2.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
"eslint-plugin-react": "^7.22.0",
|
"eslint-plugin-react": "^7.23.2",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"extract-react-intl-messages": "^4.1.1",
|
"extract-react-intl-messages": "^4.1.1",
|
||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
|
@ -121,13 +121,14 @@
|
||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-preset-env": "^6.7.0",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"prettier-eslint": "^12.0.0",
|
"prettier-eslint": "^12.0.0",
|
||||||
"rollup": "^2.38.3",
|
"prisma": "2.21.2",
|
||||||
|
"rollup": "^2.45.2",
|
||||||
"rollup-plugin-hashbang": "^2.2.2",
|
"rollup-plugin-hashbang": "^2.2.2",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"stylelint": "^13.10.0",
|
"stylelint": "^13.13.0",
|
||||||
"stylelint-config-css-modules": "^2.2.0",
|
"stylelint-config-css-modules": "^2.2.0",
|
||||||
"stylelint-config-prettier": "^8.0.1",
|
"stylelint-config-prettier": "^8.0.1",
|
||||||
"stylelint-config-recommended": "^3.0.0",
|
"stylelint-config-recommended": "^5.0.0",
|
||||||
"tar": "^6.0.5"
|
"tar": "^6.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import { IntlProvider } from 'react-intl';
|
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';
|
||||||
|
@ -25,15 +26,22 @@ const Intl = ({ children }) => {
|
||||||
export default function App({ Component, pageProps }) {
|
export default function App({ Component, pageProps }) {
|
||||||
useForceSSL(process.env.FORCE_SSL);
|
useForceSSL(process.env.FORCE_SSL);
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const { basePath } = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Head>
|
<Head>
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
|
<link rel="icon" href={`${basePath}/favicon.ico`} />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href={`${basePath}/apple-touch-icon.png`} />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href={`${basePath}/favicon-32x32.png`} />
|
||||||
<link rel="manifest" href="site.webmanifest" />
|
<link rel="icon" type="image/png" sizes="16x16" href={`${basePath}/favicon-16x16.png`} />
|
||||||
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#5bbad5" />
|
<link rel="manifest" href={`${basePath}/site.webmanifest`} />
|
||||||
|
<link rel="mask-icon" href={`${basePath}/safari-pinned-tab.svg`} color="#5bbad5" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<meta name="msapplication-TileColor" content="#da532c" />
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
|
@ -1,22 +1,36 @@
|
||||||
import isBot from 'isbot-fast';
|
import isbot from 'isbot';
|
||||||
|
import ipaddr from 'ipaddr.js';
|
||||||
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 { getIpAddress } from 'lib/request';
|
||||||
import { ok, badRequest } from 'lib/response';
|
import { ok, badRequest } from 'lib/response';
|
||||||
import { createToken } from 'lib/crypto';
|
import { createToken } from 'lib/crypto';
|
||||||
import { getIpAddress } from '../../lib/request';
|
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useCors(req, res);
|
await useCors(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) {
|
if (process.env.IGNORE_IP) {
|
||||||
const ips = process.env.IGNORE_IP.split(',').map(n => n.trim());
|
const ips = process.env.IGNORE_IP.split(',').map(n => n.trim());
|
||||||
const ip = getIpAddress(req);
|
const ip = getIpAddress(req);
|
||||||
|
const blocked = ips.find(i => {
|
||||||
|
if (i === ip) return true;
|
||||||
|
|
||||||
if (ips.includes(ip)) {
|
// CIDR notation
|
||||||
|
if (i.indexOf('/') > 0) {
|
||||||
|
const addr = ipaddr.parse(ip);
|
||||||
|
const range = ipaddr.parseCIDR(i);
|
||||||
|
|
||||||
|
if (addr.match(range)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (blocked) {
|
||||||
return ok(res);
|
return ok(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `account` (
|
||||||
|
`user_id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` VARCHAR(255) NOT NULL,
|
||||||
|
`password` VARCHAR(60) NOT NULL,
|
||||||
|
`is_admin` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`created_at` TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`updated_at` TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
UNIQUE INDEX `account.username_unique`(`username`),
|
||||||
|
|
||||||
|
PRIMARY KEY (`user_id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `event` (
|
||||||
|
`event_id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`website_id` INTEGER UNSIGNED NOT NULL,
|
||||||
|
`session_id` INTEGER UNSIGNED NOT NULL,
|
||||||
|
`created_at` TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`url` VARCHAR(500) NOT NULL,
|
||||||
|
`event_type` VARCHAR(50) NOT NULL,
|
||||||
|
`event_value` VARCHAR(50) NOT NULL,
|
||||||
|
INDEX `event_created_at_idx`(`created_at`),
|
||||||
|
INDEX `event_session_id_idx`(`session_id`),
|
||||||
|
INDEX `event_website_id_idx`(`website_id`),
|
||||||
|
|
||||||
|
PRIMARY KEY (`event_id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `pageview` (
|
||||||
|
`view_id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`website_id` INTEGER UNSIGNED NOT NULL,
|
||||||
|
`session_id` INTEGER UNSIGNED NOT NULL,
|
||||||
|
`created_at` TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`url` VARCHAR(500) NOT NULL,
|
||||||
|
`referrer` VARCHAR(500),
|
||||||
|
INDEX `pageview_created_at_idx`(`created_at`),
|
||||||
|
INDEX `pageview_session_id_idx`(`session_id`),
|
||||||
|
INDEX `pageview_website_id_created_at_idx`(`website_id`, `created_at`),
|
||||||
|
INDEX `pageview_website_id_idx`(`website_id`),
|
||||||
|
INDEX `pageview_website_id_session_id_created_at_idx`(`website_id`, `session_id`, `created_at`),
|
||||||
|
|
||||||
|
PRIMARY KEY (`view_id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `session` (
|
||||||
|
`session_id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`session_uuid` VARCHAR(36) NOT NULL,
|
||||||
|
`website_id` INTEGER UNSIGNED NOT NULL,
|
||||||
|
`created_at` TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
`hostname` VARCHAR(100),
|
||||||
|
`browser` VARCHAR(20),
|
||||||
|
`os` VARCHAR(20),
|
||||||
|
`device` VARCHAR(20),
|
||||||
|
`screen` VARCHAR(11),
|
||||||
|
`language` VARCHAR(35),
|
||||||
|
`country` CHAR(2),
|
||||||
|
UNIQUE INDEX `session.session_uuid_unique`(`session_uuid`),
|
||||||
|
INDEX `session_created_at_idx`(`created_at`),
|
||||||
|
INDEX `session_website_id_idx`(`website_id`),
|
||||||
|
|
||||||
|
PRIMARY KEY (`session_id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `website` (
|
||||||
|
`website_id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`website_uuid` VARCHAR(36) NOT NULL,
|
||||||
|
`user_id` INTEGER UNSIGNED NOT NULL,
|
||||||
|
`name` VARCHAR(100) NOT NULL,
|
||||||
|
`domain` VARCHAR(500),
|
||||||
|
`share_id` VARCHAR(64),
|
||||||
|
`created_at` TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP(0),
|
||||||
|
UNIQUE INDEX `website.website_uuid_unique`(`website_uuid`),
|
||||||
|
UNIQUE INDEX `website.share_id_unique`(`share_id`),
|
||||||
|
INDEX `website_user_id_idx`(`user_id`),
|
||||||
|
|
||||||
|
PRIMARY KEY (`website_id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `event` ADD FOREIGN KEY (`session_id`) REFERENCES `session`(`session_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `event` ADD FOREIGN KEY (`website_id`) REFERENCES `website`(`website_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `pageview` ADD FOREIGN KEY (`session_id`) REFERENCES `session`(`session_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `pageview` ADD FOREIGN KEY (`website_id`) REFERENCES `website`(`website_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `session` ADD FOREIGN KEY (`website_id`) REFERENCES `website`(`website_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `website` ADD FOREIGN KEY (`user_id`) REFERENCES `account`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "mysql"
|
|
@ -0,0 +1 @@
|
||||||
|
../schema.mysql.prisma
|
|
@ -0,0 +1 @@
|
||||||
|
../seed.js
|
|
@ -0,0 +1,129 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "account" (
|
||||||
|
"user_id" SERIAL NOT NULL,
|
||||||
|
"username" VARCHAR(255) NOT NULL,
|
||||||
|
"password" VARCHAR(60) NOT NULL,
|
||||||
|
"is_admin" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
PRIMARY KEY ("user_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "event" (
|
||||||
|
"event_id" SERIAL NOT NULL,
|
||||||
|
"website_id" INTEGER NOT NULL,
|
||||||
|
"session_id" INTEGER NOT NULL,
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"url" VARCHAR(500) NOT NULL,
|
||||||
|
"event_type" VARCHAR(50) NOT NULL,
|
||||||
|
"event_value" VARCHAR(50) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY ("event_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "pageview" (
|
||||||
|
"view_id" SERIAL NOT NULL,
|
||||||
|
"website_id" INTEGER NOT NULL,
|
||||||
|
"session_id" INTEGER NOT NULL,
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"url" VARCHAR(500) NOT NULL,
|
||||||
|
"referrer" VARCHAR(500),
|
||||||
|
|
||||||
|
PRIMARY KEY ("view_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "session" (
|
||||||
|
"session_id" SERIAL NOT NULL,
|
||||||
|
"session_uuid" UUID NOT NULL,
|
||||||
|
"website_id" INTEGER NOT NULL,
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"hostname" VARCHAR(100),
|
||||||
|
"browser" VARCHAR(20),
|
||||||
|
"os" VARCHAR(20),
|
||||||
|
"device" VARCHAR(20),
|
||||||
|
"screen" VARCHAR(11),
|
||||||
|
"language" VARCHAR(35),
|
||||||
|
"country" CHAR(2),
|
||||||
|
|
||||||
|
PRIMARY KEY ("session_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "website" (
|
||||||
|
"website_id" SERIAL NOT NULL,
|
||||||
|
"website_uuid" UUID NOT NULL,
|
||||||
|
"user_id" INTEGER NOT NULL,
|
||||||
|
"name" VARCHAR(100) NOT NULL,
|
||||||
|
"domain" VARCHAR(500),
|
||||||
|
"share_id" VARCHAR(64),
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
PRIMARY KEY ("website_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "account.username_unique" ON "account"("username");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "event_created_at_idx" ON "event"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "event_session_id_idx" ON "event"("session_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "event_website_id_idx" ON "event"("website_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pageview_created_at_idx" ON "pageview"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pageview_session_id_idx" ON "pageview"("session_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pageview_website_id_created_at_idx" ON "pageview"("website_id", "created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pageview_website_id_idx" ON "pageview"("website_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pageview_website_id_session_id_created_at_idx" ON "pageview"("website_id", "session_id", "created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "session.session_uuid_unique" ON "session"("session_uuid");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_created_at_idx" ON "session"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_website_id_idx" ON "session"("website_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "website.website_uuid_unique" ON "website"("website_uuid");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "website.share_id_unique" ON "website"("share_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_user_id_idx" ON "website"("user_id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "event" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "event" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "pageview" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "pageview" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
|
@ -0,0 +1 @@
|
||||||
|
../schema.postgresql.prisma
|
|
@ -0,0 +1 @@
|
||||||
|
../seed.js
|
|
@ -8,23 +8,23 @@ datasource db {
|
||||||
}
|
}
|
||||||
|
|
||||||
model account {
|
model account {
|
||||||
user_id Int @default(autoincrement()) @id
|
user_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||||
username String @unique
|
username String @unique @db.VarChar(255)
|
||||||
password String
|
password String @db.VarChar(60)
|
||||||
is_admin Boolean @default(false)
|
is_admin Boolean @default(false)
|
||||||
created_at DateTime? @default(now())
|
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||||
updated_at DateTime? @default(now())
|
updated_at DateTime? @default(now()) @db.Timestamp(0)
|
||||||
website website[]
|
website website[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model event {
|
model event {
|
||||||
event_id Int @default(autoincrement()) @id
|
event_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||||
website_id Int
|
website_id Int @db.UnsignedInt
|
||||||
session_id Int
|
session_id Int @db.UnsignedInt
|
||||||
created_at DateTime? @default(now())
|
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||||
url String
|
url String @db.VarChar(500)
|
||||||
event_type String
|
event_type String @db.VarChar(50)
|
||||||
event_value String
|
event_value String @db.VarChar(50)
|
||||||
session session @relation(fields: [session_id], references: [session_id])
|
session session @relation(fields: [session_id], references: [session_id])
|
||||||
website website @relation(fields: [website_id], references: [website_id])
|
website website @relation(fields: [website_id], references: [website_id])
|
||||||
|
|
||||||
|
@ -34,32 +34,34 @@ model event {
|
||||||
}
|
}
|
||||||
|
|
||||||
model pageview {
|
model pageview {
|
||||||
view_id Int @default(autoincrement()) @id
|
view_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||||
website_id Int
|
website_id Int @db.UnsignedInt
|
||||||
session_id Int
|
session_id Int @db.UnsignedInt
|
||||||
created_at DateTime? @default(now())
|
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||||
url String
|
url String @db.VarChar(500)
|
||||||
referrer String?
|
referrer String? @db.VarChar(500)
|
||||||
session session @relation(fields: [session_id], references: [session_id])
|
session session @relation(fields: [session_id], references: [session_id])
|
||||||
website website @relation(fields: [website_id], references: [website_id])
|
website website @relation(fields: [website_id], references: [website_id])
|
||||||
|
|
||||||
@@index([created_at], name: "pageview_created_at_idx")
|
@@index([created_at], name: "pageview_created_at_idx")
|
||||||
@@index([session_id], name: "pageview_session_id_idx")
|
@@index([session_id], name: "pageview_session_id_idx")
|
||||||
|
@@index([website_id, created_at], name: "pageview_website_id_created_at_idx")
|
||||||
@@index([website_id], name: "pageview_website_id_idx")
|
@@index([website_id], name: "pageview_website_id_idx")
|
||||||
|
@@index([website_id, session_id, created_at], name: "pageview_website_id_session_id_created_at_idx")
|
||||||
}
|
}
|
||||||
|
|
||||||
model session {
|
model session {
|
||||||
session_id Int @default(autoincrement()) @id
|
session_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||||
session_uuid String @unique
|
session_uuid String @unique @db.VarChar(36)
|
||||||
website_id Int
|
website_id Int @db.UnsignedInt
|
||||||
created_at DateTime? @default(now())
|
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||||
hostname String?
|
hostname String? @db.VarChar(100)
|
||||||
browser String?
|
browser String? @db.VarChar(20)
|
||||||
os String?
|
os String? @db.VarChar(20)
|
||||||
device String?
|
device String? @db.VarChar(20)
|
||||||
screen String?
|
screen String? @db.VarChar(11)
|
||||||
language String?
|
language String? @db.VarChar(35)
|
||||||
country String?
|
country String? @db.Char(2)
|
||||||
website website @relation(fields: [website_id], references: [website_id])
|
website website @relation(fields: [website_id], references: [website_id])
|
||||||
event event[]
|
event event[]
|
||||||
pageview pageview[]
|
pageview pageview[]
|
||||||
|
@ -69,13 +71,13 @@ model session {
|
||||||
}
|
}
|
||||||
|
|
||||||
model website {
|
model website {
|
||||||
website_id Int @default(autoincrement()) @id
|
website_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||||
website_uuid String @unique
|
website_uuid String @unique @db.VarChar(36)
|
||||||
user_id Int
|
user_id Int @db.UnsignedInt
|
||||||
name String
|
name String @db.VarChar(100)
|
||||||
domain String?
|
domain String? @db.VarChar(500)
|
||||||
created_at DateTime? @default(now())
|
share_id String? @unique @db.VarChar(64)
|
||||||
share_id String? @unique
|
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||||
account account @relation(fields: [user_id], references: [user_id])
|
account account @relation(fields: [user_id], references: [user_id])
|
||||||
event event[]
|
event event[]
|
||||||
pageview pageview[]
|
pageview pageview[]
|
||||||
|
|
|
@ -8,23 +8,23 @@ datasource db {
|
||||||
}
|
}
|
||||||
|
|
||||||
model account {
|
model account {
|
||||||
user_id Int @default(autoincrement()) @id
|
user_id Int @id @default(autoincrement())
|
||||||
username String @unique
|
username String @unique @db.VarChar(255)
|
||||||
password String
|
password String @db.VarChar(60)
|
||||||
is_admin Boolean @default(false)
|
is_admin Boolean @default(false)
|
||||||
created_at DateTime? @default(now())
|
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
updated_at DateTime? @default(now())
|
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
website website[]
|
website website[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model event {
|
model event {
|
||||||
event_id Int @default(autoincrement()) @id
|
event_id Int @id @default(autoincrement())
|
||||||
website_id Int
|
website_id Int
|
||||||
session_id Int
|
session_id Int
|
||||||
created_at DateTime? @default(now())
|
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
url String
|
url String @db.VarChar(500)
|
||||||
event_type String
|
event_type String @db.VarChar(50)
|
||||||
event_value String
|
event_value String @db.VarChar(50)
|
||||||
session session @relation(fields: [session_id], references: [session_id])
|
session session @relation(fields: [session_id], references: [session_id])
|
||||||
website website @relation(fields: [website_id], references: [website_id])
|
website website @relation(fields: [website_id], references: [website_id])
|
||||||
|
|
||||||
|
@ -34,32 +34,34 @@ model event {
|
||||||
}
|
}
|
||||||
|
|
||||||
model pageview {
|
model pageview {
|
||||||
view_id Int @default(autoincrement()) @id
|
view_id Int @id @default(autoincrement())
|
||||||
website_id Int
|
website_id Int
|
||||||
session_id Int
|
session_id Int
|
||||||
created_at DateTime? @default(now())
|
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
url String
|
url String @db.VarChar(500)
|
||||||
referrer String?
|
referrer String? @db.VarChar(500)
|
||||||
session session @relation(fields: [session_id], references: [session_id])
|
session session @relation(fields: [session_id], references: [session_id])
|
||||||
website website @relation(fields: [website_id], references: [website_id])
|
website website @relation(fields: [website_id], references: [website_id])
|
||||||
|
|
||||||
@@index([created_at], name: "pageview_created_at_idx")
|
@@index([created_at], name: "pageview_created_at_idx")
|
||||||
@@index([session_id], name: "pageview_session_id_idx")
|
@@index([session_id], name: "pageview_session_id_idx")
|
||||||
|
@@index([website_id, created_at], name: "pageview_website_id_created_at_idx")
|
||||||
@@index([website_id], name: "pageview_website_id_idx")
|
@@index([website_id], name: "pageview_website_id_idx")
|
||||||
|
@@index([website_id, session_id, created_at], name: "pageview_website_id_session_id_created_at_idx")
|
||||||
}
|
}
|
||||||
|
|
||||||
model session {
|
model session {
|
||||||
session_id Int @default(autoincrement()) @id
|
session_id Int @id @default(autoincrement())
|
||||||
session_uuid String @unique
|
session_uuid String @unique @db.Uuid
|
||||||
website_id Int
|
website_id Int
|
||||||
created_at DateTime? @default(now())
|
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
hostname String?
|
hostname String? @db.VarChar(100)
|
||||||
browser String?
|
browser String? @db.VarChar(20)
|
||||||
os String?
|
os String? @db.VarChar(20)
|
||||||
screen String?
|
device String? @db.VarChar(20)
|
||||||
language String?
|
screen String? @db.VarChar(11)
|
||||||
country String?
|
language String? @db.VarChar(35)
|
||||||
device String?
|
country String? @db.Char(2)
|
||||||
website website @relation(fields: [website_id], references: [website_id])
|
website website @relation(fields: [website_id], references: [website_id])
|
||||||
event event[]
|
event event[]
|
||||||
pageview pageview[]
|
pageview pageview[]
|
||||||
|
@ -69,13 +71,13 @@ model session {
|
||||||
}
|
}
|
||||||
|
|
||||||
model website {
|
model website {
|
||||||
website_id Int @default(autoincrement()) @id
|
website_id Int @id @default(autoincrement())
|
||||||
website_uuid String @unique
|
website_uuid String @unique @db.Uuid
|
||||||
name String
|
|
||||||
created_at DateTime? @default(now())
|
|
||||||
user_id Int
|
user_id Int
|
||||||
domain String?
|
name String @db.VarChar(100)
|
||||||
share_id String? @unique
|
domain String? @db.VarChar(500)
|
||||||
|
share_id String? @unique @db.VarChar(64)
|
||||||
|
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
account account @relation(fields: [user_id], references: [user_id])
|
account account @relation(fields: [user_id], references: [user_id])
|
||||||
event event[]
|
event event[]
|
||||||
pageview pageview[]
|
pageview pageview[]
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
const SALT_ROUNDS = 10;
|
||||||
|
|
||||||
|
const hashPassword = password => {
|
||||||
|
return bcrypt.hash(password, SALT_ROUNDS);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const password = await hashPassword(process.env.ADMIN_PASSWORD || 'umami');
|
||||||
|
await prisma.account.upsert({
|
||||||
|
where: { username: 'admin' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
username: 'admin',
|
||||||
|
password: password,
|
||||||
|
is_admin: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
||||||
|
{"AF":"Afghanistan","ZA":"Afrika Selatan","AL":"Albania","DZ":"Algeria","US":"Amerika Syarikat","AD":"Andorra","AO":"Angola","AI":"Anguilla","AQ":"Antartika","AG":"Antigua dan Barbuda","SA":"Arab Saudi","AR":"Argentina","AM":"Armenia","AW":"Aruba","AU":"Australia","AT":"Austria","AZ":"Azerbaijan","BS":"Bahamas","BH":"Bahrain","BD":"Bangladesh","BB":"Barbados","NL":"Belanda","BQ":"Belanda Caribbean","BY":"Belarus","BE":"Belgium","BZ":"Belize","BJ":"Benin","BM":"Bermuda","BT":"Bhutan","BO":"Bolivia","BA":"Bosnia dan Herzegovina","BW":"Botswana","BR":"Brazil","BN":"Brunei","BG":"Bulgaria","BF":"Burkina Faso","BI":"Burundi","CM":"Cameroon","CV":"Cape Verde","TD":"Chad","CL":"Chile","CN":"China","CO":"Colombia","KM":"Comoros","CG":"Congo - Brazzaville","CD":"Congo - Kinshasa","CR":"Costa Rica","CI":"Cote d\u2019Ivoire","HR":"Croatia","CU":"Cuba","CW":"Curacao","CY":"Cyprus","CZ":"Czechia","DK":"Denmark","DJ":"Djibouti","DM":"Dominica","EC":"Ecuador","SV":"El Salvador","AE":"Emiriah Arab Bersatu","ER":"Eritrea","EE":"Estonia","ET":"Ethiopia","FJ":"Fiji","PH":"Filipina","FI":"Finland","GA":"Gabon","GM":"Gambia","GE":"Georgia","GH":"Ghana","GI":"Gibraltar","GR":"Greece","GL":"Greenland","GD":"Grenada","GP":"Guadeloupe","GU":"Guam","GT":"Guatemala","GG":"Guernsey","GF":"Guiana Perancis","GN":"Guinea","GW":"Guinea Bissau","GQ":"Guinea Khatulistiwa","GY":"Guyana","HT":"Haiti","HN":"Honduras","HK":"Hong Kong SAR China","HU":"Hungary","IS":"Iceland","IN":"India","ID":"Indonesia","IR":"Iran","IQ":"Iraq","IE":"Ireland","IM":"Isle of Man","IL":"Israel","IT":"Itali","JM":"Jamaica","JP":"Jepun","DE":"Jerman","JE":"Jersey","JO":"Jordan","CA":"Kanada","KZ":"Kazakhstan","KH":"Kemboja","KE":"Kenya","AX":"Kepulauan Aland","KY":"Kepulauan Cayman","CC":"Kepulauan Cocos (Keeling)","CK":"Kepulauan Cook","FK":"Kepulauan Falkland","FO":"Kepulauan Faroe","GS":"Kepulauan Georgia Selatan & Sandwich Selatan","HM":"Kepulauan Heard & McDonald","MP":"Kepulauan Mariana Utara","MH":"Kepulauan Marshall","PN":"Kepulauan Pitcairn","SB":"Kepulauan Solomon","UM":"Kepulauan Terpencil A.S.","TC":"Kepulauan Turks dan Caicos","VI":"Kepulauan Virgin A.S.","VG":"Kepulauan Virgin British","KI":"Kiribati","KR":"Korea Selatan","KP":"Korea Utara","VA":"Kota Vatican","KW":"Kuwait","KG":"Kyrgyzstan","LA":"Laos","LV":"Latvia","LS":"Lesotho","LR":"Liberia","LY":"Libya","LI":"Liechtenstein","LT":"Lithuania","LB":"Lubnan","LU":"Luxembourg","MO":"Macau SAR China","MK":"Macedonia Utara","MG":"Madagaskar","MA":"Maghribi","MW":"Malawi","MY":"Malaysia","MV":"Maldives","ML":"Mali","MT":"Malta","MQ":"Martinique","MR":"Mauritania","MU":"Mauritius","YT":"Mayotte","EG":"Mesir","MX":"Mexico","FM":"Micronesia","MD":"Moldova","MC":"Monaco","MN":"Mongolia","ME":"Montenegro","MS":"Montserrat","MZ":"Mozambique","MM":"Myanmar (Burma)","NA":"Namibia","NR":"Nauru","NP":"Nepal","NC":"New Caledonia","NZ":"New Zealand","NI":"Nicaragua","NE":"Niger","NG":"Nigeria","NU":"Niue","NO":"Norway","OM":"Oman","PK":"Pakistan","PW":"Palau","PA":"Panama","PG":"Papua New Guinea","PY":"Paraguay","FR":"Perancis","PE":"Peru","PL":"Poland","PF":"Polinesia Perancis","PT":"Portugal","PR":"Puerto Rico","BV":"Pulau Bouvet","CX":"Pulau Krismas","NF":"Pulau Norfolk","QA":"Qatar","CF":"Republik Afrika Tengah","DO":"Republik Dominica","RE":"Reunion","RO":"Romania","RU":"Rusia","RW":"Rwanda","EH":"Sahara Barat","SH":"Saint Helena","KN":"Saint Kitts dan Nevis","LC":"Saint Lucia","MF":"Saint Martin","PM":"Saint Pierre dan Miquelon","VC":"Saint Vincent dan Grenadines","WS":"Samoa","AS":"Samoa Amerika","SM":"San Marino","ST":"Sao Tome dan Principe","SN":"Senegal","ES":"Sepanyol","RS":"Serbia","SC":"Seychelles","SL":"Sierra Leone","SG":"Singapura","SX":"Sint Maarten","SK":"Slovakia","SI":"Slovenia","SO":"Somalia","LK":"Sri Lanka","BL":"St. Barthelemy","SD":"Sudan","SS":"Sudan Selatan","SR":"Surinam","SJ":"Svalbard dan Jan Mayen","SZ":"Swaziland","SE":"Sweden","CH":"Switzerland","SY":"Syria","TW":"Taiwan","TJ":"Tajikistan","TZ":"Tanzania","TH":"Thailand","TL":"Timor-Leste","TG":"Togo","TK":"Tokelau","TO":"Tonga","TT":"Trinidad dan Tobago","TN":"Tunisia","TR":"Turki","TM":"Turkmenistan","TV":"Tuvalu","UG":"Uganda","UA":"Ukraine","GB":"United Kingdom","UY":"Uruguay","UZ":"Uzbekistan","VU":"Vanuatu","VE":"Venezuela","VN":"Vietnam","WF":"Wallis dan Futuna","IO":"Wilayah Lautan Hindi British","PS":"Wilayah Palestin","TF":"Wilayah Selatan Perancis","YE":"Yaman","ZM":"Zambia","ZW":"Zimbabwe"}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -109,6 +109,7 @@ function mockPageView(
|
||||||
hostname: 'localhost',
|
hostname: 'localhost',
|
||||||
screen: '1680x1050',
|
screen: '1680x1050',
|
||||||
url: '/LOADTESTING',
|
url: '/LOADTESTING',
|
||||||
|
referrer: '/REFERRER',
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -22,7 +22,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.zh-TW {
|
.zh-TW {
|
||||||
font-family: 'Noto Sans SC', sans-serif !important;
|
font-family: 'Noto Sans TC', sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ja-JP {
|
.ja-JP {
|
||||||
|
|
Loading…
Reference in New Issue