Merge branch 'mikecao:master' into master
commit
c015880a4e
|
@ -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
|
||||
|
|
|
@ -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: |
|
||||
|
|
|
@ -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,20 +13,43 @@ 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.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 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}>
|
||||
<div className={styles.items}>
|
||||
<div className={active ? classNames(styles.active) : ''}>
|
||||
<Link href="/dashboard">
|
||||
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
|
||||
</Link>
|
||||
|
@ -37,9 +60,10 @@ export default function Header() {
|
|||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||
</Link>
|
||||
</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}>
|
||||
<ThemeButton />
|
||||
<LanguageButton menuAlign="right" />
|
||||
|
@ -47,6 +71,8 @@ export default function Header() {
|
|||
</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 {
|
||||
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) : '';
|
||||
}
|
||||
if (sw <= 550) {
|
||||
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) : '';
|
||||
}
|
||||
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,6 +92,7 @@ export default function WebsiteChart({
|
|||
<div className="row">
|
||||
<div className="col">
|
||||
{error && <ErrorMessage />}
|
||||
{!hideChart && (
|
||||
<PageviewsChart
|
||||
websiteId={websiteId}
|
||||
data={chartData}
|
||||
|
@ -98,6 +100,7 @@ export default function WebsiteChart({
|
|||
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;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
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 && (
|
||||
<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) {
|
||||
|
|
|
@ -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.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": "Изменить пароль",
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
...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,
|
||||
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,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
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[]
|
||||
|
|
|
@ -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',
|
||||
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…
Reference in New Issue