-
+
{url && }
-
+
}
{title}
-
+
{showLink && (
@@ -24,7 +24,7 @@ export default function WebsiteHeader({ websiteId, token, title, showLink = fals
size="small"
iconRight
>
-
+
)}
diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js
new file mode 100644
index 00000000..1a2a3f54
--- /dev/null
+++ b/components/pages/RealtimeDashboard.js
@@ -0,0 +1,158 @@
+import React, { useState, useEffect, useMemo, useCallback } from 'react';
+import { FormattedMessage } from 'react-intl';
+import { subMinutes, startOfMinute } from 'date-fns';
+import firstBy from 'thenby';
+import Page from 'components/layout/Page';
+import GridLayout, { GridRow, GridColumn } from 'components/layout/GridLayout';
+import RealtimeChart from 'components/metrics/RealtimeChart';
+import RealtimeLog from 'components/metrics/RealtimeLog';
+import RealtimeHeader from 'components/metrics/RealtimeHeader';
+import WorldMap from 'components/common/WorldMap';
+import DataTable from 'components/metrics/DataTable';
+import RealtimeViews from 'components/metrics/RealtimeViews';
+import useFetch from 'hooks/useFetch';
+import useLocale from 'hooks/useLocale';
+import useCountryNames from 'hooks/useCountryNames';
+import { percentFilter } from 'lib/filters';
+import { TOKEN_HEADER, REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
+import styles from './RealtimeDashboard.module.css';
+
+function mergeData(state, data, time) {
+ const ids = state.map(({ __id }) => __id);
+ return state
+ .concat(data.filter(({ __id }) => !ids.includes(__id)))
+ .filter(({ created_at }) => new Date(created_at).getTime() >= time);
+}
+
+function filterWebsite(data, id) {
+ return data.filter(({ website_id }) => website_id === id);
+}
+
+export default function RealtimeDashboard() {
+ const [locale] = useLocale();
+ const countryNames = useCountryNames(locale);
+ const [data, setData] = useState();
+ const [websiteId, setWebsiteId] = useState(0);
+ const { data: init, loading } = useFetch('/api/realtime/init');
+ const { data: updates } = useFetch('/api/realtime/update', {
+ params: { start_at: data?.timestamp },
+ disabled: !init?.websites?.length || !data,
+ interval: REALTIME_INTERVAL,
+ headers: { [TOKEN_HEADER]: init?.token },
+ });
+
+ const renderCountryName = useCallback(
+ ({ x }) => {countryNames[x]},
+ [countryNames],
+ );
+
+ const realtimeData = useMemo(() => {
+ if (data) {
+ const { pageviews, sessions, events } = data;
+
+ if (websiteId) {
+ return {
+ pageviews: filterWebsite(pageviews, websiteId),
+ sessions: filterWebsite(sessions, websiteId),
+ events: filterWebsite(events, websiteId),
+ };
+ }
+ }
+
+ return data;
+ }, [data, websiteId]);
+
+ const countries = useMemo(() => {
+ if (realtimeData?.sessions) {
+ return percentFilter(
+ realtimeData.sessions
+ .reduce((arr, { country }) => {
+ if (country) {
+ const row = arr.find(({ x }) => x === country);
+
+ if (!row) {
+ arr.push({ x: country, y: 1 });
+ } else {
+ row.y += 1;
+ }
+ }
+ return arr;
+ }, [])
+ .sort(firstBy('y', -1)),
+ );
+ }
+ return [];
+ }, [realtimeData?.sessions]);
+
+ useEffect(() => {
+ if (init && !data) {
+ const { websites, data } = init;
+
+ setData({ websites, ...data });
+ }
+ }, [init]);
+
+ useEffect(() => {
+ if (updates) {
+ const { pageviews, sessions, events, timestamp } = updates;
+ const time = subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime();
+
+ setData(state => ({
+ ...state,
+ pageviews: mergeData(state.pageviews, pageviews, time),
+ sessions: mergeData(state.sessions, sessions, time),
+ events: mergeData(state.events, events, time),
+ timestamp,
+ }));
+ }
+ }, [updates]);
+
+ if (!init || !data || loading) {
+ return null;
+ }
+
+ const { websites } = data;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ metric={}
+ data={countries}
+ renderLabel={renderCountryName}
+ height={500}
+ />
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/pages/RealtimeDashboard.module.css b/components/pages/RealtimeDashboard.module.css
new file mode 100644
index 00000000..b9b5a632
--- /dev/null
+++ b/components/pages/RealtimeDashboard.module.css
@@ -0,0 +1,7 @@
+.container {
+ display: flex;
+}
+
+.chart {
+ margin-bottom: 30px;
+}
diff --git a/components/pages/TestConsole.js b/components/pages/TestConsole.js
index f6fa8a23..fef6c620 100644
--- a/components/pages/TestConsole.js
+++ b/components/pages/TestConsole.js
@@ -7,7 +7,7 @@ import Page from '../layout/Page';
import PageHeader from '../layout/PageHeader';
import useFetch from '../../hooks/useFetch';
import DropDown from '../common/DropDown';
-import styles from './Test.module.css';
+import styles from './TestConsole.module.css';
import WebsiteChart from '../metrics/WebsiteChart';
import EventsChart from '../metrics/EventsChart';
import Button from '../common/Button';
diff --git a/components/pages/Test.module.css b/components/pages/TestConsole.module.css
similarity index 100%
rename from components/pages/Test.module.css
rename to components/pages/TestConsole.module.css
diff --git a/components/pages/WebsiteDetails.js b/components/pages/WebsiteDetails.js
index 66545928..6c310909 100644
--- a/components/pages/WebsiteDetails.js
+++ b/components/pages/WebsiteDetails.js
@@ -4,6 +4,7 @@ import classNames from 'classnames';
import WebsiteChart from 'components/metrics/WebsiteChart';
import WorldMap from 'components/common/WorldMap';
import Page from 'components/layout/Page';
+import GridLayout, { GridRow, GridColumn } from 'components/layout/GridLayout';
import MenuLayout from 'components/layout/MenuLayout';
import Link from 'components/common/Link';
import Loading from 'components/common/Loading';
@@ -19,6 +20,8 @@ import EventsTable from '../metrics/EventsTable';
import EventsChart from '../metrics/EventsChart';
import useFetch from 'hooks/useFetch';
import usePageQuery from 'hooks/usePageQuery';
+import useShareToken from 'hooks/useShareToken';
+import { DEFAULT_ANIMATION_DURATION, TOKEN_HEADER } from 'lib/constants';
const views = {
url: PagesTable,
@@ -30,8 +33,11 @@ const views = {
event: EventsTable,
};
-export default function WebsiteDetails({ websiteId, token }) {
- const { data } = useFetch(`/api/website/${websiteId}`, { token });
+export default function WebsiteDetails({ websiteId }) {
+ const shareToken = useShareToken();
+ const { data } = useFetch(`/api/website/${websiteId}`, {
+ headers: { [TOKEN_HEADER]: shareToken?.token },
+ });
const [chartLoaded, setChartLoaded] = useState(false);
const [countryData, setCountryData] = useState();
const [eventsData, setEventsData] = useState();
@@ -50,7 +56,7 @@ export default function WebsiteDetails({ websiteId, token }) {
icon={}
size="small"
>
-
+
);
@@ -91,7 +97,6 @@ export default function WebsiteDetails({ websiteId, token }) {
const tableProps = {
websiteId,
- token,
websiteDomain: data?.domain,
limit: 10,
};
@@ -100,7 +105,7 @@ export default function WebsiteDetails({ websiteId, token }) {
function handleDataLoad() {
if (!chartLoaded) {
- setTimeout(() => setChartLoaded(true), 300);
+ setTimeout(() => setChartLoaded(true), DEFAULT_ANIMATION_DURATION);
}
}
@@ -114,7 +119,6 @@ export default function WebsiteDetails({ websiteId, token }) {
{!chartLoaded && }
{chartLoaded && !view && (
- <>
-
-
-
+
+
+
+
-
-
+
+
-
-
+
+
-
-
-
- 0 })}
- >
-
+
+
+ 0 })}>
+
-
-
-
-
-
- >
+
+
+
+
+
+
)}
- {view && (
+ {view && chartLoaded && (
-
+
)}
diff --git a/components/pages/WebsiteDetails.module.css b/components/pages/WebsiteDetails.module.css
index 750a1ac7..cd771c96 100644
--- a/components/pages/WebsiteDetails.module.css
+++ b/components/pages/WebsiteDetails.module.css
@@ -26,37 +26,10 @@
transform: rotate(180deg);
}
-.row {
- border-top: 1px solid var(--gray300);
- min-height: 430px;
-}
-
-.row > [class*='col-'] {
- border-left: 1px solid var(--gray300);
- padding: 20px;
-}
-
-.row > [class*='col-']:first-child {
- border-left: 0;
- padding-left: 0;
-}
-
-.row > [class*='col-']:last-child {
- padding-right: 0;
-}
-
.hidden {
display: none;
}
-@media only screen and (max-width: 992px) {
- .row {
- border: 0;
- }
-
- .row > [class*='col-'] {
- border-top: 1px solid var(--gray300);
- border-left: 0;
- padding: 0;
- }
+.eventschart {
+ padding: 30px 0;
}
diff --git a/components/pages/WebsiteList.js b/components/pages/WebsiteList.js
index 73d20c98..cd96ae9f 100644
--- a/components/pages/WebsiteList.js
+++ b/components/pages/WebsiteList.js
@@ -9,7 +9,7 @@ import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteList.module.css';
export default function WebsiteList({ userId }) {
- const { data } = useFetch('/api/websites', { user_id: userId });
+ const { data } = useFetch('/api/websites', { params: { user_id: userId } });
if (!data) {
return null;
diff --git a/components/settings/AccountSettings.js b/components/settings/AccountSettings.js
index aa206fa9..b2b0011e 100644
--- a/components/settings/AccountSettings.js
+++ b/components/settings/AccountSettings.js
@@ -25,7 +25,7 @@ export default function AccountSettings() {
const [deleteAccount, setDeleteAccount] = useState();
const [saved, setSaved] = useState(0);
const [message, setMessage] = useState();
- const { data } = useFetch(`/api/accounts`, {}, { update: [saved] });
+ const { data } = useFetch(`/api/accounts`, {}, [saved]);
const Checkmark = ({ is_admin }) => (is_admin ? } size="medium" /> : null);
@@ -42,10 +42,10 @@ export default function AccountSettings() {
row.username !== 'admin' ? (
} size="small" onClick={() => setEditAccount(row)}>
-
+
} size="small" onClick={() => setDeleteAccount(row)}>
-
+
) : null;
@@ -98,12 +98,12 @@ export default function AccountSettings() {
} size="small" onClick={() => setAddAccount(true)}>
-
+
{editAccount && (
- }>
+ }>
)}
{addAccount && (
- }>
+ }>
)}
{deleteAccount && (
}
+ title={}
>
>
);
diff --git a/components/settings/ProfileSettings.js b/components/settings/ProfileSettings.js
index 6fe18d19..ecd9252d 100644
--- a/components/settings/ProfileSettings.js
+++ b/components/settings/ProfileSettings.js
@@ -34,7 +34,7 @@ export default function ProfileSettings() {
} size="small" onClick={() => setChangePassword(true)}>
-
+
@@ -57,7 +57,7 @@ export default function ProfileSettings() {
{changePassword && (
}
+ title={
}
>
>
);
diff --git a/components/settings/WebsiteSettings.js b/components/settings/WebsiteSettings.js
index 17fc5952..0ff6246d 100644
--- a/components/settings/WebsiteSettings.js
+++ b/components/settings/WebsiteSettings.js
@@ -29,7 +29,7 @@ export default function WebsiteSettings() {
const [showUrl, setShowUrl] = useState();
const [saved, setSaved] = useState(0);
const [message, setMessage] = useState();
- const { data } = useFetch(`/api/websites`, {}, { update: [saved] });
+ const { data } = useFetch(`/api/websites`, {}, [saved]);
const Buttons = row => (
@@ -52,10 +52,10 @@ export default function WebsiteSettings() {
onClick={() => setShowCode(row)}
/>
} size="small" onClick={() => setEditWebsite(row)}>
-
+
} size="small" onClick={() => setDeleteWebsite(row)}>
-
+
);
@@ -113,7 +113,7 @@ export default function WebsiteSettings() {
}
>
} size="medium" onClick={() => setAddWebsite(true)}>
-
+
);
@@ -125,23 +125,23 @@ export default function WebsiteSettings() {