Moved code into src folder. Added build for component library.
This commit is contained in:
51
src/components/pages/websites/WebsiteChart.js
Normal file
51
src/components/pages/websites/WebsiteChart.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useMemo } from 'react';
|
||||
import PageviewsChart from 'components/metrics/PageviewsChart';
|
||||
import { useApi, useDateRange, useTimezone, usePageQuery } from 'components/hooks';
|
||||
import { getDateArray } 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]);
|
||||
|
||||
return <PageviewsChart websiteId={websiteId} data={chartData} unit={unit} loading={isLoading} />;
|
||||
}
|
||||
|
||||
export default WebsiteChart;
|
||||
17
src/components/pages/websites/WebsiteChart.module.css
Normal file
17
src/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;
|
||||
}
|
||||
50
src/components/pages/websites/WebsiteChartList.js
Normal file
50
src/components/pages/websites/WebsiteChartList.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Button, Text, Icon } from 'react-basics';
|
||||
import { useMemo } from 'react';
|
||||
import { firstBy } from 'thenby';
|
||||
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 'components/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(
|
||||
() =>
|
||||
websites
|
||||
.map(website => ({ ...website, order: websiteOrder.indexOf(website.id) || 0 }))
|
||||
.sort(firstBy('order')),
|
||||
[websites, websiteOrder],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ordered.map(({ id }, index) => {
|
||||
return index < limit ? (
|
||||
<div key={id} className={styles.website}>
|
||||
<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} />
|
||||
{showCharts && <WebsiteChart websiteId={id} />}
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
src/components/pages/websites/WebsiteDetailsPage.js
Normal file
40
src/components/pages/websites/WebsiteDetailsPage.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Loading } from 'react-basics';
|
||||
import { useRouter } from 'next/router';
|
||||
import Page from 'components/layout/Page';
|
||||
import WebsiteChart from 'components/pages/websites/WebsiteChart';
|
||||
import FilterTags from 'components/metrics/FilterTags';
|
||||
import usePageQuery from 'components/hooks/usePageQuery';
|
||||
import WebsiteTableView from './WebsiteTableView';
|
||||
import WebsiteMenuView from './WebsiteMenuView';
|
||||
import { useWebsite } from 'components/hooks';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
||||
|
||||
export default function WebsiteDetailsPage({ websiteId }) {
|
||||
const { data: website, isLoading, error } = useWebsite(websiteId);
|
||||
const { pathname } = useRouter();
|
||||
const showLinks = !pathname.includes('/share/');
|
||||
|
||||
const {
|
||||
query: { view, url, referrer, os, browser, device, country, region, city, title },
|
||||
} = usePageQuery();
|
||||
|
||||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
<WebsiteHeader websiteId={websiteId} showLinks={showLinks} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
40
src/components/pages/websites/WebsiteEventData.js
Normal file
40
src/components/pages/websites/WebsiteEventData.js
Normal file
@@ -0,0 +1,40 @@
|
||||
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 'components/hooks';
|
||||
import styles from './WebsiteEventData.module.css';
|
||||
|
||||
function useData(websiteId, event) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, error, isLoading } = useQuery(
|
||||
['event-data:events', { websiteId, startDate, endDate, event }],
|
||||
() =>
|
||||
get('/event-data/events', {
|
||||
websiteId,
|
||||
startAt: +startDate,
|
||||
endAt: +endDate,
|
||||
event,
|
||||
}),
|
||||
{ enabled: !!(websiteId && startDate && endDate) },
|
||||
);
|
||||
|
||||
return { data, error, isLoading };
|
||||
}
|
||||
|
||||
export default function WebsiteEventData({ websiteId }) {
|
||||
const {
|
||||
query: { event },
|
||||
} = usePageQuery();
|
||||
const { data } = useData(websiteId, event);
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.container} direction="column" gap={20}>
|
||||
<EventDataMetricsBar websiteId={websiteId} />
|
||||
{!event && <EventDataTable data={data} />}
|
||||
{event && <EventDataValueTable event={event} data={data} />}
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.container a {
|
||||
color: var(--font-color100);
|
||||
}
|
||||
|
||||
.container a:hover {
|
||||
color: var(--primary400);
|
||||
}
|
||||
12
src/components/pages/websites/WebsiteEventDataPage.js
Normal file
12
src/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>
|
||||
);
|
||||
}
|
||||
75
src/components/pages/websites/WebsiteHeader.js
Normal file
75
src/components/pages/websites/WebsiteHeader.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import classNames from 'classnames';
|
||||
import { 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 Icons from 'components/icons';
|
||||
import { useMessages, useWebsite } from 'components/hooks';
|
||||
import styles from './WebsiteHeader.module.css';
|
||||
|
||||
export function WebsiteHeader({ websiteId, showLinks = true, children }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { 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>
|
||||
<ActiveUsers websiteId={websiteId} />
|
||||
</Column>
|
||||
<Column className={styles.actions} variant="two">
|
||||
{showLinks && (
|
||||
<div className={styles.links}>
|
||||
{links.map(({ label, icon, path }) => {
|
||||
const selected = path ? pathname.endsWith(path) : pathname === '/websites/[id]';
|
||||
|
||||
return (
|
||||
<Link key={label} href={`/websites/${websiteId}${path}`} shallow={true}>
|
||||
<Button
|
||||
variant="quiet"
|
||||
className={classNames({
|
||||
[styles.selected]: selected,
|
||||
})}
|
||||
>
|
||||
<Icon className={styles.icon}>{icon}</Icon>
|
||||
<Text className={styles.label}>{label}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteHeader;
|
||||
55
src/components/pages/websites/WebsiteHeader.module.css
Normal file
55
src/components/pages/websites/WebsiteHeader.module.css
Normal file
@@ -0,0 +1,55 @@
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 30px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.links {
|
||||
justify-content: space-evenly;
|
||||
flex: 1;
|
||||
border-bottom: 1px solid var(--base300);
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon,
|
||||
.icon svg {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
11
src/components/pages/websites/WebsiteList.module.css
Normal file
11
src/components/pages/websites/WebsiteList.module.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.website {
|
||||
padding-bottom: 30px;
|
||||
border-bottom: 1px solid var(--base300);
|
||||
margin-bottom: 30px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.website:last-child {
|
||||
border-bottom: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
139
src/components/pages/websites/WebsiteMenuView.js
Normal file
139
src/components/pages/websites/WebsiteMenuView.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Icon, Button, Flexbox, Text } from 'react-basics';
|
||||
import Link from 'next/link';
|
||||
import { GridRow, GridColumn } from 'components/layout/Grid';
|
||||
import BrowsersTable from 'components/metrics/BrowsersTable';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import RegionsTable from 'components/metrics/RegionsTable';
|
||||
import CitiesTable from 'components/metrics/CitiesTable';
|
||||
import DevicesTable from 'components/metrics/DevicesTable';
|
||||
import LanguagesTable from 'components/metrics/LanguagesTable';
|
||||
import OSTable from 'components/metrics/OSTable';
|
||||
import PagesTable from 'components/metrics/PagesTable';
|
||||
import QueryParametersTable from 'components/metrics/QueryParametersTable';
|
||||
import ReferrersTable from 'components/metrics/ReferrersTable';
|
||||
import ScreenTable from 'components/metrics/ScreenTable';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import Icons from 'components/icons';
|
||||
import SideNav from 'components/layout/SideNav';
|
||||
import usePageQuery from 'components/hooks/usePageQuery';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import styles from './WebsiteMenuView.module.css';
|
||||
import useLocale from 'components/hooks/useLocale';
|
||||
|
||||
const views = {
|
||||
url: PagesTable,
|
||||
title: PagesTable,
|
||||
referrer: ReferrersTable,
|
||||
browser: BrowsersTable,
|
||||
os: OSTable,
|
||||
device: DevicesTable,
|
||||
screen: ScreenTable,
|
||||
country: CountriesTable,
|
||||
region: RegionsTable,
|
||||
city: CitiesTable,
|
||||
language: LanguagesTable,
|
||||
event: EventsTable,
|
||||
query: QueryParametersTable,
|
||||
};
|
||||
|
||||
export default function WebsiteMenuView({ websiteId, websiteDomain }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { dir } = useLocale();
|
||||
const {
|
||||
resolveUrl,
|
||||
query: { view },
|
||||
} = usePageQuery();
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'url',
|
||||
label: formatMessage(labels.pages),
|
||||
url: resolveUrl({ view: 'url' }),
|
||||
},
|
||||
{
|
||||
key: 'referrer',
|
||||
label: formatMessage(labels.referrers),
|
||||
url: resolveUrl({ view: 'referrer' }),
|
||||
},
|
||||
{
|
||||
key: 'browser',
|
||||
label: formatMessage(labels.browsers),
|
||||
url: resolveUrl({ view: 'browser' }),
|
||||
},
|
||||
{
|
||||
key: 'os',
|
||||
label: formatMessage(labels.os),
|
||||
url: resolveUrl({ view: 'os' }),
|
||||
},
|
||||
{
|
||||
key: 'device',
|
||||
label: formatMessage(labels.devices),
|
||||
url: resolveUrl({ view: 'device' }),
|
||||
},
|
||||
{
|
||||
key: 'country',
|
||||
label: formatMessage(labels.countries),
|
||||
url: resolveUrl({ view: 'country' }),
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
label: formatMessage(labels.regions),
|
||||
url: resolveUrl({ view: 'region' }),
|
||||
},
|
||||
{
|
||||
key: 'city',
|
||||
label: formatMessage(labels.cities),
|
||||
url: resolveUrl({ view: 'city' }),
|
||||
},
|
||||
{
|
||||
key: 'language',
|
||||
label: formatMessage(labels.languages),
|
||||
url: resolveUrl({ view: 'language' }),
|
||||
},
|
||||
{
|
||||
key: 'screen',
|
||||
label: formatMessage(labels.screens),
|
||||
url: resolveUrl({ view: 'screen' }),
|
||||
},
|
||||
{
|
||||
key: 'event',
|
||||
label: formatMessage(labels.events),
|
||||
url: resolveUrl({ view: 'event' }),
|
||||
},
|
||||
{
|
||||
key: 'query',
|
||||
label: formatMessage(labels.queryParameters),
|
||||
url: resolveUrl({ view: 'query' }),
|
||||
},
|
||||
];
|
||||
|
||||
const DetailsComponent = views[view] || (() => null);
|
||||
|
||||
return (
|
||||
<GridRow>
|
||||
<GridColumn xs={12} sm={12} md={12} defaultSize={3} className={styles.menu}>
|
||||
<Link href={resolveUrl({ view: undefined })}>
|
||||
<Flexbox justifyContent="center">
|
||||
<Button variant="quiet">
|
||||
<Icon rotate={dir === 'rtl' ? 0 : 180}>
|
||||
<Icons.ArrowRight />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.back)}</Text>
|
||||
</Button>
|
||||
</Flexbox>
|
||||
</Link>
|
||||
<SideNav items={items} selectedKey={view} shallow={true} />
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} sm={12} md={12} defaultSize={9} className={styles.content}>
|
||||
<DetailsComponent
|
||||
websiteId={websiteId}
|
||||
websiteDomain={websiteDomain}
|
||||
limit={false}
|
||||
animate={false}
|
||||
showFilters={true}
|
||||
virtualize={true}
|
||||
/>
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
);
|
||||
}
|
||||
7
src/components/pages/websites/WebsiteMenuView.module.css
Normal file
7
src/components/pages/websites/WebsiteMenuView.module.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
min-height: 800px;
|
||||
}
|
||||
121
src/components/pages/websites/WebsiteMetricsBar.js
Normal file
121
src/components/pages/websites/WebsiteMetricsBar.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import classNames from 'classnames';
|
||||
import { Row, Column } from 'react-basics';
|
||||
import { formatShortTime } 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 'components/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 { 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 { 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}>
|
||||
{!error && isFetched && (
|
||||
<>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.views)}
|
||||
value={pageviews.value}
|
||||
change={pageviews.change}
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.visitors)}
|
||||
value={uniques.value}
|
||||
change={uniques.change}
|
||||
/>
|
||||
<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;
|
||||
35
src/components/pages/websites/WebsiteMetricsBar.module.css
Normal file
35
src/components/pages/websites/WebsiteMetricsBar.module.css
Normal file
@@ -0,0 +1,35 @@
|
||||
.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);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.actions {
|
||||
margin-top: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 992px) {
|
||||
.sticky {
|
||||
position: sticky;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.isSticky {
|
||||
border-bottom: 1px solid var(--base300);
|
||||
}
|
||||
}
|
||||
56
src/components/pages/websites/WebsiteReportsPage.js
Normal file
56
src/components/pages/websites/WebsiteReportsPage.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import Page from 'components/layout/Page';
|
||||
import Empty from 'components/common/Empty';
|
||||
import ReportsTable from 'components/pages/reports/ReportsTable';
|
||||
import { useMessages, useWebsiteReports } from 'components/hooks';
|
||||
import Link from 'next/link';
|
||||
import { Button, Flexbox, Icon, Icons, Text } from 'react-basics';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
|
||||
export function WebsiteReportsPage({ websiteId }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const {
|
||||
reports,
|
||||
error,
|
||||
isLoading,
|
||||
deleteReport,
|
||||
filter,
|
||||
handleFilterChange,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
} = useWebsiteReports(websiteId);
|
||||
|
||||
const hasData = (reports && reports.data.length !== 0) || filter;
|
||||
|
||||
const handleDelete = async id => {
|
||||
await deleteReport(id);
|
||||
};
|
||||
|
||||
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>
|
||||
{hasData && (
|
||||
<ReportsTable
|
||||
data={reports}
|
||||
onDelete={handleDelete}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
)}
|
||||
{!hasData && <Empty />}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteReportsPage;
|
||||
59
src/components/pages/websites/WebsiteTableView.js
Normal file
59
src/components/pages/websites/WebsiteTableView.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useState } from 'react';
|
||||
import { GridRow, GridColumn } from 'components/layout/Grid';
|
||||
import PagesTable from 'components/metrics/PagesTable';
|
||||
import ReferrersTable from 'components/metrics/ReferrersTable';
|
||||
import BrowsersTable from 'components/metrics/BrowsersTable';
|
||||
import OSTable from 'components/metrics/OSTable';
|
||||
import DevicesTable from 'components/metrics/DevicesTable';
|
||||
import WorldMap from 'components/common/WorldMap';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import EventsChart from 'components/metrics/EventsChart';
|
||||
|
||||
export default function WebsiteTableView({ websiteId }) {
|
||||
const [countryData, setCountryData] = useState();
|
||||
const tableProps = {
|
||||
websiteId,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<GridRow>
|
||||
<GridColumn variant="two">
|
||||
<PagesTable {...tableProps} />
|
||||
</GridColumn>
|
||||
<GridColumn variant="two">
|
||||
<ReferrersTable {...tableProps} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn variant="three">
|
||||
<BrowsersTable {...tableProps} />
|
||||
</GridColumn>
|
||||
<GridColumn variant="three">
|
||||
<OSTable {...tableProps} />
|
||||
</GridColumn>
|
||||
<GridColumn variant="three">
|
||||
<DevicesTable {...tableProps} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} sm={12} md={12} defaultSize={8}>
|
||||
<WorldMap data={countryData} />
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} sm={12} md={12} defaultSize={4}>
|
||||
<CountriesTable {...tableProps} onDataLoad={setCountryData} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} sm={12} md={12} lg={4} defaultSize={4}>
|
||||
<EventsTable {...tableProps} />
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} sm={12} md={12} lg={8} defaultSize={8}>
|
||||
<EventsChart websiteId={websiteId} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
35
src/components/pages/websites/WebsiteTableView.module.css
Normal file
35
src/components/pages/websites/WebsiteTableView.module.css
Normal file
@@ -0,0 +1,35 @@
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
border-top: 1px solid var(--base300);
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.row > .col {
|
||||
border-left: 1px solid var(--base300);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.row > .col:first-child {
|
||||
border-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.row > .col:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.row {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.row > .col {
|
||||
border-top: 1px solid var(--base300);
|
||||
border-left: 0;
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
77
src/components/pages/websites/WebsitesPage.js
Normal file
77
src/components/pages/websites/WebsitesPage.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm';
|
||||
import WebsiteList from 'components/pages/settings/websites/WebsitesList';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import useUser from 'components/hooks/useUser';
|
||||
import useConfig from 'components/hooks/useConfig';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Icons,
|
||||
Item,
|
||||
Modal,
|
||||
ModalTrigger,
|
||||
Tabs,
|
||||
Text,
|
||||
useToasts,
|
||||
} from 'react-basics';
|
||||
|
||||
export function WebsitesPage() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const [tab, setTab] = useState('my-websites');
|
||||
const [fetch, setFetch] = useState(1);
|
||||
const { user } = useUser();
|
||||
const { cloudMode } = useConfig();
|
||||
const { showToast } = useToasts();
|
||||
|
||||
const handleSave = async () => {
|
||||
setFetch(fetch + 1);
|
||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||
};
|
||||
|
||||
const addButton = (
|
||||
<>
|
||||
{user.role !== ROLES.viewOnly && (
|
||||
<ModalTrigger>
|
||||
<Button variant="primary">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.addWebsite)}</Text>
|
||||
</Button>
|
||||
<Modal title={formatMessage(labels.addWebsite)}>
|
||||
{close => <WebsiteAddForm onSave={handleSave} onClose={close} />}
|
||||
</Modal>
|
||||
</ModalTrigger>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader title={formatMessage(labels.websites)}>{!cloudMode && addButton}</PageHeader>
|
||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
|
||||
<Item key="my-websites">{formatMessage(labels.myWebsites)}</Item>
|
||||
<Item key="team-webaites">{formatMessage(labels.teamWebsites)}</Item>
|
||||
</Tabs>
|
||||
|
||||
{tab === 'my-websites' && (
|
||||
<WebsiteList showEditButton={!cloudMode} showHeader={false} fetch={fetch} />
|
||||
)}
|
||||
{tab === 'team-webaites' && (
|
||||
<WebsiteList
|
||||
showEditButton={!cloudMode}
|
||||
showHeader={false}
|
||||
fetch={fetch}
|
||||
showTeam={true}
|
||||
onlyTeams={true}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsitesPage;
|
||||
Reference in New Issue
Block a user