Merge remote-tracking branch 'origin/dev' into dev
1
assets/expand.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 48 48"><path d="M7.5 40.018v-10.5c0-1.379-1.12-2.5-2.5-2.5s-2.5 1.121-2.5 2.5v11a4.5 4.5 0 0 0 4.5 4.5h12a2.5 2.5 0 0 0 0-5zm33 0H29a2.5 2.5 0 0 0 0 5h12a4.5 4.5 0 0 0 4.5-4.5v-11c0-1.379-1.12-2.5-2.5-2.5s-2.5 1.121-2.5 2.5zm-33-33H19a2.5 2.5 0 0 0 0-5H7a4.5 4.5 0 0 0-4.5 4.5v11a2.5 2.5 0 0 0 5 0zm33 0v10.5a2.5 2.5 0 0 0 5 0v-11a4.5 4.5 0 0 0-4.5-4.5H29a2.5 2.5 0 0 0 0 5z"/></svg>
|
After Width: | Height: | Size: 547 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="enable-background:new 0 0 512 512" viewBox="0 0 512 512"><path d="M223.718 124.76c-48.027 11.198-86.688 49.285-98.494 97.031-11.843 47.899 1.711 96.722 36.259 130.601C173.703 364.377 181 383.586 181 403.777V407c0 13.296 5.801 25.26 15 33.505V467c0 24.813 20.187 45 45 45h30c24.813 0 45-20.187 45-45v-26.495c9.199-8.245 15-20.208 15-33.505v-3.282c0-19.884 7.687-39.458 20.563-52.361C376.994 325.87 391 292.005 391 256c0-86.079-79.769-151.638-167.282-131.24zM286 467c0 8.271-6.729 15-15 15h-30c-8.271 0-15-6.729-15-15v-15h60v15zm44.326-136.834C311.689 348.843 301 375.651 301 403.718V407c0 8.271-6.729 15-15 15h-60c-8.271 0-15-6.729-15-15v-3.223c0-28.499-10.393-55.035-28.513-72.804-26.89-26.37-37.409-64.493-28.141-101.981 9.125-36.907 39.029-66.353 76.184-75.015C299.202 137.964 361 189.228 361 256c0 28.004-10.894 54.343-30.674 74.166zM139.327 118.114 96.9 75.688c-5.857-5.858-15.355-5.858-21.213 0-5.858 5.858-5.858 15.355 0 21.213l42.427 42.426c5.857 5.858 15.356 5.858 21.213 0 5.858-5.858 5.858-15.355 0-21.213zM76 241H15c-8.284 0-15 6.716-15 15s6.716 15 15 15h61c8.284 0 15-6.716 15-15s-6.716-15-15-15zM497 241h-61c-8.284 0-15 6.716-15 15s6.716 15 15 15h61c8.284 0 15-6.716 15-15s-6.716-15-15-15zM436.313 75.688c-5.856-5.858-15.354-5.858-21.213 0l-42.427 42.426c-5.858 5.857-5.858 15.355 0 21.213 5.857 5.858 15.355 5.858 21.213 0l42.427-42.426c5.858-5.857 5.858-15.355 0-21.213zM256 0c-8.284 0-15 6.716-15 15v61c0 8.284 6.716 15 15 15s15-6.716 15-15V15c0-8.284-6.716-15-15-15z"/><path d="M256 181c-6.166 0-12.447.739-18.658 2.194-25.865 6.037-47.518 27.328-53.879 52.979-1.994 8.041 2.907 16.175 10.947 18.17 8.042 1.994 16.176-2.909 18.17-10.948 3.661-14.758 16.647-27.5 31.593-30.989 3.982-.933 7.962-1.406 11.827-1.406 8.284 0 15-6.716 15-15s-6.716-15-15-15z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="enable-background:new 0 0 512 512" viewBox="0 0 512 512"><path d="M223.718 124.76c-48.027 11.198-86.688 49.285-98.494 97.031-11.843 47.899 1.711 96.722 36.259 130.601C173.703 364.377 181 383.586 181 403.777V407c0 13.296 5.801 25.26 15 33.505V467c0 24.813 20.187 45 45 45h30c24.813 0 45-20.187 45-45v-26.495c9.199-8.245 15-20.208 15-33.505v-3.282c0-19.884 7.687-39.458 20.563-52.361C376.994 325.87 391 292.005 391 256c0-86.079-79.769-151.638-167.282-131.24zM286 467c0 8.271-6.729 15-15 15h-30c-8.271 0-15-6.729-15-15v-15h60v15zm44.326-136.834C311.689 348.843 301 375.651 301 403.718V407c0 8.271-6.729 15-15 15h-60c-8.271 0-15-6.729-15-15v-3.223c0-28.499-10.393-55.035-28.513-72.804-26.89-26.37-37.409-64.493-28.141-101.981 9.125-36.907 39.029-66.353 76.184-75.015C299.202 137.964 361 189.228 361 256c0 28.004-10.894 54.343-30.674 74.166zM139.327 118.114 96.9 75.688c-5.857-5.858-15.355-5.858-21.213 0-5.858 5.858-5.858 15.355 0 21.213l42.427 42.426c5.857 5.858 15.356 5.858 21.213 0 5.858-5.858 5.858-15.355 0-21.213zM76 241H15c-8.284 0-15 6.716-15 15s6.716 15 15 15h61c8.284 0 15-6.716 15-15s-6.716-15-15-15zm421 0h-61c-8.284 0-15 6.716-15 15s6.716 15 15 15h61c8.284 0 15-6.716 15-15s-6.716-15-15-15zM436.313 75.688c-5.856-5.858-15.354-5.858-21.213 0l-42.427 42.426c-5.858 5.857-5.858 15.355 0 21.213 5.857 5.858 15.355 5.858 21.213 0l42.427-42.426c5.858-5.857 5.858-15.355 0-21.213zM256 0c-8.284 0-15 6.716-15 15v61c0 8.284 6.716 15 15 15s15-6.716 15-15V15c0-8.284-6.716-15-15-15z"/><path d="M256 181c-6.166 0-12.447.739-18.658 2.194-25.865 6.037-47.518 27.328-53.879 52.979-1.994 8.041 2.907 16.175 10.947 18.17 8.042 1.994 16.176-2.909 18.17-10.948 3.661-14.758 16.647-27.5 31.593-30.989 3.982-.933 7.962-1.406 11.827-1.406 8.284 0 15-6.716 15-15s-6.716-15-15-15z"/></svg>
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
1
assets/overview.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M452 36H60C26.916 36 0 62.916 0 96v240c0 33.084 26.916 60 60 60h176v40H132v40h248v-40H276v-40h176c33.084 0 60-26.916 60-60V96c0-33.084-26.916-60-60-60zm20 300c0 11.028-8.972 20-20 20H60c-11.028 0-20-8.972-20-20V96c0-11.028 8.972-20 20-20h392c11.028 0 20 8.972 20 20v240z"/></svg>
|
After Width: | Height: | Size: 371 B |
1
assets/reports.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M61.17 18.91A32 32 0 1 0 46.4 60.54l.15-.06.16-.1a31.93 31.93 0 0 0 14.47-41.44s-.01-.02-.01-.03zm-4.53-.16L34 28.91V4.1a28 28 0 0 1 22.64 14.65zM4 32A28 28 0 0 1 30 4.1V32a1.74 1.74 0 0 0 0 .39.17.17 0 0 0 0 .07 1.49 1.49 0 0 0 .15.4l12.76 24.9A28 28 0 0 1 4 32zm42.47 23.94L34.74 33l23.54-10.6a28 28 0 0 1-11.81 33.54z"/></svg>
|
After Width: | Height: | Size: 398 B |
@ -15,7 +15,6 @@ export function HamburgerButton() {
|
||||
label: formatMessage(labels.dashboard),
|
||||
url: '/dashboard',
|
||||
},
|
||||
{ label: formatMessage(labels.realtime), url: '/realtime' },
|
||||
!cloudMode && {
|
||||
label: formatMessage(labels.settings),
|
||||
url: '/settings',
|
||||
|
@ -10,7 +10,10 @@ import Globe from 'assets/globe.svg';
|
||||
import Lock from 'assets/lock.svg';
|
||||
import Logo from 'assets/logo.svg';
|
||||
import Moon from 'assets/moon.svg';
|
||||
import Nodes from 'assets/nodes.svg';
|
||||
import Overview from 'assets/overview.svg';
|
||||
import Profile from 'assets/profile.svg';
|
||||
import Reports from 'assets/reports.svg';
|
||||
import Sun from 'assets/sun.svg';
|
||||
import User from 'assets/user.svg';
|
||||
import Users from 'assets/users.svg';
|
||||
@ -29,7 +32,10 @@ const icons = {
|
||||
Lock,
|
||||
Logo,
|
||||
Moon,
|
||||
Nodes,
|
||||
Overview,
|
||||
Profile,
|
||||
Reports,
|
||||
Sun,
|
||||
User,
|
||||
Users,
|
||||
|
@ -1,11 +1,12 @@
|
||||
import useApi from 'hooks/useApi';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import DateFilter from './DateFilter';
|
||||
import styles from './WebsiteDateFilter.module.css';
|
||||
|
||||
export default function WebsiteDateFilter({ websiteId, value }) {
|
||||
export default function WebsiteDateFilter({ websiteId }) {
|
||||
const { get } = useApi();
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
const { value, startDate, endDate } = dateRange;
|
||||
|
||||
const handleChange = async value => {
|
||||
if (value === 'all' && websiteId) {
|
||||
@ -20,6 +21,12 @@ export default function WebsiteDateFilter({ websiteId, value }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={handleChange} />
|
||||
<DateFilter
|
||||
className={styles.dropdown}
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
3
components/input/WebsiteDateFilter.module.css
Normal file
@ -0,0 +1,3 @@
|
||||
.dropdown {
|
||||
min-width: 200px;
|
||||
}
|
@ -29,6 +29,7 @@
|
||||
|
||||
.row > .col {
|
||||
border-top: 1px solid var(--base300);
|
||||
border-inline-start: 0;
|
||||
border-inline-end: 0;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
@ -18,8 +18,6 @@ export function NavBar() {
|
||||
|
||||
const links = [
|
||||
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
|
||||
{ label: formatMessage(labels.realtime), url: '/realtime' },
|
||||
{ label: formatMessage(labels.reports), url: '/reports' },
|
||||
!cloudMode && { label: formatMessage(labels.settings), url: '/settings' },
|
||||
].filter(n => n);
|
||||
|
||||
|
@ -158,6 +158,8 @@ export const labels = defineMessages({
|
||||
max: { id: 'labels.max', defaultMessage: 'Max' },
|
||||
unique: { id: 'labels.unique', defaultMessage: 'Unique' },
|
||||
value: { id: 'labels.value', defaultMessage: 'Value' },
|
||||
overview: { id: 'labels.overview', defaultMessage: 'Overview' },
|
||||
totalRecords: { id: 'labels.total-records', defaultMessage: 'Total records' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
@ -2,12 +2,23 @@ import FilterLink from 'components/common/FilterLink';
|
||||
import MetricsTable from 'components/metrics/MetricsTable';
|
||||
import { BROWSERS } from 'lib/constants';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export function BrowsersTable({ websiteId, ...props }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { basePath } = useRouter();
|
||||
|
||||
function renderLink({ x: browser }) {
|
||||
return <FilterLink id="browser" value={browser} label={BROWSERS[browser] || browser} />;
|
||||
return (
|
||||
<FilterLink id="browser" value={browser} label={BROWSERS[browser] || browser}>
|
||||
<img
|
||||
src={`${basePath}/images/browsers/${browser || 'unknown'}.png`}
|
||||
alt={browser}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</FilterLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,13 +1,15 @@
|
||||
import MetricsTable from './MetricsTable';
|
||||
import { useRouter } from 'next/router';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import MetricsTable from './MetricsTable';
|
||||
|
||||
export function CountriesTable({ websiteId, ...props }) {
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { basePath } = useRouter();
|
||||
|
||||
function renderLink({ x: code }) {
|
||||
return (
|
||||
@ -17,7 +19,7 @@ export function CountriesTable({ websiteId, ...props }) {
|
||||
value={countryNames[code] && code}
|
||||
label={countryNames[code]}
|
||||
>
|
||||
<img src={`/images/flags/${code?.toLowerCase() || 'xx'}.png`} alt={code} />
|
||||
<img src={`${basePath}/images/flags/${code?.toLowerCase() || 'xx'}.png`} alt={code} />
|
||||
</FilterLink>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import MetricsTable from './MetricsTable';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export function DevicesTable({ websiteId, ...props }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { basePath } = useRouter();
|
||||
|
||||
function renderLink({ x: device }) {
|
||||
return (
|
||||
@ -11,7 +13,14 @@ export function DevicesTable({ websiteId, ...props }) {
|
||||
id="device"
|
||||
value={labels[device] && device}
|
||||
label={formatMessage(labels[device] || labels.unknown)}
|
||||
/>
|
||||
>
|
||||
<img
|
||||
src={`${basePath}/images/device/${device.toLowerCase() || 'unknown'}.png`}
|
||||
alt={device}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</FilterLink>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,114 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { Loading } from 'react-basics';
|
||||
import ErrorMessage from 'components/common/ErrorMessage';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
||||
import MetricCard from './MetricCard';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import styles from './MetricsBar.module.css';
|
||||
|
||||
export function MetricsBar({ websiteId }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { get, useQuery } = useApi();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, modified } = dateRange;
|
||||
const [format, setFormat] = useState(true);
|
||||
const {
|
||||
query: { url, referrer, title, os, browser, device, country, region, city },
|
||||
} = usePageQuery();
|
||||
|
||||
const { data, error, isLoading, isFetched } = useQuery(
|
||||
[
|
||||
'websites:stats',
|
||||
{ websiteId, modified, url, referrer, title, os, browser, device, country, region, city },
|
||||
],
|
||||
() =>
|
||||
get(`/websites/${websiteId}/stats`, {
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
url,
|
||||
referrer,
|
||||
title,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
}),
|
||||
);
|
||||
|
||||
const formatFunc = format
|
||||
? n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`)
|
||||
: formatNumber;
|
||||
|
||||
function handleSetFormat() {
|
||||
setFormat(state => !state);
|
||||
}
|
||||
|
||||
const { pageviews, uniques, bounces, totaltime } = data || {};
|
||||
const num = Math.min(data && uniques.value, data && bounces.value);
|
||||
const diffs = data && {
|
||||
pageviews: pageviews.value - pageviews.change,
|
||||
uniques: uniques.value - uniques.change,
|
||||
bounces: bounces.value - bounces.change,
|
||||
totaltime: totaltime.value - totaltime.change,
|
||||
};
|
||||
|
||||
export function MetricsBar({ children, onClick, isLoading, isFetched, error }) {
|
||||
return (
|
||||
<div className={styles.bar} onClick={handleSetFormat}>
|
||||
<div className={styles.bar} onClick={onClick}>
|
||||
{isLoading && !isFetched && <Loading icon="dots" />}
|
||||
{error && <ErrorMessage />}
|
||||
{data && !error && isFetched && (
|
||||
<>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.views)}
|
||||
value={pageviews.value}
|
||||
change={pageviews.change}
|
||||
format={formatFunc}
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.visitors)}
|
||||
value={uniques.value}
|
||||
change={uniques.change}
|
||||
format={formatFunc}
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.bounceRate)}
|
||||
value={uniques.value ? (num / uniques.value) * 100 : 0}
|
||||
change={
|
||||
uniques.value && uniques.change
|
||||
? (num / uniques.value) * 100 -
|
||||
(Math.min(diffs.uniques, diffs.bounces) / diffs.uniques) * 100 || 0
|
||||
: 0
|
||||
}
|
||||
format={n => Number(n).toFixed(0) + '%'}
|
||||
reverseColors
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.averageVisitTime)}
|
||||
value={
|
||||
totaltime.value && pageviews.value
|
||||
? totaltime.value / (pageviews.value - bounces.value)
|
||||
: 0
|
||||
}
|
||||
change={
|
||||
totaltime.value && pageviews.value
|
||||
? (diffs.totaltime / (diffs.pageviews - diffs.bounces) -
|
||||
totaltime.value / (pageviews.value - bounces.value)) *
|
||||
-1 || 0
|
||||
: 0
|
||||
}
|
||||
format={n => `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
.bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
min-height: 110px;
|
||||
gap: 20px;
|
||||
|
@ -1,12 +1,25 @@
|
||||
import MetricsTable from './MetricsTable';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export function OSTable({ websiteId, ...props }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { basePath } = useRouter();
|
||||
|
||||
function renderLink({ x: os }) {
|
||||
return <FilterLink id="os" value={os} />;
|
||||
return (
|
||||
<FilterLink id="os" value={os}>
|
||||
<img
|
||||
src={`${basePath}/images/os/${
|
||||
os.toLowerCase().replaceAll(/[^\w]+/g, '-') || 'unknown'
|
||||
}.png`}
|
||||
alt={os}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</FilterLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,15 +1,17 @@
|
||||
import MetricsTable from './MetricsTable';
|
||||
import { emptyFilter } from 'lib/filters';
|
||||
import { useRouter } from 'next/router';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import { emptyFilter } from 'lib/filters';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import regions from 'public/iso-3166-2.json';
|
||||
|
||||
export function RegionsTable({ websiteId, ...props }) {
|
||||
const { locale } = useLocale();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const { basePath } = useRouter();
|
||||
|
||||
const renderLabel = x => {
|
||||
return regions[x] ? `${regions[x]}, ${countryNames[x.split('-')[0]]}` : x;
|
||||
@ -18,7 +20,10 @@ export function RegionsTable({ websiteId, ...props }) {
|
||||
const renderLink = ({ x: code }) => {
|
||||
return (
|
||||
<FilterLink id="region" className={locale} value={code} label={renderLabel(code)}>
|
||||
<img src={`/images/flags/${code?.split('-')?.[0]?.toLowerCase() || 'xx'}.png`} alt={code} />
|
||||
<img
|
||||
src={`${basePath}/images/flags/${code?.split('-')?.[0]?.toLowerCase() || 'xx'}.png`}
|
||||
alt={code}
|
||||
/>
|
||||
</FilterLink>
|
||||
);
|
||||
};
|
||||
|
@ -1,132 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Icon, Text, Row, Column } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import classNames from 'classnames';
|
||||
import PageviewsChart from './PageviewsChart';
|
||||
import MetricsBar from './MetricsBar';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
|
||||
import ErrorMessage from 'components/common/ErrorMessage';
|
||||
import FilterTags from 'components/metrics/FilterTags';
|
||||
import RefreshButton from 'components/input/RefreshButton';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import useTimezone from 'hooks/useTimezone';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import { getDateArray, getDateLength } from 'lib/date';
|
||||
import Icons from 'components/icons';
|
||||
import useSticky from 'hooks/useSticky';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import styles from './WebsiteChart.module.css';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export function WebsiteChart({
|
||||
websiteId,
|
||||
name,
|
||||
domain,
|
||||
stickyHeader = false,
|
||||
showChart = true,
|
||||
showDetailsButton = false,
|
||||
onDataLoad = () => {},
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit, value, modified } = dateRange;
|
||||
const [timezone] = useTimezone();
|
||||
const {
|
||||
query: { url, referrer, os, browser, device, country, region, city, title },
|
||||
} = usePageQuery();
|
||||
const { get, useQuery } = useApi();
|
||||
const { ref, isSticky } = useSticky({ enabled: stickyHeader });
|
||||
|
||||
const { data, isLoading, error } = useQuery(
|
||||
[
|
||||
'websites:pageviews',
|
||||
{ websiteId, modified, url, referrer, os, browser, device, country, region, city, title },
|
||||
],
|
||||
() =>
|
||||
get(`/websites/${websiteId}/pageviews`, {
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
unit,
|
||||
timezone,
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
title,
|
||||
}),
|
||||
{ onSuccess: onDataLoad },
|
||||
);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (data) {
|
||||
return {
|
||||
pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
|
||||
sessions: getDateArray(data.sessions, startDate, endDate, unit),
|
||||
};
|
||||
}
|
||||
return { pageviews: [], sessions: [] };
|
||||
}, [data, modified]);
|
||||
|
||||
const { dir } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} name={name} domain={domain}>
|
||||
{showDetailsButton && (
|
||||
<Link href={`/websites/${websiteId}`}>
|
||||
<Button variant="primary">
|
||||
<Text>{formatMessage(labels.viewDetails)}</Text>
|
||||
<Icon>
|
||||
<Icon rotate={dir === 'rtl' ? 180 : 0}>
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
</Icon>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</WebsiteHeader>
|
||||
<FilterTags
|
||||
websiteId={websiteId}
|
||||
params={{ url, referrer, os, browser, device, country, region, city, title }}
|
||||
/>
|
||||
<Row
|
||||
ref={ref}
|
||||
className={classNames(styles.header, {
|
||||
[styles.sticky]: stickyHeader,
|
||||
[styles.isSticky]: isSticky,
|
||||
})}
|
||||
>
|
||||
<Column defaultSize={12} xl={8}>
|
||||
<MetricsBar websiteId={websiteId} />
|
||||
</Column>
|
||||
<Column defaultSize={12} xl={4}>
|
||||
<div className={styles.actions}>
|
||||
<RefreshButton websiteId={websiteId} isLoading={isLoading} />
|
||||
<WebsiteDateFilter websiteId={websiteId} value={value} className={styles.dropdown} />
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row>
|
||||
<Column className={styles.chart}>
|
||||
{error && <ErrorMessage />}
|
||||
{showChart && (
|
||||
<PageviewsChart
|
||||
websiteId={websiteId}
|
||||
data={chartData}
|
||||
unit={unit}
|
||||
records={getDateLength(startDate, endDate, unit)}
|
||||
loading={isLoading}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteChart;
|
@ -1,21 +0,0 @@
|
||||
import { Row, Column, Text } from 'react-basics';
|
||||
import Favicon from 'components/common/Favicon';
|
||||
import ActiveUsers from './ActiveUsers';
|
||||
import styles from './WebsiteHeader.module.css';
|
||||
|
||||
export function WebsiteHeader({ websiteId, name, domain, children }) {
|
||||
return (
|
||||
<Row className={styles.header} justifyContent="center">
|
||||
<Column className={styles.title} variant="two">
|
||||
<Favicon domain={domain} />
|
||||
<Text>{name}</Text>
|
||||
</Column>
|
||||
<Column className={styles.info} variant="two">
|
||||
<ActiveUsers websiteId={websiteId} />
|
||||
{children}
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteHeader;
|
@ -2,7 +2,7 @@ import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import EventsChart from 'components/metrics/EventsChart';
|
||||
import WebsiteChart from 'components/metrics/WebsiteChart';
|
||||
import WebsiteChart from 'components/pages/websites/WebsiteChart';
|
||||
import useApi from 'hooks/useApi';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
@ -143,12 +143,7 @@ export function TestConsole() {
|
||||
</Row>
|
||||
<Row>
|
||||
<Column>
|
||||
<WebsiteChart
|
||||
websiteId={website.id}
|
||||
name={website.name}
|
||||
domain={website.domain}
|
||||
showLink
|
||||
/>
|
||||
<WebsiteChart websiteId={website.id} />
|
||||
<EventsChart websiteId={website.id} />
|
||||
</Column>
|
||||
</Row>
|
||||
|
47
components/pages/event-data/EventDataMetricsBar.js
Normal file
@ -0,0 +1,47 @@
|
||||
import { Column, Row } from 'react-basics';
|
||||
import { useApi, useDateRange } from 'hooks';
|
||||
import MetricCard from 'components/metrics/MetricCard';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
|
||||
import MetricsBar from 'components/metrics/MetricsBar';
|
||||
import styles from './EventDataMetricsBar.module.css';
|
||||
|
||||
export function EventDataMetricsBar({ websiteId }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { get, useQuery } = useApi();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, modified } = dateRange;
|
||||
|
||||
const { data, error, isLoading, isFetched } = useQuery(
|
||||
['event-data:fields', { websiteId, startDate, endDate, modified }],
|
||||
() =>
|
||||
get(`/event-data/fields`, {
|
||||
websiteId,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<Row className={styles.row}>
|
||||
<Column defaultSize={12} xl={8}>
|
||||
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
|
||||
{!error && isFetched && (
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.fields)}
|
||||
value={data?.length}
|
||||
/>
|
||||
)}
|
||||
</MetricsBar>
|
||||
</Column>
|
||||
<Column defaultSize={12} xl={4}>
|
||||
<div className={styles.actions}>
|
||||
<WebsiteDateFilter websiteId={websiteId} />
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventDataMetricsBar;
|
46
components/pages/event-data/EventDataMetricsBar.module.css
Normal file
@ -0,0 +1,46 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
min-height: 90px;
|
||||
margin-bottom: 20px;
|
||||
background: var(--base50);
|
||||
z-index: var(--z-index-above);
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
min-height: 110px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card {
|
||||
justify-self: flex-start;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.card {
|
||||
flex-basis: calc(50% - 20px);
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
32
components/pages/event-data/EventDataTable.js
Normal file
@ -0,0 +1,32 @@
|
||||
import Link from 'next/link';
|
||||
import { GridTable, GridColumn } from 'react-basics';
|
||||
import { useMessages, usePageQuery } from 'hooks';
|
||||
import Empty from 'components/common/Empty';
|
||||
|
||||
export function EventDataTable({ data = [], showValue }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { resolveUrl } = usePageQuery();
|
||||
|
||||
if (data.length === 0) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
return (
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="field" label={formatMessage(labels.field)}>
|
||||
{row => {
|
||||
return (
|
||||
<Link href={resolveUrl({ view: row.field })} shallow={true}>
|
||||
{row.field}
|
||||
</Link>
|
||||
);
|
||||
}}
|
||||
</GridColumn>
|
||||
<GridColumn name="total" label={formatMessage(labels.totalRecords)}>
|
||||
{({ total }) => total.toLocaleString()}
|
||||
</GridColumn>
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventDataTable;
|
44
components/pages/event-data/EventDataValueTable.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { GridTable, GridColumn, Button, Icon, Text, Flexbox } from 'react-basics';
|
||||
import { useMessages, usePageQuery } from 'hooks';
|
||||
import Link from 'next/link';
|
||||
import Icons from 'components/icons';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Empty from 'components/common/Empty';
|
||||
|
||||
export function EventDataTable({ data = [], field }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { resolveUrl } = usePageQuery();
|
||||
|
||||
const Title = () => {
|
||||
return (
|
||||
<>
|
||||
<Link href={resolveUrl({ view: undefined })}>
|
||||
<Button>
|
||||
<Icon rotate={180}>
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.back)}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
<Text>{field}</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={<Title />} />
|
||||
{data.length <= 0 && <Empty />}
|
||||
{data.length > 0 && (
|
||||
<GridTable data={data}>
|
||||
<GridColumn name="value" label={formatMessage(labels.value)} />
|
||||
<GridColumn name="total" label={formatMessage(labels.totalRecords)} width="200px">
|
||||
{({ total }) => total.toLocaleString()}
|
||||
</GridColumn>
|
||||
</GridTable>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventDataTable;
|
@ -1,22 +1,20 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { subMinutes, startOfMinute } from 'date-fns';
|
||||
import { useRouter } from 'next/router';
|
||||
import firstBy from 'thenby';
|
||||
import { GridRow, GridColumn } from 'components/layout/Grid';
|
||||
import Page from 'components/layout/Page';
|
||||
import RealtimeChart from 'components/metrics/RealtimeChart';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import WorldMap from 'components/common/WorldMap';
|
||||
import RealtimeLog from 'components/pages/realtime/RealtimeLog';
|
||||
import RealtimeHeader from 'components/pages/realtime/RealtimeHeader';
|
||||
import RealtimeUrls from 'components/pages/realtime/RealtimeUrls';
|
||||
import RealtimeCountries from 'components/pages/realtime/RealtimeCountries';
|
||||
import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||
import WebsiteHeader from 'components/pages/websites/WebsiteHeader';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import { percentFilter } from 'lib/filters';
|
||||
import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
|
||||
import styles from './RealtimeDashboard.module.css';
|
||||
import styles from './RealtimePage.module.css';
|
||||
import { useWebsite } from 'hooks';
|
||||
|
||||
function mergeData(state = [], data = [], time) {
|
||||
const ids = state.map(({ __id }) => __id);
|
||||
@ -25,12 +23,10 @@ function mergeData(state = [], data = [], time) {
|
||||
.filter(({ timestamp }) => timestamp >= time);
|
||||
}
|
||||
|
||||
export function RealtimeDashboard({ websiteId }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const router = useRouter();
|
||||
export function RealtimePage({ websiteId }) {
|
||||
const [currentData, setCurrentData] = useState();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data: website } = useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`));
|
||||
const { data: website } = useWebsite(websiteId);
|
||||
const { data, isLoading, error } = useQuery(
|
||||
['realtime', websiteId],
|
||||
() => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }),
|
||||
@ -93,15 +89,9 @@ export function RealtimeDashboard({ websiteId }) {
|
||||
return currentData;
|
||||
}, [currentData]);
|
||||
|
||||
const handleSelect = id => {
|
||||
router.push(`/realtime/${id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
<PageHeader title={formatMessage(labels.realtime)}>
|
||||
<WebsiteSelect websiteId={websiteId} onSelect={handleSelect} />
|
||||
</PageHeader>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<RealtimeHeader websiteId={websiteId} data={currentData} />
|
||||
<div className={styles.chart}>
|
||||
<RealtimeChart data={realtimeData} unit="minute" records={REALTIME_RANGE} />
|
||||
@ -126,4 +116,4 @@ export function RealtimeDashboard({ websiteId }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default RealtimeDashboard;
|
||||
export default RealtimePage;
|
@ -9,24 +9,12 @@ import styles from './ReportTemplates.module.css';
|
||||
import { useMessages } from 'hooks';
|
||||
|
||||
const reports = [
|
||||
{
|
||||
title: 'Event data',
|
||||
description: 'Query your custom event data.',
|
||||
url: '/reports/event-data',
|
||||
icon: <Nodes />,
|
||||
},
|
||||
{
|
||||
title: 'Funnel',
|
||||
description: 'Understand the conversion and drop-off rate of users.',
|
||||
url: '/reports/funnel',
|
||||
icon: <Funnel />,
|
||||
},
|
||||
{
|
||||
title: 'Insights',
|
||||
description: 'Explore your data by applying segments and filters.',
|
||||
url: '/reports/insights',
|
||||
icon: <Lightbulb />,
|
||||
},
|
||||
];
|
||||
|
||||
function ReportItem({ title, description, url, icon }) {
|
||||
|
@ -2,6 +2,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||||
gap: 20px;
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.report {
|
||||
|
@ -5,13 +5,13 @@ import { Button, Icon, Icons, Text } from 'react-basics';
|
||||
import { useMessages, useReports } from 'hooks';
|
||||
import ReportsTable from './ReportsTable';
|
||||
|
||||
export function ReportsList() {
|
||||
export function ReportsPage() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { reports, error, isLoading } = useReports();
|
||||
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
<PageHeader title="Reports">
|
||||
<PageHeader title={formatMessage(labels.reports)}>
|
||||
<Link href="/reports/create">
|
||||
<Button variant="primary">
|
||||
<Icon>
|
||||
@ -26,4 +26,4 @@ export function ReportsList() {
|
||||
);
|
||||
}
|
||||
|
||||
export default ReportsList;
|
||||
export default ReportsPage;
|
@ -41,7 +41,6 @@ export function EventDataParameters() {
|
||||
const parameterGroups = [
|
||||
{ label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
|
||||
{ label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters },
|
||||
{ label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups },
|
||||
];
|
||||
|
||||
const parameterData = {
|
||||
@ -55,11 +54,9 @@ export function EventDataParameters() {
|
||||
};
|
||||
|
||||
const handleAdd = (group, value) => {
|
||||
const data = parameterData[group];
|
||||
const data = parameterData[group].filter(({ name }) => name !== value.name);
|
||||
|
||||
if (!data.find(({ name }) => name === value.name)) {
|
||||
updateReport({ parameters: { [group]: data.concat(value) } });
|
||||
}
|
||||
updateReport({ parameters: { [group]: data.concat(value) } });
|
||||
};
|
||||
|
||||
const handleRemove = (group, index) => {
|
||||
@ -127,11 +124,6 @@ export function EventDataParameters() {
|
||||
<div>{value[1]}</div>
|
||||
</>
|
||||
)}
|
||||
{group === REPORT_PARAMETERS.groups && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
@ -3,12 +3,12 @@ import ReportHeader from '../ReportHeader';
|
||||
import ReportMenu from '../ReportMenu';
|
||||
import ReportBody from '../ReportBody';
|
||||
import EventDataParameters from './EventDataParameters';
|
||||
import Nodes from 'assets/nodes.svg';
|
||||
import EventDataTable from './EventDataTable';
|
||||
import Nodes from 'assets/nodes.svg';
|
||||
|
||||
const defaultParameters = {
|
||||
type: 'event-data',
|
||||
parameters: { fields: [], filters: [], groups: [] },
|
||||
parameters: { fields: [], filters: [] },
|
||||
};
|
||||
|
||||
export default function EventDataReport({ reportId }) {
|
||||
|
44
components/pages/reports/insights/FieldAddForm.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { REPORT_PARAMETERS } from 'lib/constants';
|
||||
import PopupForm from '../PopupForm';
|
||||
import FieldSelectForm from '../FieldSelectForm';
|
||||
import FieldAggregateForm from '../FieldAggregateForm';
|
||||
import FieldFilterForm from '../FieldFilterForm';
|
||||
import styles from './FieldAddForm.module.css';
|
||||
|
||||
export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {
|
||||
const [selected, setSelected] = useState();
|
||||
|
||||
const handleSelect = value => {
|
||||
const { type } = value;
|
||||
|
||||
if (group === REPORT_PARAMETERS.groups || type === 'array' || type === 'boolean') {
|
||||
value.value = group === REPORT_PARAMETERS.groups ? '' : 'total';
|
||||
handleSave(value);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelected(value);
|
||||
};
|
||||
|
||||
const handleSave = value => {
|
||||
onAdd(group, value);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<PopupForm className={styles.popup} element={element} onClose={onClose}>
|
||||
{!selected && <FieldSelectForm fields={fields} onSelect={handleSelect} />}
|
||||
{selected && group === REPORT_PARAMETERS.fields && (
|
||||
<FieldAggregateForm {...selected} onSelect={handleSave} />
|
||||
)}
|
||||
{selected && group === REPORT_PARAMETERS.filters && (
|
||||
<FieldFilterForm {...selected} onSelect={handleSave} />
|
||||
)}
|
||||
</PopupForm>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
export default FieldAddForm;
|
38
components/pages/reports/insights/FieldAddForm.module.css
Normal file
@ -0,0 +1,38 @@
|
||||
.menu {
|
||||
width: 360px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.type {
|
||||
color: var(--font-color300);
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.popup {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
min-width: 60px;
|
||||
}
|
151
components/pages/reports/insights/InsightsParameters.js
Normal file
@ -0,0 +1,151 @@
|
||||
import { useContext, useRef } from 'react';
|
||||
import { useApi, useMessages } from 'hooks';
|
||||
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
|
||||
import { ReportContext } from 'components/pages/reports/Report';
|
||||
import Empty from 'components/common/Empty';
|
||||
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
||||
import Icons from 'components/icons';
|
||||
import FieldAddForm from './FieldAddForm';
|
||||
import BaseParameters from '../BaseParameters';
|
||||
import ParameterList from '../ParameterList';
|
||||
import styles from './InsightsParameters.module.css';
|
||||
|
||||
function useFields(websiteId, startDate, endDate) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, error, isLoading } = useQuery(
|
||||
['fields', websiteId, startDate, endDate],
|
||||
() =>
|
||||
get('/reports/event-data', {
|
||||
websiteId,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
}),
|
||||
{ enabled: !!(websiteId && startDate && endDate) },
|
||||
);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export function InsightsParameters() {
|
||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const ref = useRef(null);
|
||||
const { parameters } = report || {};
|
||||
const { websiteId, dateRange, fields, filters, groups } = parameters || {};
|
||||
const { startDate, endDate } = dateRange || {};
|
||||
const queryEnabled = websiteId && dateRange && fields?.length;
|
||||
const { data, error } = useFields(websiteId, startDate, endDate);
|
||||
const parametersSelected = websiteId && startDate && endDate;
|
||||
const hasData = data?.length !== 0;
|
||||
|
||||
const parameterGroups = [
|
||||
{ label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
|
||||
{ label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters },
|
||||
{ label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups },
|
||||
];
|
||||
|
||||
const parameterData = {
|
||||
fields,
|
||||
filters,
|
||||
groups,
|
||||
};
|
||||
|
||||
const handleSubmit = values => {
|
||||
runReport(values);
|
||||
};
|
||||
|
||||
const handleAdd = (group, value) => {
|
||||
const data = parameterData[group];
|
||||
|
||||
if (!data.find(({ name }) => name === value.name)) {
|
||||
updateReport({ parameters: { [group]: data.concat(value) } });
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (group, index) => {
|
||||
const data = [...parameterData[group]];
|
||||
data.splice(index, 1);
|
||||
updateReport({ parameters: { [group]: data } });
|
||||
};
|
||||
|
||||
const AddButton = ({ group }) => {
|
||||
return (
|
||||
<PopupTrigger>
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Popup position="bottom" alignment="start">
|
||||
{(close, element) => {
|
||||
return (
|
||||
<FieldAddForm
|
||||
fields={data.map(({ eventKey, InsightsType }) => ({
|
||||
name: eventKey,
|
||||
type: DATA_TYPES[InsightsType],
|
||||
}))}
|
||||
group={group}
|
||||
element={element}
|
||||
onAdd={handleAdd}
|
||||
onClose={close}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Popup>
|
||||
</PopupTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form ref={ref} values={parameters} error={error} onSubmit={handleSubmit}>
|
||||
<BaseParameters />
|
||||
{!hasData && <Empty message={formatMessage(messages.noInsights)} />}
|
||||
{parametersSelected &&
|
||||
hasData &&
|
||||
parameterGroups.map(({ label, group }) => {
|
||||
return (
|
||||
<FormRow
|
||||
key={label}
|
||||
label={label}
|
||||
action={<AddButton group={group} onAdd={handleAdd} />}
|
||||
>
|
||||
<ParameterList
|
||||
items={parameterData[group]}
|
||||
onRemove={index => handleRemove(group, index)}
|
||||
>
|
||||
{({ name, value }) => {
|
||||
return (
|
||||
<div className={styles.parameter}>
|
||||
{group === REPORT_PARAMETERS.fields && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value}</div>
|
||||
</>
|
||||
)}
|
||||
{group === REPORT_PARAMETERS.filters && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
<div className={styles.op}>{value[0]}</div>
|
||||
<div>{value[1]}</div>
|
||||
</>
|
||||
)}
|
||||
{group === REPORT_PARAMETERS.groups && (
|
||||
<>
|
||||
<div>{name}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ParameterList>
|
||||
</FormRow>
|
||||
);
|
||||
})}
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" disabled={!queryEnabled} loading={isRunning}>
|
||||
{formatMessage(labels.runQuery)}
|
||||
</SubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default InsightsParameters;
|
@ -0,0 +1,12 @@
|
||||
.parameter {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.op {
|
||||
font-weight: bold;
|
||||
}
|
26
components/pages/reports/insights/InsightsReport.js
Normal file
@ -0,0 +1,26 @@
|
||||
import Report from '../Report';
|
||||
import ReportHeader from '../ReportHeader';
|
||||
import ReportMenu from '../ReportMenu';
|
||||
import ReportBody from '../ReportBody';
|
||||
import InsightsParameters from './InsightsParameters';
|
||||
import InsightsTable from './InsightsTable';
|
||||
import Lightbulb from 'assets/lightbulb.svg';
|
||||
|
||||
const defaultParameters = {
|
||||
type: 'insights',
|
||||
parameters: { fields: [], filters: [], groups: [] },
|
||||
};
|
||||
|
||||
export default function InsightsReport({ reportId }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Lightbulb />} />
|
||||
<ReportMenu>
|
||||
<InsightsParameters />
|
||||
</ReportMenu>
|
||||
<ReportBody>
|
||||
<InsightsTable />
|
||||
</ReportBody>
|
||||
</Report>
|
||||
);
|
||||
}
|
19
components/pages/reports/insights/InsightsTable.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { useContext } from 'react';
|
||||
import { GridTable, GridColumn } from 'react-basics';
|
||||
import { useMessages } from 'hooks';
|
||||
import { ReportContext } from '../Report';
|
||||
|
||||
export function InsightsTable() {
|
||||
const { report } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<GridTable data={report?.data || []}>
|
||||
<GridColumn name="field" label={formatMessage(labels.field)} />
|
||||
<GridColumn name="value" label={formatMessage(labels.value)} />
|
||||
<GridColumn name="total" label={formatMessage(labels.total)} />
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default InsightsTable;
|
59
components/pages/websites/WebsiteChart.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { useMemo } from 'react';
|
||||
import PageviewsChart from 'components/metrics/PageviewsChart';
|
||||
import { useApi, useDateRange, useTimezone, usePageQuery } from 'hooks';
|
||||
import { getDateArray, getDateLength } from 'lib/date';
|
||||
|
||||
export function WebsiteChart({ websiteId }) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit, modified } = dateRange;
|
||||
const [timezone] = useTimezone();
|
||||
const {
|
||||
query: { url, referrer, os, browser, device, country, region, city, title },
|
||||
} = usePageQuery();
|
||||
const { get, useQuery } = useApi();
|
||||
|
||||
const { data, isLoading } = useQuery(
|
||||
[
|
||||
'websites:pageviews',
|
||||
{ websiteId, modified, url, referrer, os, browser, device, country, region, city, title },
|
||||
],
|
||||
() =>
|
||||
get(`/websites/${websiteId}/pageviews`, {
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
unit,
|
||||
timezone,
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
title,
|
||||
}),
|
||||
);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (data) {
|
||||
return {
|
||||
pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
|
||||
sessions: getDateArray(data.sessions, startDate, endDate, unit),
|
||||
};
|
||||
}
|
||||
return { pageviews: [], sessions: [] };
|
||||
}, [data, startDate, endDate, unit, modified]);
|
||||
|
||||
return (
|
||||
<PageviewsChart
|
||||
websiteId={websiteId}
|
||||
data={chartData}
|
||||
unit={unit}
|
||||
records={getDateLength(startDate, endDate, unit)}
|
||||
loading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteChart;
|
17
components/pages/websites/WebsiteChart.module.css
Normal file
@ -0,0 +1,17 @@
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.chart {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-lg);
|
||||
line-height: 60px;
|
||||
font-weight: 600;
|
||||
}
|
@ -1,11 +1,19 @@
|
||||
import { Button, Text, Icon } from 'react-basics';
|
||||
import { useMemo } from 'react';
|
||||
import { firstBy } from 'thenby';
|
||||
import WebsiteChart from 'components/metrics/WebsiteChart';
|
||||
import Link from 'next/link';
|
||||
import WebsiteChart from 'components/pages/websites/WebsiteChart';
|
||||
import useDashboard from 'store/dashboard';
|
||||
import styles from './WebsiteList.module.css';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
||||
import { useMessages, useLocale } from 'hooks';
|
||||
import Icons from 'components/icons';
|
||||
|
||||
export default function WebsiteChartList({ websites, showCharts, limit }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { websiteOrder } = useDashboard();
|
||||
const { dir } = useLocale();
|
||||
|
||||
const ordered = useMemo(
|
||||
() =>
|
||||
@ -17,16 +25,23 @@ export default function WebsiteChartList({ websites, showCharts, limit }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ordered.map(({ id, name, domain }, index) => {
|
||||
{ordered.map(({ id }, index) => {
|
||||
return index < limit ? (
|
||||
<div key={id} className={styles.website}>
|
||||
<WebsiteChart
|
||||
websiteId={id}
|
||||
name={name}
|
||||
domain={domain}
|
||||
showChart={showCharts}
|
||||
showDetailsButton={true}
|
||||
/>
|
||||
<WebsiteHeader websiteId={id} showLinks={false}>
|
||||
<Link href={`/websites/${id}`}>
|
||||
<Button variant="primary">
|
||||
<Text>{formatMessage(labels.viewDetails)}</Text>
|
||||
<Icon>
|
||||
<Icon rotate={dir === 'rtl' ? 180 : 0}>
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
</Icon>
|
||||
</Button>
|
||||
</Link>
|
||||
</WebsiteHeader>
|
||||
<WebsiteMetricsBar websiteId={id} />
|
||||
<WebsiteChart websiteId={id} showChart={showCharts} />
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
|
@ -1,47 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Loading } from 'react-basics';
|
||||
import Page from 'components/layout/Page';
|
||||
import WebsiteChart from 'components/metrics/WebsiteChart';
|
||||
import useApi from 'hooks/useApi';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
import WebsiteTableView from './WebsiteTableView';
|
||||
import WebsiteMenuView from './WebsiteMenuView';
|
||||
|
||||
export default function WebsiteDetails({ websiteId }) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error } = useQuery(['websites', websiteId], () =>
|
||||
get(`/websites/${websiteId}`),
|
||||
);
|
||||
const [chartLoaded, setChartLoaded] = useState(false);
|
||||
|
||||
const {
|
||||
query: { view },
|
||||
} = usePageQuery();
|
||||
|
||||
function handleDataLoad() {
|
||||
if (!chartLoaded) {
|
||||
setTimeout(() => setChartLoaded(true), DEFAULT_ANIMATION_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
<WebsiteChart
|
||||
websiteId={websiteId}
|
||||
name={data?.name}
|
||||
domain={data?.domain}
|
||||
onDataLoad={handleDataLoad}
|
||||
showLink={false}
|
||||
stickyHeader={true}
|
||||
/>
|
||||
{!chartLoaded && <Loading icon="dots" style={{ minHeight: 300 }} />}
|
||||
{chartLoaded && (
|
||||
<>
|
||||
{!view && <WebsiteTableView websiteId={websiteId} />}
|
||||
{view && <WebsiteMenuView websiteId={websiteId} />}
|
||||
</>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
.chart {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.view {
|
||||
border-top: 1px solid var(--base300);
|
||||
}
|
||||
|
||||
.menu {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.content {
|
||||
min-height: 600px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.backButton svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
37
components/pages/websites/WebsiteDetailsPage.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { Loading } from 'react-basics';
|
||||
import Page from 'components/layout/Page';
|
||||
import WebsiteChart from 'components/pages/websites/WebsiteChart';
|
||||
import FilterTags from 'components/metrics/FilterTags';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import WebsiteTableView from './WebsiteTableView';
|
||||
import WebsiteMenuView from './WebsiteMenuView';
|
||||
import { useWebsite } from 'hooks';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
||||
|
||||
export default function WebsiteDetailsPage({ websiteId }) {
|
||||
const { data: website, isLoading, error } = useWebsite(websiteId);
|
||||
|
||||
const {
|
||||
query: { view, url, referrer, os, browser, device, country, region, city, title },
|
||||
} = usePageQuery();
|
||||
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} sticky={true} />
|
||||
<WebsiteChart websiteId={websiteId} />
|
||||
<FilterTags
|
||||
websiteId={websiteId}
|
||||
params={{ url, referrer, os, browser, device, country, region, city, title }}
|
||||
/>
|
||||
{!website && <Loading icon="dots" style={{ minHeight: 300 }} />}
|
||||
{website && (
|
||||
<>
|
||||
{!view && <WebsiteTableView websiteId={websiteId} />}
|
||||
{view && <WebsiteMenuView websiteId={websiteId} />}
|
||||
</>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
39
components/pages/websites/WebsiteEventData.js
Normal file
@ -0,0 +1,39 @@
|
||||
import { Flexbox } from 'react-basics';
|
||||
import EventDataTable from 'components/pages/event-data/EventDataTable';
|
||||
import EventDataValueTable from 'components/pages/event-data/EventDataValueTable';
|
||||
import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetricsBar';
|
||||
import { useDateRange, useApi, usePageQuery } from 'hooks';
|
||||
|
||||
function useFields(websiteId, field) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, error, isLoading } = useQuery(
|
||||
['event-data:fields', { websiteId, startDate, endDate, field }],
|
||||
() =>
|
||||
get('/event-data', {
|
||||
websiteId,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
field,
|
||||
}),
|
||||
{ enabled: !!(websiteId && startDate && endDate) },
|
||||
);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export default function WebsiteEventData({ websiteId }) {
|
||||
const {
|
||||
query: { view },
|
||||
} = usePageQuery();
|
||||
const { data } = useFields(websiteId, view);
|
||||
|
||||
return (
|
||||
<Flexbox direction="column" gap={20}>
|
||||
<EventDataMetricsBar websiteId={websiteId} />
|
||||
{!view && <EventDataTable data={data} />}
|
||||
{view && <EventDataValueTable field={view} data={data} />}
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
4
components/pages/websites/WebsiteEventData.module.css
Normal file
@ -0,0 +1,4 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
12
components/pages/websites/WebsiteEventDataPage.js
Normal file
@ -0,0 +1,12 @@
|
||||
import Page from 'components/layout/Page';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
import WebsiteEventData from './WebsiteEventData';
|
||||
|
||||
export default function WebsiteEventDataPage({ websiteId }) {
|
||||
return (
|
||||
<Page>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<WebsiteEventData websiteId={websiteId} />
|
||||
</Page>
|
||||
);
|
||||
}
|
78
components/pages/websites/WebsiteHeader.js
Normal file
@ -0,0 +1,78 @@
|
||||
import classNames from 'classnames';
|
||||
import { Flexbox, Row, Column, Text, Button, Icon } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import Favicon from 'components/common/Favicon';
|
||||
import ActiveUsers from 'components/metrics/ActiveUsers';
|
||||
import styles from './WebsiteHeader.module.css';
|
||||
import Icons from 'components/icons';
|
||||
import { useMessages, useWebsite } from 'hooks';
|
||||
|
||||
export function WebsiteHeader({ websiteId, showLinks = true, children }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { asPath, pathname } = useRouter();
|
||||
const { data: website } = useWebsite(websiteId);
|
||||
const { name, domain } = website || {};
|
||||
|
||||
const links = [
|
||||
{
|
||||
label: formatMessage(labels.overview),
|
||||
icon: <Icons.Overview />,
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.realtime),
|
||||
icon: <Icons.Clock />,
|
||||
path: '/realtime',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.reports),
|
||||
icon: <Icons.Reports />,
|
||||
path: '/reports',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.eventData),
|
||||
icon: <Icons.Nodes />,
|
||||
path: '/event-data',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Row className={styles.header} justifyContent="center">
|
||||
<Column className={styles.title} variant="two">
|
||||
<Favicon domain={domain} />
|
||||
<Text>{name}</Text>
|
||||
</Column>
|
||||
<Column className={styles.actions} variant="two">
|
||||
<ActiveUsers websiteId={websiteId} />
|
||||
{showLinks && (
|
||||
<Flexbox alignItems="center">
|
||||
{links.map(({ label, icon, path }) => {
|
||||
const query = path.indexOf('?');
|
||||
const selected = path
|
||||
? asPath.endsWith(query >= 0 ? path.substring(0, query) : path)
|
||||
: pathname === '/websites/[id]';
|
||||
|
||||
return (
|
||||
<Link key={label} href={`/websites/${websiteId}${path}`} shallow={true}>
|
||||
<Button
|
||||
variant="quiet"
|
||||
className={classNames({
|
||||
[styles.selected]: selected,
|
||||
})}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
<Text>{label}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Flexbox>
|
||||
)}
|
||||
{children}
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteHeader;
|
@ -15,7 +15,7 @@
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.info {
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@ -23,3 +23,7 @@
|
||||
gap: 30px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: bold;
|
||||
}
|
138
components/pages/websites/WebsiteMetricsBar.js
Normal file
@ -0,0 +1,138 @@
|
||||
import { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Row, Column } from 'react-basics';
|
||||
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
||||
import MetricCard from 'components/metrics/MetricCard';
|
||||
import RefreshButton from 'components/input/RefreshButton';
|
||||
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
|
||||
import MetricsBar from 'components/metrics/MetricsBar';
|
||||
import { useApi, useDateRange, usePageQuery, useMessages, useSticky } from 'hooks';
|
||||
import styles from './WebsiteMetricsBar.module.css';
|
||||
|
||||
export function WebsiteMetricsBar({ websiteId, sticky }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { get, useQuery } = useApi();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, modified } = dateRange;
|
||||
const [format, setFormat] = useState(true);
|
||||
const { ref, isSticky } = useSticky({ enabled: sticky });
|
||||
const {
|
||||
query: { url, referrer, title, os, browser, device, country, region, city },
|
||||
} = usePageQuery();
|
||||
|
||||
const { data, error, isLoading, isFetched } = useQuery(
|
||||
[
|
||||
'websites:stats',
|
||||
{ websiteId, modified, url, referrer, title, os, browser, device, country, region, city },
|
||||
],
|
||||
() =>
|
||||
get(`/websites/${websiteId}/stats`, {
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
url,
|
||||
referrer,
|
||||
title,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
}),
|
||||
);
|
||||
|
||||
const formatFunc = format
|
||||
? n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`)
|
||||
: formatNumber;
|
||||
|
||||
function handleSetFormat() {
|
||||
setFormat(state => !state);
|
||||
}
|
||||
|
||||
const { pageviews, uniques, bounces, totaltime } = data || {};
|
||||
const num = Math.min(data && uniques.value, data && bounces.value);
|
||||
const diffs = data && {
|
||||
pageviews: pageviews.value - pageviews.change,
|
||||
uniques: uniques.value - uniques.change,
|
||||
bounces: bounces.value - bounces.change,
|
||||
totaltime: totaltime.value - totaltime.change,
|
||||
};
|
||||
|
||||
return (
|
||||
<Row
|
||||
ref={ref}
|
||||
className={classNames(styles.container, {
|
||||
[styles.sticky]: sticky,
|
||||
[styles.isSticky]: isSticky,
|
||||
})}
|
||||
>
|
||||
<Column defaultSize={12} xl={8}>
|
||||
<MetricsBar
|
||||
isLoading={isLoading}
|
||||
isFetched={isFetched}
|
||||
error={error}
|
||||
onClick={handleSetFormat}
|
||||
>
|
||||
{!error && isFetched && (
|
||||
<>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.views)}
|
||||
value={pageviews.value}
|
||||
change={pageviews.change}
|
||||
format={formatFunc}
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.visitors)}
|
||||
value={uniques.value}
|
||||
change={uniques.change}
|
||||
format={formatFunc}
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.bounceRate)}
|
||||
value={uniques.value ? (num / uniques.value) * 100 : 0}
|
||||
change={
|
||||
uniques.value && uniques.change
|
||||
? (num / uniques.value) * 100 -
|
||||
(Math.min(diffs.uniques, diffs.bounces) / diffs.uniques) * 100 || 0
|
||||
: 0
|
||||
}
|
||||
format={n => Number(n).toFixed(0) + '%'}
|
||||
reverseColors
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.averageVisitTime)}
|
||||
value={
|
||||
totaltime.value && pageviews.value
|
||||
? totaltime.value / (pageviews.value - bounces.value)
|
||||
: 0
|
||||
}
|
||||
change={
|
||||
totaltime.value && pageviews.value
|
||||
? (diffs.totaltime / (diffs.pageviews - diffs.bounces) -
|
||||
totaltime.value / (pageviews.value - bounces.value)) *
|
||||
-1 || 0
|
||||
: 0
|
||||
}
|
||||
format={n =>
|
||||
`${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</MetricsBar>
|
||||
</Column>
|
||||
<Column defaultSize={12} xl={4}>
|
||||
<div className={styles.actions}>
|
||||
<RefreshButton websiteId={websiteId} />
|
||||
<WebsiteDateFilter websiteId={websiteId} />
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteMetricsBar;
|
@ -1,22 +1,4 @@
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.chart {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-lg);
|
||||
line-height: 60px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@ -35,8 +17,10 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
min-width: 200px;
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.actions {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 992px) {
|
||||
@ -49,9 +33,3 @@
|
||||
border-bottom: 1px solid var(--base300);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.actions {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
30
components/pages/websites/WebsiteReportsPage.js
Normal file
@ -0,0 +1,30 @@
|
||||
import Page from 'components/layout/Page';
|
||||
import Link from 'next/link';
|
||||
import { Button, Icon, Icons, Text, Flexbox } from 'react-basics';
|
||||
import { useMessages, useReports } from 'hooks';
|
||||
import ReportsTable from 'components/pages/reports/ReportsTable';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
|
||||
export function WebsiteReportsPage({ websiteId }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { reports, error, isLoading } = useReports(websiteId);
|
||||
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<Flexbox alignItems="center" justifyContent="end">
|
||||
<Link href="/reports/create">
|
||||
<Button variant="primary">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.createReport)}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
</Flexbox>
|
||||
<ReportsTable websiteId={websiteId} data={reports} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteReportsPage;
|
@ -18,3 +18,4 @@ export * from './useSticky';
|
||||
export * from './useTheme';
|
||||
export * from './useTimezone';
|
||||
export * from './useUser';
|
||||
export * from './useWebsite';
|
||||
|
@ -1,8 +1,8 @@
|
||||
import useApi from './useApi';
|
||||
|
||||
export function useReports() {
|
||||
export function useReports(websiteId) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, error, isLoading } = useQuery(['reports'], () => get(`/reports`));
|
||||
const { data, error, isLoading } = useQuery(['reports'], () => get(`/reports`, { websiteId }));
|
||||
|
||||
return { reports: data, error, isLoading };
|
||||
}
|
||||
|
10
hooks/useWebsite.js
Normal file
@ -0,0 +1,10 @@
|
||||
import useApi from './useApi';
|
||||
|
||||
export function useWebsite(websiteId) {
|
||||
const { get, useQuery } = useApi();
|
||||
return useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`), {
|
||||
enabled: !!websiteId,
|
||||
});
|
||||
}
|
||||
|
||||
export default useWebsite;
|
@ -166,8 +166,9 @@ export const EVENT_COLORS = [
|
||||
'#ffec16',
|
||||
];
|
||||
|
||||
export const DOMAIN_REGEX =
|
||||
/^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63})$/;
|
||||
export const DOMAIN_REGEX =
|
||||
/^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9-]+(-[a-z0-9-]+)*\.)+(xn--)?[a-z0-9-]{2,63})$/;
|
||||
|
||||
|
||||
export const SHARE_ID_REGEX = /^[a-zA-Z0-9]{16}$/;
|
||||
|
||||
@ -182,7 +183,7 @@ export const DESKTOP_OS = [
|
||||
'BeOS',
|
||||
'Chrome OS',
|
||||
'Linux',
|
||||
'macOS',
|
||||
'Mac OS',
|
||||
'Open BSD',
|
||||
'OS/2',
|
||||
'QNX',
|
||||
@ -204,33 +205,34 @@ export const DESKTOP_OS = [
|
||||
export const MOBILE_OS = ['Amazon OS', 'Android OS', 'BlackBerry OS', 'iOS', 'Windows Mobile'];
|
||||
|
||||
export const BROWSERS = {
|
||||
android: 'Android',
|
||||
aol: 'AOL',
|
||||
edge: 'Edge',
|
||||
'edge-ios': 'Edge (iOS)',
|
||||
yandexbrowser: 'Yandex',
|
||||
kakaotalk: 'KaKaoTalk',
|
||||
samsung: 'Samsung',
|
||||
silk: 'Silk',
|
||||
miui: 'MIUI',
|
||||
beaker: 'Beaker',
|
||||
'edge-chromium': 'Edge (Chromium)',
|
||||
bb10: 'BlackBerry 10',
|
||||
chrome: 'Chrome',
|
||||
'chromium-webview': 'Chrome (webview)',
|
||||
phantomjs: 'PhantomJS',
|
||||
crios: 'Chrome (iOS)',
|
||||
curl: 'Curl',
|
||||
edge: 'Edge',
|
||||
'edge-chromium': 'Edge (Chromium)',
|
||||
'edge-ios': 'Edge (iOS)',
|
||||
facebook: 'Facebook',
|
||||
firefox: 'Firefox',
|
||||
fxios: 'Firefox (iOS)',
|
||||
'opera-mini': 'Opera Mini',
|
||||
opera: 'Opera',
|
||||
ie: 'IE',
|
||||
bb10: 'BlackBerry 10',
|
||||
android: 'Android',
|
||||
ios: 'iOS',
|
||||
safari: 'Safari',
|
||||
facebook: 'Facebook',
|
||||
instagram: 'Instagram',
|
||||
ios: 'iOS',
|
||||
'ios-webview': 'iOS (webview)',
|
||||
kakaotalk: 'KaKaoTalk',
|
||||
miui: 'MIUI',
|
||||
opera: 'Opera',
|
||||
'opera-mini': 'Opera Mini',
|
||||
phantomjs: 'PhantomJS',
|
||||
safari: 'Safari',
|
||||
samsung: 'Samsung',
|
||||
silk: 'Silk',
|
||||
searchbot: 'Searchbot',
|
||||
yandexbrowser: 'Yandex',
|
||||
};
|
||||
|
||||
export const MAP_FILE = '/datamaps.world.json';
|
||||
|
@ -95,7 +95,7 @@
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"react": "^18.2.0",
|
||||
"react-basics": "^0.89.0",
|
||||
"react-basics": "^0.91.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.4",
|
||||
@ -105,7 +105,7 @@
|
||||
"react-use-measure": "^2.0.4",
|
||||
"react-window": "^1.8.6",
|
||||
"request-ip": "^3.3.0",
|
||||
"semver": "^7.3.6",
|
||||
"semver": "^7.5.2",
|
||||
"thenby": "^1.3.4",
|
||||
"timezone-support": "^2.0.2",
|
||||
"uuid": "^8.3.2",
|
||||
@ -151,7 +151,7 @@
|
||||
"rollup-plugin-node-externals": "^5.1.2",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"stylelint": "^14.16.1",
|
||||
"stylelint": "^15.10.1",
|
||||
"stylelint-config-css-modules": "^4.1.0",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-recommended": "^9.0.0",
|
||||
|
36
pages/api/event-data/fields.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { useCors, useAuth } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
||||
import { getEventDataFields } from 'queries';
|
||||
|
||||
export interface EventDataFieldsRequestBody {
|
||||
websiteId: string;
|
||||
dateRange: {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async (
|
||||
req: NextApiRequestQueryBody<any, EventDataFieldsRequestBody>,
|
||||
res: NextApiResponse<any>,
|
||||
) => {
|
||||
await useCors(req, res);
|
||||
await useAuth(req, res);
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const { websiteId, startAt, endAt } = req.query;
|
||||
|
||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const data = await getEventDataFields(websiteId, new Date(+startAt), new Date(+endAt));
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
37
pages/api/event-data/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { useCors, useAuth } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
||||
import { getEventData } from 'queries';
|
||||
|
||||
export interface EventDataRequestBody {
|
||||
websiteId: string;
|
||||
dateRange: {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
};
|
||||
field?: string;
|
||||
}
|
||||
|
||||
export default async (
|
||||
req: NextApiRequestQueryBody<any, EventDataRequestBody>,
|
||||
res: NextApiResponse<any>,
|
||||
) => {
|
||||
await useCors(req, res);
|
||||
await useAuth(req, res);
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const { websiteId, startAt, endAt, field } = req.query;
|
||||
|
||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const data = await getEventData(websiteId, new Date(+startAt), new Date(+endAt), field);
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
@ -1,78 +0,0 @@
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { useCors, useAuth } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
||||
import { getEventDataFields } from 'queries/analytics/eventData/getEventDataFields';
|
||||
import { getEventData } from 'queries';
|
||||
|
||||
export interface EventDataRequestBody {
|
||||
websiteId: string;
|
||||
dateRange: {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
};
|
||||
fields: [
|
||||
{
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
},
|
||||
];
|
||||
filters: [
|
||||
{
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
},
|
||||
];
|
||||
groups: [
|
||||
{
|
||||
name: string;
|
||||
type: string;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default async (
|
||||
req: NextApiRequestQueryBody<any, EventDataRequestBody>,
|
||||
res: NextApiResponse<any>,
|
||||
) => {
|
||||
await useCors(req, res);
|
||||
await useAuth(req, res);
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const { websiteId, startAt, endAt } = req.query;
|
||||
|
||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const data = await getEventDataFields(websiteId, new Date(+startAt), new Date(+endAt));
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
const {
|
||||
websiteId,
|
||||
dateRange: { startDate, endDate },
|
||||
...criteria
|
||||
} = req.body;
|
||||
|
||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const data = await getEventData(
|
||||
websiteId,
|
||||
new Date(startDate),
|
||||
new Date(endDate),
|
||||
criteria as any,
|
||||
);
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
@ -2,8 +2,9 @@ import { uuid } from 'lib/crypto';
|
||||
import { useAuth, useCors } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok } from 'next-basics';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { createReport, getReports } from 'queries';
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
|
||||
export interface ReportRequestBody {
|
||||
websiteId: string;
|
||||
@ -23,12 +24,18 @@ export default async (
|
||||
await useCors(req, res);
|
||||
await useAuth(req, res);
|
||||
|
||||
const { websiteId } = req.query;
|
||||
|
||||
const {
|
||||
user: { id: userId },
|
||||
} = req.auth;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const data = await getReports(userId);
|
||||
if (!(websiteId && (await canViewWebsite(req.auth, websiteId)))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const data = await getReports({ websiteId });
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
|
@ -1,60 +0,0 @@
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { useAuth, useCors } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, WebsiteEventDataMetric } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { getEventData } from 'queries';
|
||||
|
||||
export interface WebsiteEventDataRequestQuery {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface WebsiteEventDataRequestBody {
|
||||
startAt: string;
|
||||
endAt: string;
|
||||
eventName?: string;
|
||||
urlPath?: string;
|
||||
timeSeries?: {
|
||||
unit: string;
|
||||
timezone: string;
|
||||
};
|
||||
filters: [
|
||||
{
|
||||
eventKey?: string;
|
||||
eventValue?: string | number | boolean | Date;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default async (
|
||||
req: NextApiRequestQueryBody<WebsiteEventDataRequestQuery, WebsiteEventDataRequestBody>,
|
||||
res: NextApiResponse<WebsiteEventDataMetric[]>,
|
||||
) => {
|
||||
await useCors(req, res);
|
||||
await useAuth(req, res);
|
||||
|
||||
const { id: websiteId } = req.query;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { startAt, endAt, eventName, urlPath, filters } = req.body;
|
||||
|
||||
const startDate = new Date(+startAt);
|
||||
const endDate = new Date(+endAt);
|
||||
|
||||
const events = await getEventData(websiteId, {
|
||||
startDate,
|
||||
endDate,
|
||||
eventName,
|
||||
urlPath,
|
||||
filters,
|
||||
});
|
||||
|
||||
return ok(res, events);
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import AppLayout from 'components/layout/AppLayout';
|
||||
import TestConsole from 'components/pages/console/TestConsole';
|
||||
|
||||
export default function ConsolePage({ disabled }) {
|
||||
export default function ({ disabled }) {
|
||||
if (disabled) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export default function DefaultPage() {
|
||||
export default function () {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import LoginLayout from 'components/pages/login/LoginLayout';
|
||||
import LoginForm from 'components/pages/login/LoginForm';
|
||||
|
||||
export default function LoginPage({ disabled }) {
|
||||
export default function ({ disabled }) {
|
||||
if (disabled) {
|
||||
return null;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import useApi from 'hooks/useApi';
|
||||
import { setUser } from 'store/app';
|
||||
import { removeClientAuthToken } from 'lib/client';
|
||||
|
||||
export default function LogoutPage({ disabled }) {
|
||||
export default function ({ disabled }) {
|
||||
const router = useRouter();
|
||||
const { post } = useApi();
|
||||
|
||||
|
@ -1,26 +0,0 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import AppLayout from 'components/layout/AppLayout';
|
||||
import RealtimeDashboard from 'components/pages/realtime/RealtimeDashboard';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useApi from 'hooks/useApi';
|
||||
|
||||
export default function RealtimeDetailsPage() {
|
||||
const router = useRouter();
|
||||
const { id: websiteId } = router.query;
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data: website } = useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`), {
|
||||
enabled: !!websiteId,
|
||||
});
|
||||
const title = `${formatMessage(labels.realtime)}${website?.name ? ` - ${website.name}` : ''}`;
|
||||
|
||||
if (!websiteId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout title={title}>
|
||||
<RealtimeDashboard key={websiteId} websiteId={websiteId} />
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import AppLayout from 'components/layout/AppLayout';
|
||||
import RealtimeHome from 'components/pages/realtime/RealtimeHome';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export default function RealtimePage() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
return (
|
||||
<AppLayout title={formatMessage(labels.realtime)}>
|
||||
<RealtimeHome />
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
@ -3,7 +3,7 @@ import AppLayout from 'components/layout/AppLayout';
|
||||
import ReportDetails from 'components/pages/reports/ReportDetails';
|
||||
import { useApi, useMessages } from 'hooks';
|
||||
|
||||
export default function ReportsPage() {
|
||||
export default function () {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
@ -2,7 +2,7 @@ import AppLayout from 'components/layout/AppLayout';
|
||||
import ReportTemplates from 'components/pages/reports/ReportTemplates';
|
||||
import { useMessages } from 'hooks';
|
||||
|
||||
export default function ReportsPage() {
|
||||
export default function () {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
|
@ -2,7 +2,7 @@ import AppLayout from 'components/layout/AppLayout';
|
||||
import EventDataReport from 'components/pages/reports/event-data/EventDataReport';
|
||||
import { useMessages } from 'hooks';
|
||||
|
||||
export default function Report() {
|
||||
export default function () {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
|
@ -2,7 +2,7 @@ import AppLayout from 'components/layout/AppLayout';
|
||||
import FunnelReport from 'components/pages/reports/funnel/FunnelReport';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export default function Funnel() {
|
||||
export default function () {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
|
@ -1,13 +0,0 @@
|
||||
import AppLayout from 'components/layout/AppLayout';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import ReportsList from 'components/pages/reports/ReportsList';
|
||||
|
||||
export default function ReportsPage() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<AppLayout title={formatMessage(labels.reports)}>
|
||||
<ReportsList />
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
@ -3,7 +3,7 @@ import SettingsLayout from 'components/layout/SettingsLayout';
|
||||
import ProfileSettings from 'components/pages/settings/profile/ProfileSettings';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export default function ProfilePage() {
|
||||
export default function () {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
return (
|
||||
<AppLayout title={`${formatMessage(labels.settings)} - ${formatMessage(labels.profile)}`}>
|
||||
|
@ -4,7 +4,7 @@ import TeamSettings from 'components/pages/settings/teams/TeamSettings';
|
||||
import { useRouter } from 'next/router';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export default function TeamDetailPage({ disabled }) {
|
||||
export default function ({ disabled }) {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
@ -3,7 +3,7 @@ import SettingsLayout from 'components/layout/SettingsLayout';
|
||||
import TeamsList from 'components/pages/settings/teams/TeamsList';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export default function TeamsPage({ disabled }) {
|
||||
export default function ({ disabled }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
if (disabled) {
|
||||
return null;
|
||||
|
@ -4,7 +4,7 @@ import UserSettings from 'components/pages/settings/users/UserSettings';
|
||||
import { useRouter } from 'next/router';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export default function TeamDetailPage({ disabled }) {
|
||||
export default function ({ disabled }) {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
@ -3,7 +3,7 @@ import SettingsLayout from 'components/layout/SettingsLayout';
|
||||
import UsersList from 'components/pages/settings/users/UsersList';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export default function UsersPage({ disabled }) {
|
||||
export default function ({ disabled }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
if (disabled) {
|
||||
return null;
|
||||
|
@ -4,7 +4,7 @@ import WebsiteSettings from 'components/pages/settings/websites/WebsiteSettings'
|
||||
import SettingsLayout from 'components/layout/SettingsLayout';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export default function WebsiteSettingsPage({ disabled }) {
|
||||
export default function ({ disabled }) {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
@ -3,7 +3,7 @@ import SettingsLayout from 'components/layout/SettingsLayout';
|
||||
import WebsitesList from 'components/pages/settings/websites/WebsitesList';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export default function WebsitesPage({ disabled }) {
|
||||
export default function ({ disabled }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
if (disabled) {
|
||||
return null;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import ShareLayout from 'components/layout/ShareLayout';
|
||||
import WebsiteDetails from 'components/pages/websites/WebsiteDetails';
|
||||
import WebsiteDetailsPage from 'components/pages/websites/WebsiteDetailsPage';
|
||||
import useShareToken from 'hooks/useShareToken';
|
||||
|
||||
export default function SharePage() {
|
||||
export default function () {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
const shareId = id?.[0];
|
||||
@ -15,7 +15,7 @@ export default function SharePage() {
|
||||
|
||||
return (
|
||||
<ShareLayout>
|
||||
<WebsiteDetails websiteId={shareToken.websiteId} />
|
||||
<WebsiteDetailsPage websiteId={shareToken.websiteId} />
|
||||
</ShareLayout>
|
||||
);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { Loading } from 'react-basics';
|
||||
import { useRouter } from 'next/router';
|
||||
import { setClientAuthToken } from 'lib/client';
|
||||
|
||||
export default function SingleSignOnPage() {
|
||||
export default function () {
|
||||
const router = useRouter();
|
||||
const { token, url } = router.query;
|
||||
|
||||
|
20
pages/websites/[id]/event-data.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import AppLayout from 'components/layout/AppLayout';
|
||||
import WebsiteEventDataPage from 'components/pages/websites/WebsiteEventDataPage';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export default function () {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout title={formatMessage(labels.websites)}>
|
||||
<WebsiteEventDataPage websiteId={id} />
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import AppLayout from 'components/layout/AppLayout';
|
||||
import WebsiteDetails from 'components/pages/websites/WebsiteDetails';
|
||||
import WebsiteDetailsPage from 'components/pages/websites/WebsiteDetailsPage';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export default function DetailsPage() {
|
||||
export default function () {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
@ -14,7 +14,7 @@ export default function DetailsPage() {
|
||||
|
||||
return (
|
||||
<AppLayout title={formatMessage(labels.websites)}>
|
||||
<WebsiteDetails websiteId={id} />
|
||||
<WebsiteDetailsPage websiteId={id} />
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
18
pages/websites/[id]/realtime.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import AppLayout from 'components/layout/AppLayout';
|
||||
import RealtimePage from 'components/pages/realtime/RealtimePage';
|
||||
|
||||
export default function () {
|
||||
const router = useRouter();
|
||||
const { id: websiteId } = router.query;
|
||||
|
||||
if (!websiteId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<RealtimePage websiteId={websiteId} />
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
18
pages/websites/[id]/reports.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import AppLayout from 'components/layout/AppLayout';
|
||||
import WebsiteReportsPage from 'components/pages/websites/WebsiteReportsPage';
|
||||
|
||||
export default function () {
|
||||
const router = useRouter();
|
||||
const { id: websiteId } = router.query;
|
||||
|
||||
if (!websiteId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<WebsiteReportsPage websiteId={websiteId} />
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
BIN
public/images/browsers/android-webview.png
Normal file
After Width: | Height: | Size: 806 B |
BIN
public/images/browsers/android.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
public/images/browsers/aol.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
public/images/browsers/beaker.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
public/images/browsers/blackberry.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
public/images/browsers/brave.png
Normal file
After Width: | Height: | Size: 635 B |
BIN
public/images/browsers/chrome.png
Normal file
After Width: | Height: | Size: 819 B |
BIN
public/images/browsers/chromium-webview.png
Normal file
After Width: | Height: | Size: 680 B |
BIN
public/images/browsers/crios.png
Normal file
After Width: | Height: | Size: 819 B |
BIN
public/images/browsers/curl.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
public/images/browsers/edge-chromium.png
Normal file
After Width: | Height: | Size: 811 B |
BIN
public/images/browsers/edge-ios.png
Normal file
After Width: | Height: | Size: 811 B |
BIN
public/images/browsers/edge.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
public/images/browsers/facebook.png
Normal file
After Width: | Height: | Size: 2.0 KiB |