mirror of
https://github.com/umami-software/umami.git
synced 2026-02-17 19:15:37 +01:00
Merge branch 'mikecao:master' into master
This commit is contained in:
commit
c015880a4e
62 changed files with 2258 additions and 1098 deletions
|
|
@ -4,7 +4,7 @@
|
|||
"es2020": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:react/recommended", "prettier", "prettier/react"],
|
||||
"extends": ["eslint:recommended", "plugin:react/recommended", "prettier"],
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
|
|
|
|||
6
.github/workflows/main.yml
vendored
6
.github/workflows/main.yml
vendored
|
|
@ -28,9 +28,9 @@ jobs:
|
|||
.
|
||||
|
||||
- name: Docker login
|
||||
env:
|
||||
CR_PAT: ${{ secrets.CR_PAT }}
|
||||
run: docker login -u $GITHUB_ACTOR -p $CR_PAT ghcr.io
|
||||
run: >-
|
||||
echo "${{ secrets.GITHUB_TOKEN }}"
|
||||
| docker login -u "${{ github.actor }}" --password-stdin ghcr.io
|
||||
|
||||
- name: Push image to GitHub
|
||||
run: |
|
||||
|
|
|
|||
1
assets/chart-bar.svg
Normal file
1
assets/chart-bar.svg
Normal file
|
|
@ -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 { dateFormat } from 'lib/date';
|
||||
import { chunk } from 'lib/array';
|
||||
import { dateLocales } from 'lib/lang';
|
||||
import Chevron from 'assets/chevron-down.svg';
|
||||
import Cross from 'assets/times.svg';
|
||||
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 startWeek = startOfWeek(date);
|
||||
const startMonth = startOfMonth(date);
|
||||
const startWeek = startOfWeek(date, { locale: dateLocales[locale] });
|
||||
const startMonth = startOfMonth(date, { locale: dateLocales[locale] });
|
||||
const startDay = subDays(startMonth, startMonth.getDay());
|
||||
const month = date.getMonth();
|
||||
const year = date.getFullYear();
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@ import styles from './Checkbox.module.css';
|
|||
function Checkbox({ name, value, label, onChange }) {
|
||||
const ref = useRef();
|
||||
|
||||
const onClick = () => ref.current.click();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.checkbox} onClick={() => ref.current.click()}>
|
||||
<div className={styles.checkbox} onClick={onClick}>
|
||||
{value && <Icon icon={<Check />} size="small" />}
|
||||
</div>
|
||||
<label className={styles.label} htmlFor={name}>
|
||||
<label className={styles.label} htmlFor={name} onClick={onClick}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -20,7 +22,7 @@ function Checkbox({ name, value, label, onChange }) {
|
|||
className={styles.input}
|
||||
type="checkbox"
|
||||
name={name}
|
||||
value={value}
|
||||
defaultChecked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
.label {
|
||||
margin-left: 10px;
|
||||
user-select: none; /* disable text selection when clicking to toggle the checkbox */
|
||||
}
|
||||
|
||||
.input {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ const filterOptions = [
|
|||
];
|
||||
|
||||
function DateFilter({ value, startDate, endDate, onChange, className }) {
|
||||
const [locale] = useLocale();
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const displayValue =
|
||||
value === 'custom' ? (
|
||||
|
|
@ -68,7 +69,7 @@ function DateFilter({ value, startDate, endDate, onChange, className }) {
|
|||
setShowPicker(true);
|
||||
return;
|
||||
}
|
||||
onChange(getDateRange(value));
|
||||
onChange(getDateRange(value, locale));
|
||||
}
|
||||
|
||||
function handlePickerChange(value) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ function EmptyPlaceholder({ msg, children }) {
|
|||
return (
|
||||
<div className={styles.placeholder}>
|
||||
<Icon className={styles.icon} icon={<Logo />} size="xlarge" />
|
||||
<h2>{msg}</h2>
|
||||
<h2 className={styles.msg}>{msg}</h2>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,3 +9,7 @@
|
|||
.icon {
|
||||
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 useDateRange from 'hooks/useDateRange';
|
||||
import { getDateRange } from '../../lib/date';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
function RefreshButton({ websiteId }) {
|
||||
const dispatch = useDispatch();
|
||||
const [locale] = useLocale();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const completed = useSelector(state => state.queries[`/api/website/${websiteId}/stats`]);
|
||||
|
|
@ -18,7 +20,7 @@ function RefreshButton({ websiteId }) {
|
|||
function handleClick() {
|
||||
if (dateRange) {
|
||||
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 { useRouter } from 'next/router';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
||||
import classNames from 'classnames';
|
||||
import tinycolor from 'tinycolor2';
|
||||
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 useCountryNames from 'hooks/useCountryNames';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const geoUrl = '/world-110m.json';
|
||||
|
||||
function WorldMap({ data, className }) {
|
||||
const { basePath } = useRouter();
|
||||
|
|
@ -60,10 +58,10 @@ function WorldMap({ data, className }) {
|
|||
>
|
||||
<ComposableMap projection="geoMercator">
|
||||
<ZoomableGroup zoom={0.8} minZoom={0.7} center={[0, 40]}>
|
||||
<Geographies geography={`${basePath}${geoUrl}`}>
|
||||
<Geographies geography={`${basePath}${MAP_FILE}`}>
|
||||
{({ geographies }) => {
|
||||
return geographies.map(geo => {
|
||||
const code = geo.properties.ISO_A2;
|
||||
const code = ISO_COUNTRIES[geo.id];
|
||||
|
||||
return (
|
||||
<Geography
|
||||
|
|
|
|||
|
|
@ -72,12 +72,12 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
|||
<FormattedMessage id="label.domain" defaultMessage="Domain" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="domain" type="text" />
|
||||
<Field name="domain" type="text" placeholder="example.com" />
|
||||
<FormError name="domain" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label></label>
|
||||
<label />
|
||||
<Field name="enable_share_url">
|
||||
{({ field }) => (
|
||||
<Checkbox
|
||||
|
|
|
|||
|
|
@ -11,14 +11,22 @@
|
|||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.8;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.row > div {
|
||||
position: relative;
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
.row > div > input {
|
||||
width: 100%;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useSelector } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
|
@ -13,40 +13,66 @@ import styles from './Header.module.css';
|
|||
|
||||
export default function Header() {
|
||||
const user = useSelector(state => state.user);
|
||||
const [active, setActive] = useState(false);
|
||||
|
||||
function handleClick() {
|
||||
setActive(state => !state);
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="container">
|
||||
<nav className="container">
|
||||
{user?.is_admin && <UpdateNotice />}
|
||||
<div className={classNames(styles.header, 'row align-items-center')}>
|
||||
<div className="col-6 col-lg-3 order-1 order-lg-1">
|
||||
<div className={styles.title}>
|
||||
<Icon icon={<Logo />} size="large" className={styles.logo} />
|
||||
<Link href={user ? '/' : 'https://umami.is'}>umami</Link>
|
||||
<div className={styles.nav}>
|
||||
<div className="">
|
||||
<div className={styles.title}>
|
||||
<Icon icon={<Logo />} size="large" className={styles.logo} />
|
||||
<Link href={user ? '/' : 'https://umami.is'}>umami</Link>
|
||||
</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 && (
|
||||
<div className={styles.nav}>
|
||||
<Link href="/dashboard">
|
||||
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
|
||||
</Link>
|
||||
<Link href="/realtime">
|
||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||
</Link>
|
||||
<Link href="/settings">
|
||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||
</Link>
|
||||
<div className={styles.items}>
|
||||
<div className={active ? classNames(styles.active) : ''}>
|
||||
<Link href="/dashboard">
|
||||
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
|
||||
</Link>
|
||||
<Link href="/realtime">
|
||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||
</Link>
|
||||
<Link href="/settings">
|
||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-6 col-lg-3 order-2 order-lg-3">
|
||||
<div className={styles.buttons}>
|
||||
<ThemeButton />
|
||||
<LanguageButton menuAlign="right" />
|
||||
{user && <UserButton />}
|
||||
<div className={styles.items}>
|
||||
<div className={active ? classNames(styles.active) : ''}>
|
||||
<div className={styles.buttons}>
|
||||
<ThemeButton />
|
||||
<LanguageButton menuAlign="right" />
|
||||
{user && <UserButton />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,19 @@
|
|||
.navbar {
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.burger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
min-height: 100px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
|
@ -15,6 +28,15 @@
|
|||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-normal);
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
@ -35,16 +57,83 @@
|
|||
@media only screen and (max-width: 992px) {
|
||||
.nav {
|
||||
font-size: var(--font-size-large);
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
justify-content: space-between;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.items {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
@media only screen and (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav {
|
||||
font-size: var(--font-size-normal);
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
justify-content: unset;
|
||||
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>
|
||||
<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>
|
||||
{header && <Header />}
|
||||
<main className="container">{children}</main>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export default function BarChart({
|
|||
function renderXLabel(label, index, values) {
|
||||
if (loading) return '';
|
||||
const d = new Date(values[index].value);
|
||||
const w = canvas.current.width;
|
||||
const sw = canvas.current.width / window.devicePixelRatio;
|
||||
|
||||
switch (unit) {
|
||||
case 'minute':
|
||||
|
|
@ -48,18 +48,27 @@ export default function BarChart({
|
|||
case 'hour':
|
||||
return dateFormat(d, 'p', locale);
|
||||
case 'day':
|
||||
if (records > 31) {
|
||||
if (w <= 500) {
|
||||
if (records > 25) {
|
||||
if (sw <= 275) {
|
||||
return index % 10 === 0 ? dateFormat(d, 'M/d', locale) : '';
|
||||
}
|
||||
return index % 5 === 0 ? dateFormat(d, 'M/d', locale) : '';
|
||||
if (sw <= 550) {
|
||||
return index % 5 === 0 ? dateFormat(d, 'M/d', locale) : '';
|
||||
}
|
||||
if (sw <= 700) {
|
||||
return index % 2 === 0 ? dateFormat(d, 'M/d', locale) : '';
|
||||
}
|
||||
return dateFormat(d, 'MMM d', locale);
|
||||
}
|
||||
if (w <= 500) {
|
||||
if (sw <= 375) {
|
||||
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);
|
||||
case 'month':
|
||||
if (w <= 660) {
|
||||
if (sw <= 330) {
|
||||
return index % 2 === 0 ? dateFormat(d, 'MMM', locale) : '';
|
||||
}
|
||||
return dateFormat(d, 'MMM', locale);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export default function WebsiteChart({
|
|||
domain,
|
||||
stickyHeader = false,
|
||||
showLink = false,
|
||||
hideChart = false,
|
||||
onDataLoad = () => {},
|
||||
}) {
|
||||
const shareToken = useShareToken();
|
||||
|
|
@ -91,13 +92,15 @@ export default function WebsiteChart({
|
|||
<div className="row">
|
||||
<div className="col">
|
||||
{error && <ErrorMessage />}
|
||||
<PageviewsChart
|
||||
websiteId={websiteId}
|
||||
data={chartData}
|
||||
unit={unit}
|
||||
records={getDateLength(startDate, endDate, unit)}
|
||||
loading={loading}
|
||||
/>
|
||||
{!hideChart && (
|
||||
<PageviewsChart
|
||||
websiteId={websiteId}
|
||||
data={chartData}
|
||||
unit={unit}
|
||||
records={getDateLength(startDate, endDate, unit)}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,26 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Link from 'components/common/Link';
|
||||
import WebsiteChart from 'components/metrics/WebsiteChart';
|
||||
import Page from 'components/layout/Page';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import Button from 'components/common/Button';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import Arrow from 'assets/arrow-right.svg';
|
||||
import Chart from 'assets/chart-bar.svg';
|
||||
import styles from './WebsiteList.module.css';
|
||||
|
||||
export default function WebsiteList({ userId }) {
|
||||
const { data } = useFetch('/api/websites', { params: { user_id: userId } });
|
||||
const [hideCharts, setHideCharts] = useState(false);
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<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 && (
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Page>
|
||||
<EmptyPlaceholder
|
||||
msg={
|
||||
<FormattedMessage
|
||||
|
|
@ -35,7 +33,26 @@ export default function WebsiteList({ userId }) {
|
|||
<FormattedMessage id="message.go-to-settings" defaultMessage="Go to settings" />
|
||||
</Link>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,3 +9,10 @@
|
|||
border-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 { getDateRange } from 'lib/date';
|
||||
import styles from './DateRangeSetting.module.css';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export default function DateRangeSetting() {
|
||||
const [locale] = useLocale();
|
||||
const [dateRange, setDateRange] = useDateRange();
|
||||
const { startDate, endDate, value } = dateRange;
|
||||
|
||||
function handleReset() {
|
||||
setDateRange(getDateRange(DEFAULT_DATE_RANGE));
|
||||
setDateRange(getDateRange(DEFAULT_DATE_RANGE, locale));
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -16,12 +16,18 @@ export default function LanguageButton() {
|
|||
return (
|
||||
<>
|
||||
<Head>
|
||||
{(locale === 'zh-CN' || locale === 'zh-TW') && (
|
||||
{locale === 'zh-CN' && (
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap"
|
||||
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' && (
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.button svg {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import { getItem, setItem } from 'lib/web';
|
|||
import { setDateRange } from '../redux/actions/websites';
|
||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||
import useForceUpdate from './useForceUpdate';
|
||||
import useLocale from './useLocale';
|
||||
|
||||
export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_RANGE) {
|
||||
const dispatch = useDispatch();
|
||||
const [locale] = useLocale();
|
||||
const dateRange = useSelector(state => state.websites[websiteId]?.dateRange);
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
|
|
@ -16,7 +18,7 @@ export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_
|
|||
|
||||
if (globalDefault) {
|
||||
if (typeof globalDefault === 'string') {
|
||||
globalDateRange = getDateRange(globalDefault);
|
||||
globalDateRange = getDateRange(globalDefault, locale);
|
||||
} else if (typeof globalDefault === 'object') {
|
||||
globalDateRange = {
|
||||
...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';
|
||||
|
||||
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();
|
||||
|
||||
function saveTheme(value) {
|
||||
|
|
|
|||
100
lang/fa-IR.json
Normal file
100
lang/fa-IR.json
Normal file
|
|
@ -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": "بازدید کننده"
|
||||
}
|
||||
100
lang/ms-MY.json
Normal file
100
lang/ms-MY.json
Normal file
|
|
@ -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.all": "Alles",
|
||||
"label.all-websites": "Alle websites",
|
||||
"label.all-events": "Alle gebeurtenissen",
|
||||
"label.back": "Terug",
|
||||
"label.cancel": "Annuleren",
|
||||
"label.change-password": "Wachtwoord wijzigen",
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
"label.last-days": "Ostatnie {x} dni",
|
||||
"label.last-hours": "Ostatnie {x} godzin",
|
||||
"label.logged-in-as": "Zalogowano jako {username}",
|
||||
"label.login": "Zaloguj sie",
|
||||
"label.login": "Zaloguj się",
|
||||
"label.logout": "Wyloguj",
|
||||
"label.more": "Więcej",
|
||||
"label.name": "Nazwa",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"label.administrator": "Администратор",
|
||||
"label.all": "Все",
|
||||
"label.all-websites": "Все сайты",
|
||||
"label.all-events": "Все события",
|
||||
"label.back": "Назад",
|
||||
"label.cancel": "Отменить",
|
||||
"label.change-password": "Изменить пароль",
|
||||
|
|
|
|||
99
lang/sk-SK.json
Normal file
99
lang/sk-SK.json
Normal file
|
|
@ -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.administrator": "நிர்வாகியைச் சேர்க்க",
|
||||
"label.all": "எல்லாம்",
|
||||
"label.all-events": "அனைத்து நிகழ்வுகளும்",
|
||||
"label.all-websites": "அனைத்து வலைத்தளங்களும்",
|
||||
"label.back": "பின்னால்",
|
||||
"label.cancel": "ரத்துசெய்",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"label.administrator": "Адміністратор",
|
||||
"label.all": "Всі",
|
||||
"label.all-websites": "Всі сайти",
|
||||
"label.all-events": "Всі події",
|
||||
"label.back": "Назад",
|
||||
"label.cancel": "Відмінити",
|
||||
"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 MOBILE_SCREEN_WIDTH = 479;
|
||||
|
||||
export const URL_LENGTH = 500;
|
||||
|
||||
export const DESKTOP_OS = [
|
||||
'Windows 3.11',
|
||||
'Windows 95',
|
||||
|
|
@ -140,3 +142,256 @@ export const BROWSERS = {
|
|||
'ios-webview': 'iOS (webview)',
|
||||
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 { v4, v5, validate } from 'uuid';
|
||||
import bcrypt from 'bcrypt';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { JWT, JWE, JWK } from 'jose';
|
||||
import { startOfMonth } from 'date-fns';
|
||||
|
||||
|
|
@ -40,11 +40,11 @@ export function getRandomChars(n) {
|
|||
}
|
||||
|
||||
export async function hashPassword(password) {
|
||||
return bcrypt.hash(password, SALT_ROUNDS);
|
||||
return bcrypt.hashSync(password, SALT_ROUNDS);
|
||||
}
|
||||
|
||||
export async function checkPassword(password, hash) {
|
||||
return bcrypt.compare(password, hash);
|
||||
return bcrypt.compareSync(password, hash);
|
||||
}
|
||||
|
||||
export async function createToken(payload) {
|
||||
|
|
|
|||
|
|
@ -36,8 +36,9 @@ export function getLocalTime(t) {
|
|||
return addMinutes(new Date(t), new Date().getTimezoneOffset());
|
||||
}
|
||||
|
||||
export function getDateRange(value) {
|
||||
export function getDateRange(value, locale = 'en-US') {
|
||||
const now = new Date();
|
||||
const localeOptions = dateLocales[locale];
|
||||
|
||||
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':
|
||||
return {
|
||||
startDate: startOfWeek(now),
|
||||
endDate: endOfWeek(now),
|
||||
startDate: startOfWeek(now, { locale: localeOptions }),
|
||||
endDate: endOfWeek(now, { locale: localeOptions }),
|
||||
unit: 'day',
|
||||
value,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const urlFilter = (data, { raw }) => {
|
|||
return `${pathname}${search}`;
|
||||
}
|
||||
|
||||
return removeTrailingSlash(pathname);
|
||||
return pathname;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
15
lib/lang.js
15
lib/lang.js
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
cs,
|
||||
sk,
|
||||
da,
|
||||
de,
|
||||
el,
|
||||
|
|
@ -7,11 +8,13 @@ import {
|
|||
es,
|
||||
fi,
|
||||
fr,
|
||||
faIR,
|
||||
he,
|
||||
hi,
|
||||
id,
|
||||
it,
|
||||
ja,
|
||||
ms,
|
||||
nb,
|
||||
nl,
|
||||
pl,
|
||||
|
|
@ -49,11 +52,14 @@ import idMessages from 'lang-compiled/id-ID.json';
|
|||
import ukMessages from 'lang-compiled/uk-UA.json';
|
||||
import fiMessages from 'lang-compiled/fi-FI.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 taMessages from 'lang-compiled/ta-IN.json';
|
||||
import hiMessages from 'lang-compiled/hi-IN.json';
|
||||
import heMessages from 'lang-compiled/he-IL.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 = {
|
||||
'en-US': enMessages,
|
||||
|
|
@ -79,11 +85,14 @@ export const messages = {
|
|||
'uk-UA': ukMessages,
|
||||
'fi-FI': fiMessages,
|
||||
'cs-CZ': csMessages,
|
||||
'sk-SK': skMessages,
|
||||
'pl-PL': plMessages,
|
||||
'ta-IN': taMessages,
|
||||
'hi-IN': hiMessages,
|
||||
'he-IL': heMessages,
|
||||
'it-IT': itMessages,
|
||||
'fa-IR': faIRMessages,
|
||||
'ms-MY': msMYMessages,
|
||||
};
|
||||
|
||||
export const dateLocales = {
|
||||
|
|
@ -110,11 +119,14 @@ export const dateLocales = {
|
|||
'uk-UA': uk,
|
||||
'fi-FI': fi,
|
||||
'cs-CZ': cs,
|
||||
'sk-SK': sk,
|
||||
'pl-PL': pl,
|
||||
'ta-In': ta,
|
||||
'hi-IN': hi,
|
||||
'he-IL': he,
|
||||
'it-IT': it,
|
||||
'fa-IR': faIR,
|
||||
'ms-MY': ms,
|
||||
};
|
||||
|
||||
export const menuOptions = [
|
||||
|
|
@ -125,6 +137,7 @@ export const menuOptions = [
|
|||
{ label: 'Deutsch', value: 'de-DE', display: 'de' },
|
||||
{ label: 'English', value: 'en-US', display: 'en' },
|
||||
{ label: 'Español', value: 'es-MX', display: 'es' },
|
||||
{ label: 'فارسی', value: 'fa-IR', display: 'fa' },
|
||||
{ label: 'Føroyskt', value: 'fo-FO', display: 'fo' },
|
||||
{ label: 'Français', value: 'fr-FR', display: 'fr' },
|
||||
{ label: 'Ελληνικά', value: 'el-GR', display: 'el' },
|
||||
|
|
@ -133,6 +146,7 @@ export const menuOptions = [
|
|||
{ label: 'Italiano', value: 'it-IT', display: 'it' },
|
||||
{ label: 'Bahasa Indonesia', value: 'id-ID', display: 'id' },
|
||||
{ label: '日本語', value: 'ja-JP', display: 'ja' },
|
||||
{ label: 'Malay', value: 'ms-MY', display: 'ms' },
|
||||
{ label: 'Монгол', value: 'mn-MN', display: 'mn' },
|
||||
{ label: 'Nederlands', value: 'nl-NL', display: 'nl' },
|
||||
{ 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: 'Русский', value: 'ru-RU', display: 'ru' },
|
||||
{ label: 'Română', value: 'ro-RO', display: 'ro' },
|
||||
{ label: 'Slovenčina', value: 'sk-SK', display: 'sk' },
|
||||
{ label: 'Suomi', value: 'fi-FI', display: 'fi' },
|
||||
{ label: 'Svenska', value: 'sv-SE', display: 'sv' },
|
||||
{ label: 'தமிழ்', value: 'ta-IN', display: 'ta' },
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import moment from 'moment-timezone';
|
||||
import prisma from 'lib/db';
|
||||
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() {
|
||||
const type =
|
||||
|
|
@ -152,11 +158,7 @@ export async function createSession(website_id, data) {
|
|||
return runQuery(
|
||||
prisma.session.create({
|
||||
data: {
|
||||
website: {
|
||||
connect: {
|
||||
website_id,
|
||||
},
|
||||
},
|
||||
website_id,
|
||||
...data,
|
||||
},
|
||||
select: {
|
||||
|
|
@ -180,18 +182,10 @@ export async function savePageView(website_id, session_id, url, referrer) {
|
|||
return runQuery(
|
||||
prisma.pageview.create({
|
||||
data: {
|
||||
website: {
|
||||
connect: {
|
||||
website_id,
|
||||
},
|
||||
},
|
||||
session: {
|
||||
connect: {
|
||||
session_id,
|
||||
},
|
||||
},
|
||||
url,
|
||||
referrer,
|
||||
website_id,
|
||||
session_id,
|
||||
url: url?.substr(0, URL_LENGTH),
|
||||
referrer: referrer?.substr(0, URL_LENGTH),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -201,19 +195,11 @@ export async function saveEvent(website_id, session_id, url, event_type, event_v
|
|||
return runQuery(
|
||||
prisma.event.create({
|
||||
data: {
|
||||
website: {
|
||||
connect: {
|
||||
website_id,
|
||||
},
|
||||
},
|
||||
session: {
|
||||
connect: {
|
||||
session_id,
|
||||
},
|
||||
},
|
||||
url,
|
||||
event_type,
|
||||
event_value,
|
||||
website_id,
|
||||
session_id,
|
||||
url: url?.substr(0, URL_LENGTH),
|
||||
event_type: event_type?.substr(0, 50),
|
||||
event_value: event_value?.substr(0, 50),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {
|
|||
MOBILE_SCREEN_WIDTH,
|
||||
} from './constants';
|
||||
|
||||
let lookup;
|
||||
|
||||
export function getIpAddress(req) {
|
||||
// Cloudflare
|
||||
if (req.headers['cf-connecting-ip']) {
|
||||
|
|
@ -61,7 +63,9 @@ export async function getCountry(req, ip) {
|
|||
}
|
||||
|
||||
// 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);
|
||||
|
||||
|
|
|
|||
59
package.json
59
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "umami",
|
||||
"version": "1.14.0",
|
||||
"version": "1.17.0",
|
||||
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
||||
"author": "Mike Cao <mike@mikecao.com>",
|
||||
"license": "MIT",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma",
|
||||
"copy-db-schema": "node scripts/copy-db-schema.js",
|
||||
"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",
|
||||
"format-lang": "node scripts/format-lang.js",
|
||||
"compile-lang": "formatjs compile-folder --ast build lang-compiled",
|
||||
|
|
@ -56,12 +56,12 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "2.17.0",
|
||||
"@reduxjs/toolkit": "^1.5.0",
|
||||
"bcrypt": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"@prisma/client": "2.21.2",
|
||||
"@reduxjs/toolkit": "^1.5.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chalk": "^4.1.1",
|
||||
"chart.js": "^2.9.4",
|
||||
"classnames": "^2.2.6",
|
||||
"classnames": "^2.3.1",
|
||||
"cookie": "^0.4.1",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^2.16.1",
|
||||
|
|
@ -70,27 +70,28 @@
|
|||
"dotenv": "^8.2.0",
|
||||
"formik": "^2.2.6",
|
||||
"immer": "^8.0.1",
|
||||
"ipaddr.js": "^2.0.0",
|
||||
"is-localhost-ip": "^1.4.0",
|
||||
"isbot-fast": "^1.2.0",
|
||||
"jose": "2.0.3",
|
||||
"isbot": "^3.0.26",
|
||||
"jose": "2.0.5",
|
||||
"maxmind": "^4.3.1",
|
||||
"moment-timezone": "^0.5.32",
|
||||
"next": "^10.0.7",
|
||||
"prompts": "2.4.0",
|
||||
"moment-timezone": "^0.5.33",
|
||||
"next": "^10.1.3",
|
||||
"prompts": "2.4.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-intl": "^5.12.3",
|
||||
"react-redux": "^7.2.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-intl": "^5.16.0",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-simple-maps": "^2.3.0",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-tooltip": "^4.2.14",
|
||||
"react-use-measure": "^2.0.3",
|
||||
"react-tooltip": "^4.2.18",
|
||||
"react-use-measure": "^2.0.4",
|
||||
"react-window": "^1.8.6",
|
||||
"redux": "^4.0.5",
|
||||
"redux": "^4.1.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"request-ip": "^2.1.3",
|
||||
"semver": "^7.3.4",
|
||||
"semver": "^7.3.5",
|
||||
"thenby": "^1.3.4",
|
||||
"timezone-support": "^2.0.2",
|
||||
"tinycolor2": "^1.4.2",
|
||||
|
|
@ -98,18 +99,17 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^2.13.16",
|
||||
"@prisma/cli": "2.17.0",
|
||||
"@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",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"del": "^6.0.0",
|
||||
"dotenv-cli": "^4.0.0",
|
||||
"eslint": "^7.20.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"eslint": "^7.25.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"extract-react-intl-messages": "^4.1.1",
|
||||
"husky": "^4.3.8",
|
||||
|
|
@ -121,13 +121,14 @@
|
|||
"postcss-preset-env": "^6.7.0",
|
||||
"prettier": "^2.2.1",
|
||||
"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-terser": "^7.0.2",
|
||||
"stylelint": "^13.10.0",
|
||||
"stylelint": "^13.13.0",
|
||||
"stylelint-config-css-modules": "^2.2.0",
|
||||
"stylelint-config-prettier": "^8.0.1",
|
||||
"stylelint-config-recommended": "^3.0.0",
|
||||
"stylelint-config-recommended": "^5.0.0",
|
||||
"tar": "^6.0.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { Provider } from 'react-redux';
|
||||
import { useStore } from 'redux/store';
|
||||
|
|
@ -25,15 +26,22 @@ const Intl = ({ children }) => {
|
|||
export default function App({ Component, pageProps }) {
|
||||
useForceSSL(process.env.FORCE_SSL);
|
||||
const store = useStore();
|
||||
const { basePath } = useRouter();
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Head>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" />
|
||||
<link rel="manifest" href="site.webmanifest" />
|
||||
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<link rel="icon" href={`${basePath}/favicon.ico`} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={`${basePath}/apple-touch-icon.png`} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={`${basePath}/favicon-32x32.png`} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={`${basePath}/favicon-16x16.png`} />
|
||||
<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="theme-color" content="#ffffff" />
|
||||
<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 { useCors, useSession } from 'lib/middleware';
|
||||
import { getIpAddress } from 'lib/request';
|
||||
import { ok, badRequest } from 'lib/response';
|
||||
import { createToken } from 'lib/crypto';
|
||||
import { getIpAddress } from '../../lib/request';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useCors(req, res);
|
||||
|
||||
if (isBot(req.headers['user-agent'])) {
|
||||
if (isbot(req.headers['user-agent'])) {
|
||||
return ok(res);
|
||||
}
|
||||
|
||||
if (process.env.IGNORE_IP) {
|
||||
const ips = process.env.IGNORE_IP.split(',').map(n => n.trim());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
99
prisma/mysql/migrations/20210320112658_init/migration.sql
Normal file
99
prisma/mysql/migrations/20210320112658_init/migration.sql
Normal file
|
|
@ -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;
|
||||
3
prisma/mysql/migrations/migration_lock.toml
Normal file
3
prisma/mysql/migrations/migration_lock.toml
Normal file
|
|
@ -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"
|
||||
1
prisma/mysql/schema.mysql.prisma
Symbolic link
1
prisma/mysql/schema.mysql.prisma
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../schema.mysql.prisma
|
||||
1
prisma/mysql/seed.js
Symbolic link
1
prisma/mysql/seed.js
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../seed.js
|
||||
129
prisma/postgresql/migrations/20210320112717_init/migration.sql
Normal file
129
prisma/postgresql/migrations/20210320112717_init/migration.sql
Normal file
|
|
@ -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;
|
||||
3
prisma/postgresql/migrations/migration_lock.toml
Normal file
3
prisma/postgresql/migrations/migration_lock.toml
Normal file
|
|
@ -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"
|
||||
1
prisma/postgresql/schema.postgresql.prisma
Symbolic link
1
prisma/postgresql/schema.postgresql.prisma
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../schema.postgresql.prisma
|
||||
1
prisma/postgresql/seed.js
Symbolic link
1
prisma/postgresql/seed.js
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../seed.js
|
||||
|
|
@ -8,23 +8,23 @@ datasource db {
|
|||
}
|
||||
|
||||
model account {
|
||||
user_id Int @default(autoincrement()) @id
|
||||
username String @unique
|
||||
password String
|
||||
user_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
username String @unique @db.VarChar(255)
|
||||
password String @db.VarChar(60)
|
||||
is_admin Boolean @default(false)
|
||||
created_at DateTime? @default(now())
|
||||
updated_at DateTime? @default(now())
|
||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||
updated_at DateTime? @default(now()) @db.Timestamp(0)
|
||||
website website[]
|
||||
}
|
||||
|
||||
model event {
|
||||
event_id Int @default(autoincrement()) @id
|
||||
website_id Int
|
||||
session_id Int
|
||||
created_at DateTime? @default(now())
|
||||
url String
|
||||
event_type String
|
||||
event_value String
|
||||
event_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
website_id Int @db.UnsignedInt
|
||||
session_id Int @db.UnsignedInt
|
||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||
url String @db.VarChar(500)
|
||||
event_type String @db.VarChar(50)
|
||||
event_value String @db.VarChar(50)
|
||||
session session @relation(fields: [session_id], references: [session_id])
|
||||
website website @relation(fields: [website_id], references: [website_id])
|
||||
|
||||
|
|
@ -34,32 +34,34 @@ model event {
|
|||
}
|
||||
|
||||
model pageview {
|
||||
view_id Int @default(autoincrement()) @id
|
||||
website_id Int
|
||||
session_id Int
|
||||
created_at DateTime? @default(now())
|
||||
url String
|
||||
referrer String?
|
||||
view_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
website_id Int @db.UnsignedInt
|
||||
session_id Int @db.UnsignedInt
|
||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||
url String @db.VarChar(500)
|
||||
referrer String? @db.VarChar(500)
|
||||
session session @relation(fields: [session_id], references: [session_id])
|
||||
website website @relation(fields: [website_id], references: [website_id])
|
||||
|
||||
@@index([created_at], name: "pageview_created_at_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, session_id, created_at], name: "pageview_website_id_session_id_created_at_idx")
|
||||
}
|
||||
|
||||
model session {
|
||||
session_id Int @default(autoincrement()) @id
|
||||
session_uuid String @unique
|
||||
website_id Int
|
||||
created_at DateTime? @default(now())
|
||||
hostname String?
|
||||
browser String?
|
||||
os String?
|
||||
device String?
|
||||
screen String?
|
||||
language String?
|
||||
country String?
|
||||
session_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
session_uuid String @unique @db.VarChar(36)
|
||||
website_id Int @db.UnsignedInt
|
||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||
hostname String? @db.VarChar(100)
|
||||
browser String? @db.VarChar(20)
|
||||
os String? @db.VarChar(20)
|
||||
device String? @db.VarChar(20)
|
||||
screen String? @db.VarChar(11)
|
||||
language String? @db.VarChar(35)
|
||||
country String? @db.Char(2)
|
||||
website website @relation(fields: [website_id], references: [website_id])
|
||||
event event[]
|
||||
pageview pageview[]
|
||||
|
|
@ -69,13 +71,13 @@ model session {
|
|||
}
|
||||
|
||||
model website {
|
||||
website_id Int @default(autoincrement()) @id
|
||||
website_uuid String @unique
|
||||
user_id Int
|
||||
name String
|
||||
domain String?
|
||||
created_at DateTime? @default(now())
|
||||
share_id String? @unique
|
||||
website_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
website_uuid String @unique @db.VarChar(36)
|
||||
user_id Int @db.UnsignedInt
|
||||
name String @db.VarChar(100)
|
||||
domain String? @db.VarChar(500)
|
||||
share_id String? @unique @db.VarChar(64)
|
||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||
account account @relation(fields: [user_id], references: [user_id])
|
||||
event event[]
|
||||
pageview pageview[]
|
||||
|
|
|
|||
|
|
@ -8,23 +8,23 @@ datasource db {
|
|||
}
|
||||
|
||||
model account {
|
||||
user_id Int @default(autoincrement()) @id
|
||||
username String @unique
|
||||
password String
|
||||
user_id Int @id @default(autoincrement())
|
||||
username String @unique @db.VarChar(255)
|
||||
password String @db.VarChar(60)
|
||||
is_admin Boolean @default(false)
|
||||
created_at DateTime? @default(now())
|
||||
updated_at DateTime? @default(now())
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
website website[]
|
||||
}
|
||||
|
||||
model event {
|
||||
event_id Int @default(autoincrement()) @id
|
||||
event_id Int @id @default(autoincrement())
|
||||
website_id Int
|
||||
session_id Int
|
||||
created_at DateTime? @default(now())
|
||||
url String
|
||||
event_type String
|
||||
event_value String
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
url String @db.VarChar(500)
|
||||
event_type String @db.VarChar(50)
|
||||
event_value String @db.VarChar(50)
|
||||
session session @relation(fields: [session_id], references: [session_id])
|
||||
website website @relation(fields: [website_id], references: [website_id])
|
||||
|
||||
|
|
@ -34,32 +34,34 @@ model event {
|
|||
}
|
||||
|
||||
model pageview {
|
||||
view_id Int @default(autoincrement()) @id
|
||||
view_id Int @id @default(autoincrement())
|
||||
website_id Int
|
||||
session_id Int
|
||||
created_at DateTime? @default(now())
|
||||
url String
|
||||
referrer String?
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
url String @db.VarChar(500)
|
||||
referrer String? @db.VarChar(500)
|
||||
session session @relation(fields: [session_id], references: [session_id])
|
||||
website website @relation(fields: [website_id], references: [website_id])
|
||||
|
||||
@@index([created_at], name: "pageview_created_at_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, session_id, created_at], name: "pageview_website_id_session_id_created_at_idx")
|
||||
}
|
||||
|
||||
model session {
|
||||
session_id Int @default(autoincrement()) @id
|
||||
session_uuid String @unique
|
||||
session_id Int @id @default(autoincrement())
|
||||
session_uuid String @unique @db.Uuid
|
||||
website_id Int
|
||||
created_at DateTime? @default(now())
|
||||
hostname String?
|
||||
browser String?
|
||||
os String?
|
||||
screen String?
|
||||
language String?
|
||||
country String?
|
||||
device String?
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
hostname String? @db.VarChar(100)
|
||||
browser String? @db.VarChar(20)
|
||||
os String? @db.VarChar(20)
|
||||
device String? @db.VarChar(20)
|
||||
screen String? @db.VarChar(11)
|
||||
language String? @db.VarChar(35)
|
||||
country String? @db.Char(2)
|
||||
website website @relation(fields: [website_id], references: [website_id])
|
||||
event event[]
|
||||
pageview pageview[]
|
||||
|
|
@ -69,13 +71,13 @@ model session {
|
|||
}
|
||||
|
||||
model website {
|
||||
website_id Int @default(autoincrement()) @id
|
||||
website_uuid String @unique
|
||||
name String
|
||||
created_at DateTime? @default(now())
|
||||
website_id Int @id @default(autoincrement())
|
||||
website_uuid String @unique @db.Uuid
|
||||
user_id Int
|
||||
domain String?
|
||||
share_id String? @unique
|
||||
name String @db.VarChar(100)
|
||||
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])
|
||||
event event[]
|
||||
pageview pageview[]
|
||||
|
|
|
|||
30
prisma/seed.js
Normal file
30
prisma/seed.js
Normal file
|
|
@ -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();
|
||||
});
|
||||
1
public/country/fa-IR.json
Normal file
1
public/country/fa-IR.json
Normal file
File diff suppressed because one or more lines are too long
1
public/country/ms-MY.json
Normal file
1
public/country/ms-MY.json
Normal file
|
|
@ -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"}
|
||||
1
public/country/sk-SK.json
Normal file
1
public/country/sk-SK.json
Normal file
File diff suppressed because one or more lines are too long
180
public/datamaps.world.json
Normal file
180
public/datamaps.world.json
Normal file
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',
|
||||
screen: '1680x1050',
|
||||
url: '/LOADTESTING',
|
||||
referrer: '/REFERRER',
|
||||
},
|
||||
) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ body {
|
|||
}
|
||||
|
||||
.zh-TW {
|
||||
font-family: 'Noto Sans SC', sans-serif !important;
|
||||
font-family: 'Noto Sans TC', sans-serif !important;
|
||||
}
|
||||
|
||||
.ja-JP {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue