diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js
index 00238fbe..fdadaf84 100644
--- a/components/metrics/MetricsBar.js
+++ b/components/metrics/MetricsBar.js
@@ -1,48 +1,55 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
import classNames from 'classnames';
import MetricCard from './MetricCard';
-import { get } from 'lib/web';
+import Loading from 'components/common/Loading';
+import useFetch from 'hooks/useFetch';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import styles from './MetricsBar.module.css';
+import { useDateRange } from '../../hooks/useDateRange';
-export default function MetricsBar({ websiteId, startDate, endDate, className }) {
- const [data, setData] = useState({});
+export default function MetricsBar({ websiteId, className }) {
+ const dateRange = useDateRange(websiteId);
+ const { startDate, endDate, modified } = dateRange;
+ const { data } = useFetch(
+ `/api/website/${websiteId}/metrics`,
+ {
+ start_at: +startDate,
+ end_at: +endDate,
+ },
+ {
+ update: [modified],
+ },
+ );
const [format, setFormat] = useState(true);
- const { pageviews, uniques, bounces, totaltime } = data;
const formatFunc = format ? formatLongNumber : formatNumber;
- async function loadData() {
- setData(
- await get(`/api/website/${websiteId}/metrics`, {
- start_at: +startDate,
- end_at: +endDate,
- }),
- );
- }
-
function handleSetFormat() {
setFormat(state => !state);
}
- useEffect(() => {
- loadData();
- }, [websiteId, startDate, endDate]);
+ const { pageviews, uniques, bounces, totaltime } = data || {};
return (
-
-
- Number(n).toFixed(0) + '%'}
- />
- formatShortTime(n, ['m', 's'], ' ')}
- />
+ {!data ? (
+
+ ) : (
+ <>
+
+
+ Number(n).toFixed(0) + '%'}
+ />
+ formatShortTime(n, ['m', 's'], ' ')}
+ />
+ >
+ )}
);
}
diff --git a/components/metrics/MetricsBar.module.css b/components/metrics/MetricsBar.module.css
index b52fb900..4046634e 100644
--- a/components/metrics/MetricsBar.module.css
+++ b/components/metrics/MetricsBar.module.css
@@ -4,7 +4,7 @@
}
@media only screen and (max-width: 992px) {
- .container > div:last-child {
+ .bar > div:last-child {
display: none;
}
}
diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js
index fa51d1db..aa62d035 100644
--- a/components/metrics/MetricsTable.js
+++ b/components/metrics/MetricsTable.js
@@ -1,22 +1,21 @@
-import React, { useState, useEffect, useMemo } from 'react';
+import React, { useState, useMemo } from 'react';
import { FixedSizeList } from 'react-window';
import { useSpring, animated, config } from 'react-spring';
import classNames from 'classnames';
import Button from 'components/common/Button';
+import Loading from 'components/common/Loading';
+import useFetch from 'hooks/useFetch';
import Arrow from 'assets/arrow-right.svg';
-import { get } from 'lib/web';
import { percentFilter } from 'lib/filters';
import { formatNumber, formatLongNumber } from 'lib/format';
+import { useDateRange } from 'hooks/useDateRange';
import styles from './MetricsTable.module.css';
-import Loading from '../common/Loading';
export default function MetricsTable({
- title,
- metric,
websiteId,
websiteDomain,
- startDate,
- endDate,
+ title,
+ metric,
type,
className,
dataFilter,
@@ -27,7 +26,18 @@ export default function MetricsTable({
onDataLoad = () => {},
onExpand = () => {},
}) {
- const [data, setData] = useState();
+ const dateRange = useDateRange(websiteId);
+ const { startDate, endDate, modified } = dateRange;
+ const { data } = useFetch(
+ `/api/website/${websiteId}/rankings`,
+ {
+ type,
+ start_at: +startDate,
+ end_at: +endDate,
+ domain: websiteDomain,
+ },
+ { onDataLoad, delay: 300, update: [modified] },
+ );
const [format, setFormat] = useState(true);
const formatFunc = format ? formatLongNumber : formatNumber;
const shouldAnimate = limit > 0;
@@ -43,18 +53,6 @@ export default function MetricsTable({
return [];
}, [data, dataFilter, filterOptions]);
- async function loadData() {
- const data = await get(`/api/website/${websiteId}/rankings`, {
- type,
- start_at: +startDate,
- end_at: +endDate,
- domain: websiteDomain,
- });
-
- setData(data);
- onDataLoad(data);
- }
-
const handleSetFormat = () => setFormat(state => !state);
const getRow = row => {
@@ -76,12 +74,6 @@ export default function MetricsTable({
return
{getRow(rankings[index])}
;
};
- useEffect(() => {
- if (websiteId) {
- loadData();
- }
- }, [websiteId, startDate, endDate, type]);
-
return (
{data ? (
diff --git a/components/metrics/OSTable.js b/components/metrics/OSTable.js
index 2b9b4500..60eee696 100644
--- a/components/metrics/OSTable.js
+++ b/components/metrics/OSTable.js
@@ -2,15 +2,13 @@ import React from 'react';
import MetricsTable from './MetricsTable';
import { osFilter } from 'lib/filters';
-export default function OSTable({ websiteId, startDate, endDate, limit, onExpand }) {
+export default function OSTable({ websiteId, limit, onExpand }) {
return (
}
websiteId={websiteId}
- startDate={startDate}
- endDate={endDate}
limit={limit}
dataFilter={urlFilter}
filterOptions={{ domain: websiteDomain, raw: filter === 'Raw' }}
diff --git a/components/metrics/PageviewsChart.js b/components/metrics/PageviewsChart.js
index f8aa0cdc..fc3aea57 100644
--- a/components/metrics/PageviewsChart.js
+++ b/components/metrics/PageviewsChart.js
@@ -2,7 +2,7 @@ import React from 'react';
import CheckVisible from 'components/helpers/CheckVisible';
import BarChart from './BarChart';
-export default function PageviewsChart({ websiteId, data, unit, className }) {
+export default function PageviewsChart({ websiteId, data, unit, records, className }) {
const handleUpdate = chart => {
const {
data: { datasets },
@@ -43,7 +43,7 @@ export default function PageviewsChart({ websiteId, data, unit, className }) {
},
]}
unit={unit}
- records={data.pageviews.length}
+ records={records}
animationDuration={visible ? 300 : 0}
onUpdate={handleUpdate}
/>
diff --git a/components/metrics/QuickButtons.js b/components/metrics/QuickButtons.js
index 74ebebbd..d9a5c109 100644
--- a/components/metrics/QuickButtons.js
+++ b/components/metrics/QuickButtons.js
@@ -12,8 +12,10 @@ const options = {
export default function QuickButtons({ value, onChange }) {
const selectedItem = Object.keys(options).find(key => options[key] === value);
- function handleClick(value) {
- onChange(getDateRange(options[value]));
+ function handleClick(selected) {
+ if (options[selected] !== value) {
+ onChange(getDateRange(options[selected]));
+ }
}
return (
diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js
index e49c9dc0..548b2ba2 100644
--- a/components/metrics/ReferrersTable.js
+++ b/components/metrics/ReferrersTable.js
@@ -1,16 +1,9 @@
import React, { useState } from 'react';
import MetricsTable from './MetricsTable';
import { refFilter } from 'lib/filters';
-import ButtonGroup from '../common/ButtonGroup';
+import ButtonGroup from 'components/common/ButtonGroup';
-export default function Referrers({
- websiteId,
- websiteDomain,
- startDate,
- endDate,
- limit,
- onExpand = () => {},
-}) {
+export default function ReferrersTable({ websiteId, websiteDomain, limit, onExpand = () => {} }) {
const [filter, setFilter] = useState('Combined');
const renderLink = ({ x: url }) => {
@@ -31,8 +24,6 @@ export default function Referrers({
headerComponent={limit ? null :
}
websiteId={websiteId}
websiteDomain={websiteDomain}
- startDate={startDate}
- endDate={endDate}
limit={limit}
dataFilter={refFilter}
filterOptions={{
diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js
index 9c53a560..28394fc1 100644
--- a/components/metrics/WebsiteChart.js
+++ b/components/metrics/WebsiteChart.js
@@ -1,24 +1,39 @@
-import React, { useState, useEffect, useMemo } from 'react';
+import React, { useMemo } from 'react';
+import { useDispatch } from 'react-redux';
import classNames from 'classnames';
import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar';
import QuickButtons from './QuickButtons';
-import DateFilter from '../common/DateFilter';
-import StickyHeader from '../helpers/StickyHeader';
-import { get } from 'lib/web';
-import { getDateArray, getDateRange, getTimezone } from 'lib/date';
+import DateFilter from 'components/common/DateFilter';
+import StickyHeader from 'components/helpers/StickyHeader';
+import useFetch from 'hooks/useFetch';
+import { getDateArray, getDateLength, getTimezone } from 'lib/date';
+import { setDateRange } from 'redux/actions/websites';
import styles from './WebsiteChart.module.css';
+import WebsiteHeader from './WebsiteHeader';
+import { useDateRange } from '../../hooks/useDateRange';
export default function WebsiteChart({
websiteId,
- defaultDateRange = '7day',
+ title,
stickyHeader = false,
+ showLink = false,
onDataLoad = () => {},
- onDateChange = () => {},
}) {
- const [data, setData] = useState();
- const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
- const { startDate, endDate, unit, value } = dateRange;
+ const dispatch = useDispatch();
+ const dateRange = useDateRange(websiteId);
+ const { startDate, endDate, unit, value, modified } = dateRange;
+
+ const { data } = useFetch(
+ `/api/website/${websiteId}/pageviews`,
+ {
+ start_at: +startDate,
+ end_at: +endDate,
+ unit,
+ tz: getTimezone(),
+ },
+ { onDataLoad, update: [modified] },
+ );
const [pageviews, uniques] = useMemo(() => {
if (data) {
@@ -31,40 +46,19 @@ export default function WebsiteChart({
}, [data]);
function handleDateChange(values) {
- setDateRange(values);
- onDateChange(values);
+ dispatch(setDateRange(websiteId, values));
}
- async function loadData() {
- const data = await get(`/api/website/${websiteId}/pageviews`, {
- start_at: +startDate,
- end_at: +endDate,
- unit,
- tz: getTimezone(),
- });
-
- setData(data);
- onDataLoad(data);
- }
-
- useEffect(() => {
- loadData();
- }, [websiteId, startDate, endDate, unit]);
-
return (
<>
+
-
+
diff --git a/components/metrics/WebsiteHeader.js b/components/metrics/WebsiteHeader.js
index 459fd69f..8b01626d 100644
--- a/components/metrics/WebsiteHeader.js
+++ b/components/metrics/WebsiteHeader.js
@@ -1,38 +1,36 @@
import React from 'react';
import { useRouter } from 'next/router';
import PageHeader from 'components/layout/PageHeader';
-import Link from 'components/common/Link';
import Button from 'components/common/Button';
import ActiveUsers from './ActiveUsers';
import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteHeader.module.css';
+import RefreshButton from '../common/RefreshButton';
+import ButtonLayout from '../layout/ButtonLayout';
-export default function WebsiteHeader({ websiteId, name, showLink = false }) {
+export default function WebsiteHeader({ websiteId, title, showLink = false }) {
const router = useRouter();
return (
- {showLink ? (
-
- {name}
-
- ) : (
- {name}
- )}
+ {title}
- {showLink && (
- }
- onClick={() =>
- router.push('/website/[...id]', `/website/${websiteId}/${name}`, {
- shallow: true,
- })
- }
- size="small"
- >
- View details
-
- )}
+
+
+ {showLink && (
+ }
+ onClick={() =>
+ router.push('/website/[...id]', `/website/${websiteId}/${name}`, {
+ shallow: true,
+ })
+ }
+ size="small"
+ >
+ View details
+
+ )}
+
);
}
diff --git a/components/settings/AccountSettings.js b/components/settings/AccountSettings.js
index e0b70698..cdf55988 100644
--- a/components/settings/AccountSettings.js
+++ b/components/settings/AccountSettings.js
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
import classNames from 'classnames';
import PageHeader from 'components/layout/PageHeader';
import Button from 'components/common/Button';
@@ -7,20 +7,22 @@ import Table from 'components/common/Table';
import Modal from 'components/common/Modal';
import AccountEditForm from 'components/forms/AccountEditForm';
import ButtonLayout from 'components/layout/ButtonLayout';
+import DeleteForm from 'components/forms/DeleteForm';
+import useFetch from 'hooks/useFetch';
import Pen from 'assets/pen.svg';
import Plus from 'assets/plus.svg';
import Trash from 'assets/trash.svg';
import Check from 'assets/check.svg';
-import { get } from 'lib/web';
import styles from './AccountSettings.module.css';
-import DeleteForm from '../forms/DeleteForm';
+import Toast from '../common/Toast';
export default function AccountSettings() {
- const [data, setData] = useState();
const [addAccount, setAddAccount] = useState();
const [editAccount, setEditAccount] = useState();
const [deleteAccount, setDeleteAccount] = useState();
const [saved, setSaved] = useState(0);
+ const [message, setMessage] = useState();
+ const { data } = useFetch(`/api/accounts`, {}, { update: [saved] });
const Checkmark = ({ is_admin }) => (is_admin ? } size="medium" /> : null);
@@ -52,6 +54,7 @@ export default function AccountSettings() {
function handleSave() {
setSaved(state => state + 1);
+ setMessage('Saved successfully.');
handleClose();
}
@@ -61,14 +64,6 @@ export default function AccountSettings() {
setDeleteAccount(null);
}
- async function loadData() {
- setData(await get(`/api/accounts`));
- }
-
- useEffect(() => {
- loadData();
- }, [saved]);
-
if (!data) {
return null;
}
@@ -105,6 +100,7 @@ export default function AccountSettings() {
/>
)}
+ {message && setMessage(null)} />}
>
);
}
diff --git a/components/settings/ProfileSettings.js b/components/settings/ProfileSettings.js
index 1aac00af..3c873d73 100644
--- a/components/settings/ProfileSettings.js
+++ b/components/settings/ProfileSettings.js
@@ -5,12 +5,19 @@ import Button from 'components/common/Button';
import ChangePasswordForm from '../forms/ChangePasswordForm';
import Modal from 'components/common/Modal';
import Dots from 'assets/ellipsis-h.svg';
+import Toast from '../common/Toast';
export default function ProfileSettings() {
const user = useSelector(state => state.user);
const [changePassword, setChangePassword] = useState(false);
+ const [message, setMessage] = useState();
const { user_id } = user;
+ function handleSave() {
+ setChangePassword(false);
+ setMessage('Saved successfully.');
+ }
+
return (
<>
@@ -27,11 +34,12 @@ export default function ProfileSettings() {
setChangePassword(false)}
+ onSave={handleSave}
onClose={() => setChangePassword(false)}
/>
)}
+ {message && setMessage(null)} />}
>
);
}
diff --git a/components/settings/WebsiteSettings.js b/components/settings/WebsiteSettings.js
index f759c664..0b6d9a88 100644
--- a/components/settings/WebsiteSettings.js
+++ b/components/settings/WebsiteSettings.js
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
import classNames from 'classnames';
import Table from 'components/common/Table';
import Button from 'components/common/Button';
@@ -15,17 +15,19 @@ import Trash from 'assets/trash.svg';
import Plus from 'assets/plus.svg';
import Code from 'assets/code.svg';
import Link from 'assets/link.svg';
-import { get } from 'lib/web';
import styles from './WebsiteSettings.module.css';
+import useFetch from '../../hooks/useFetch';
+import Toast from '../common/Toast';
export default function WebsiteSettings() {
- const [data, setData] = useState();
const [editWebsite, setEditWebsite] = useState();
const [deleteWebsite, setDeleteWebsite] = useState();
const [addWebsite, setAddWebsite] = useState();
const [showCode, setShowCode] = useState();
const [showUrl, setShowUrl] = useState();
const [saved, setSaved] = useState(0);
+ const [message, setMessage] = useState();
+ const { data } = useFetch(`/api/websites`, {}, { update: [saved] });
const Buttons = row => (
@@ -66,6 +68,7 @@ export default function WebsiteSettings() {
function handleSave() {
setSaved(state => state + 1);
+ setMessage('Saved successfully.');
handleClose();
}
@@ -77,14 +80,6 @@ export default function WebsiteSettings() {
setShowUrl(null);
}
- async function loadData() {
- setData(await get(`/api/websites`));
- }
-
- useEffect(() => {
- loadData();
- }, [saved]);
-
if (!data) {
return null;
}
@@ -135,6 +130,7 @@ export default function WebsiteSettings() {
)}
+ {message && setMessage(null)} />}
>
);
}
diff --git a/hooks/useDateRange.js b/hooks/useDateRange.js
new file mode 100644
index 00000000..99e49dfd
--- /dev/null
+++ b/hooks/useDateRange.js
@@ -0,0 +1,8 @@
+import { useSelector } from 'react-redux';
+import { getDateRange } from 'lib/date';
+
+export function useDateRange(websiteId, defaultDateRange = '7day') {
+ return useSelector(
+ state => state.websites[websiteId]?.dateRange || getDateRange(defaultDateRange),
+ );
+}
diff --git a/hooks/useFetch.js b/hooks/useFetch.js
new file mode 100644
index 00000000..2e1ad247
--- /dev/null
+++ b/hooks/useFetch.js
@@ -0,0 +1,39 @@
+import { useState, useEffect } from 'react';
+import { get } from 'lib/web';
+
+export default function useFetch(url, params = {}, options = {}) {
+ const [data, setData] = useState();
+ const [error, setError] = useState();
+ const keys = Object.keys(params)
+ .sort()
+ .map(key => params[key]);
+ const { update = [], onDataLoad = () => {} } = options;
+
+ async function loadData() {
+ try {
+ setError(null);
+ const data = await get(url, params);
+ setData(data);
+ onDataLoad(data);
+ } catch (e) {
+ console.error(e);
+ setError(e);
+ }
+ }
+
+ useEffect(() => {
+ if (url) {
+ const { interval, delay = 0 } = options;
+
+ setTimeout(() => loadData(), delay);
+
+ const id = interval ? setInterval(() => loadData(), interval) : null;
+
+ return () => {
+ clearInterval(id);
+ };
+ }
+ }, [url, ...keys, ...update]);
+
+ return { data, error, loadData };
+}
diff --git a/lib/constants.js b/lib/constants.js
index 1c63ae18..47592a6c 100644
--- a/lib/constants.js
+++ b/lib/constants.js
@@ -1,39 +1,30 @@
export const AUTH_COOKIE_NAME = 'umami.auth';
+export const POSTGRESQL = 'postgresql';
+export const MYSQL = 'mysql';
+
+export const MYSQL_DATE_FORMATS = {
+ minute: '%Y-%m-%d %H:%i:00',
+ hour: '%Y-%m-%d %H:00:00',
+ day: '%Y-%m-%d',
+ month: '%Y-%m-01',
+ year: '%Y-01-01',
+};
+
+export const POSTGRESQL_DATE_FORMATS = {
+ minute: 'YYYY-MM-DD HH24:MI:00',
+ hour: 'YYYY-MM-DD HH24:00:00',
+ day: 'YYYY-MM-DD',
+ month: 'YYYY-MM-01',
+ year: 'YYYY-01-01',
+};
+
export const DOMAIN_REGEX = /((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}/;
export const DESKTOP_SCREEN_WIDTH = 1920;
export const LAPTOP_SCREEN_WIDTH = 1024;
export const MOBILE_SCREEN_WIDTH = 479;
-export const OPERATING_SYSTEMS = [
- 'iOS',
- 'Android OS',
- 'BlackBerry OS',
- 'Windows Mobile',
- 'Amazon OS',
- 'Windows 3.11',
- 'Windows 95',
- 'Windows 98',
- 'Windows 2000',
- 'Windows XP',
- 'Windows Server 2003',
- 'Windows Vista',
- 'Windows 7',
- 'Windows 8',
- 'Windows 8.1',
- 'Windows 10',
- 'Windows ME',
- 'Open BSD',
- 'Sun OS',
- 'Linux',
- 'Mac OS',
- 'QNX',
- 'BeOS',
- 'OS/2',
- 'Chrome OS',
-];
-
export const DESKTOP_OS = [
'Windows 3.11',
'Windows 95',
diff --git a/lib/queries.js b/lib/queries.js
index 4fe46ee2..204b545b 100644
--- a/lib/queries.js
+++ b/lib/queries.js
@@ -1,25 +1,7 @@
import moment from 'moment-timezone';
import prisma, { runQuery } from 'lib/db';
import { subMinutes } from 'date-fns';
-
-const POSTGRESQL = 'postgresql';
-const MYSQL = 'mysql';
-
-const MYSQL_DATE_FORMATS = {
- minute: '%Y-%m-%d %H:%i:00',
- hour: '%Y-%m-%d %H:00:00',
- day: '%Y-%m-%d',
- month: '%Y-%m-01',
- year: '%Y-01-01',
-};
-
-const POSTGRESQL_DATE_FORMATS = {
- minute: 'YYYY-MM-DD HH24:MI:00',
- hour: 'YYYY-MM-DD HH24:00:00',
- day: 'YYYY-MM-DD',
- month: 'YYYY-MM-01',
- year: 'YYYY-01-01',
-};
+import { MYSQL, POSTGRESQL, MYSQL_DATE_FORMATS, POSTGRESQL_DATE_FORMATS } from 'lib/constants';
export function getDatabase() {
return (
@@ -317,7 +299,7 @@ export function getMetrics(website_id, start_at, end_at) {
);
}
- return Promise.resolve({});
+ return Promise.reject(new Error('Unknown database.'));
}
export function getPageviews(
@@ -364,7 +346,7 @@ export function getPageviews(
);
}
- return Promise.resolve([]);
+ return Promise.reject(new Error('Unknown database.'));
}
export function getRankings(website_id, start_at, end_at, type, table, domain) {
@@ -406,7 +388,7 @@ export function getRankings(website_id, start_at, end_at, type, table, domain) {
);
}
- return Promise.resolve([]);
+ return Promise.reject(new Error('Unknown database.'));
}
export function getActiveVisitors(website_id) {
@@ -439,7 +421,7 @@ export function getActiveVisitors(website_id) {
);
}
- return Promise.resolve([]);
+ return Promise.reject(new Error('Unknown database.'));
}
export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit = 'day') {
@@ -483,5 +465,5 @@ export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit =
);
}
- return Promise.resolve([]);
+ return Promise.reject(new Error('Unknown database.'));
}
diff --git a/lib/url.js b/lib/url.js
index e29243fb..0eb4a04a 100644
--- a/lib/url.js
+++ b/lib/url.js
@@ -1,11 +1,11 @@
export function removeTrailingSlash(url) {
- return url.length > 1 && url.endsWith('/') ? url.slice(0, -1) : url;
+ return url && url.length > 1 && url.endsWith('/') ? url.slice(0, -1) : url;
}
export function getDomainName(str) {
try {
return new URL(str).hostname;
- } catch {
+ } catch (e) {
return str;
}
}
diff --git a/package.json b/package.json
index 17b79794..a6cca6bb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "umami",
- "version": "0.19.0",
+ "version": "0.20.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao ",
"license": "MIT",
@@ -30,8 +30,7 @@
],
"**/*.css": [
"stylelint --fix",
- "prettier --write",
- "eslint"
+ "prettier --write"
]
},
"husky": {
@@ -53,6 +52,7 @@
"dotenv": "^8.2.0",
"formik": "^2.1.5",
"geolite2-redist": "^1.0.7",
+ "immer": "^7.0.8",
"is-localhost-ip": "^1.4.0",
"jose": "^1.28.0",
"maxmind": "^4.1.4",
diff --git a/pages/api/website/[id]/metrics.js b/pages/api/website/[id]/metrics.js
index 82ab393e..4b0d71e1 100644
--- a/pages/api/website/[id]/metrics.js
+++ b/pages/api/website/[id]/metrics.js
@@ -3,11 +3,14 @@ import { ok } from 'lib/response';
export default async (req, res) => {
const { id, start_at, end_at } = req.query;
+ const websiteId = +id;
+ const startDate = new Date(+start_at);
+ const endDate = new Date(+end_at);
- const metrics = await getMetrics(+id, new Date(+start_at), new Date(+end_at));
+ const metrics = await getMetrics(websiteId, startDate, endDate);
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
- obj[key] = +metrics[0][key];
+ obj[key] = Number(metrics[0][key]) || 0;
return obj;
}, {});
diff --git a/pages/share/[...id].js b/pages/share/[...id].js
index 21005a53..1e0897a9 100644
--- a/pages/share/[...id].js
+++ b/pages/share/[...id].js
@@ -1,46 +1,22 @@
-import React, { useState, useEffect } from 'react';
+import React from 'react';
import { useRouter } from 'next/router';
import Layout from 'components/layout/Layout';
import WebsiteDetails from 'components/WebsiteDetails';
-import NotFound from 'pages/404';
-import { get } from 'lib/web';
+import useFetch from 'hooks/useFetch';
export default function SharePage() {
- const [loading, setLoading] = useState(true);
- const [websiteId, setWebsiteId] = useState();
- const [notFound, setNotFound] = useState(false);
const router = useRouter();
const { id } = router.query;
+ const shareId = id?.[0];
+ const { data } = useFetch(shareId ? `/api/share/${shareId}` : null);
- async function loadData() {
- const website = await get(`/api/share/${id?.[0]}`);
-
- if (website) {
- setWebsiteId(website.website_id);
- } else if (typeof window !== 'undefined') {
- setNotFound(true);
- }
- }
-
- useEffect(() => {
- if (id) {
- loadData().finally(() => {
- setLoading(false);
- });
- } else {
- setLoading(false);
- }
- }, [id]);
-
- if (loading) return null;
-
- if (!id || notFound) {
- return ;
+ if (!data) {
+ return null;
}
return (
-
+
);
}
diff --git a/redux/actions/websites.js b/redux/actions/websites.js
new file mode 100644
index 00000000..d619ddb7
--- /dev/null
+++ b/redux/actions/websites.js
@@ -0,0 +1,33 @@
+import { createSlice } from '@reduxjs/toolkit';
+import produce from 'immer';
+
+const websites = createSlice({
+ name: 'user',
+ initialState: {},
+ reducers: {
+ updateWebsites(state, action) {
+ state = action.payload;
+ return state;
+ },
+ },
+});
+
+export const { updateWebsites } = websites.actions;
+
+export default websites.reducer;
+
+export function setDateRange(websiteId, dateRange) {
+ return (dispatch, getState) => {
+ const state = getState();
+ let { websites = {} } = state;
+
+ websites = produce(websites, draft => {
+ if (!draft[websiteId]) {
+ draft[websiteId] = {};
+ }
+ draft[websiteId].dateRange = { ...dateRange, modified: Date.now() };
+ });
+
+ return dispatch(updateWebsites(websites));
+ };
+}
diff --git a/redux/reducers.js b/redux/reducers.js
index f751726f..68a080f7 100644
--- a/redux/reducers.js
+++ b/redux/reducers.js
@@ -1,4 +1,5 @@
import { combineReducers } from 'redux';
import user from './actions/user';
+import websites from './actions/websites';
-export default combineReducers({ user });
+export default combineReducers({ user, websites });
diff --git a/scripts/copy-db-schema.js b/scripts/copy-db-schema.js
index f28c621e..2d480545 100644
--- a/scripts/copy-db-schema.js
+++ b/scripts/copy-db-schema.js
@@ -5,8 +5,8 @@ const path = require('path');
const databaseType =
process.env.DATABASE_TYPE || (process.env.DATABASE_URL && process.env.DATABASE_URL.split(':')[0]);
-if (!databaseType) {
- throw new Error('Database schema not specified');
+if (!databaseType || !['mysql', 'postgresql'].includes(databaseType)) {
+ throw new Error('Missing or invalid database');
}
console.log(`Database schema detected: ${databaseType}`);
diff --git a/tracker/index.js b/tracker/index.js
index a30a0220..f0ad78cd 100644
--- a/tracker/index.js
+++ b/tracker/index.js
@@ -1,6 +1,7 @@
import 'promise-polyfill/src/polyfill';
import 'unfetch/polyfill';
import { post, hook, doNotTrack } from '../lib/web';
+import { removeTrailingSlash } from '../lib/url';
(window => {
const {
@@ -17,7 +18,10 @@ import { post, hook, doNotTrack } from '../lib/web';
if (!script || (__DNT__ && doNotTrack())) return;
const website = script.getAttribute('data-website-id');
- const hostUrl = new URL(script.src).href.split('/').slice(0, -1).join('/');
+ const hostUrl = script.getAttribute('data-host-url');
+ const root = hostUrl
+ ? removeTrailingSlash(hostUrl)
+ : new URL(script.src).href.split('/').slice(0, -1).join('/');
const screen = `${width}x${height}`;
const listeners = [];
@@ -42,7 +46,7 @@ import { post, hook, doNotTrack } from '../lib/web';
});
}
- return post(`${hostUrl}/api/collect`, {
+ return post(`${root}/api/collect`, {
type,
payload,
});
diff --git a/yarn.lock b/yarn.lock
index 588dd177..73f0d4d6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4456,7 +4456,7 @@ ignore@^5.1.4, ignore@^5.1.8:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
-immer@^7.0.3:
+immer@^7.0.3, immer@^7.0.8:
version "7.0.8"
resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.8.tgz#41dcbc5669a76500d017bef3ad0d03ce0a1d7c1e"
integrity sha512-XnpIN8PXBBaOD43U8Z17qg6RQiKQYGDGGCIbz1ixmLGwBkSWwmrmx5X7d+hTtXDM8ur7m5OdLE0PiO+y5RB3pw==