Moved code into src folder. Added build for component library.

This commit is contained in:
Mike Cao
2023-08-21 02:06:09 -07:00
parent 7a7233ead4
commit ede658771e
490 changed files with 749 additions and 442 deletions

View 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;

View 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;
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -0,0 +1,7 @@
.container a {
color: var(--font-color100);
}
.container a:hover {
color: var(--primary400);
}

View 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>
);
}

View 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;

View 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;
}
}

View 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;
}

View 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>
);
}

View File

@@ -0,0 +1,7 @@
.menu {
position: relative;
}
.content {
min-height: 800px;
}

View 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;

View 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);
}
}

View 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;

View 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>
</>
);
}

View 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;
}
}

View 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;