mirror of
https://github.com/umami-software/umami.git
synced 2026-02-12 00:27:11 +01:00
Merge branch 'dev' into hosts-support
This commit is contained in:
commit
d1559c3a98
281 changed files with 7555 additions and 1973 deletions
|
|
@ -1,8 +1,11 @@
|
|||
'use client';
|
||||
import WebsitesHeader from 'app/(main)/settings/websites/WebsitesHeader';
|
||||
import WebsitesDataTable from 'app/(main)/settings/websites/WebsitesDataTable';
|
||||
import { useTeamUrl } from 'components/hooks';
|
||||
|
||||
export default function WebsitesPage() {
|
||||
const { teamId } = useTeamUrl();
|
||||
|
||||
export default function WebsitesPage({ teamId }: { teamId: string }) {
|
||||
return (
|
||||
<>
|
||||
<WebsitesHeader teamId={teamId} allowCreate={false} />
|
||||
|
|
|
|||
|
|
@ -4,17 +4,41 @@ import { getDateArray } from 'lib/date';
|
|||
import useWebsitePageviews from 'components/hooks/queries/useWebsitePageviews';
|
||||
import { useDateRange } from 'components/hooks';
|
||||
|
||||
export function WebsiteChart({ websiteId }: { websiteId: string }) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
export function WebsiteChart({
|
||||
websiteId,
|
||||
compareMode = false,
|
||||
}: {
|
||||
websiteId: string;
|
||||
compareMode?: boolean;
|
||||
}) {
|
||||
const { dateRange, dateCompare } = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit } = dateRange;
|
||||
const { data, isLoading } = useWebsitePageviews(websiteId);
|
||||
const { data, isLoading } = useWebsitePageviews(websiteId, compareMode ? dateCompare : undefined);
|
||||
const { pageviews, sessions, compare } = (data || {}) as any;
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (data) {
|
||||
return {
|
||||
pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
|
||||
sessions: getDateArray(data.sessions, startDate, endDate, unit),
|
||||
const result = {
|
||||
pageviews: getDateArray(pageviews, startDate, endDate, unit),
|
||||
sessions: getDateArray(sessions, startDate, endDate, unit),
|
||||
};
|
||||
|
||||
if (compare) {
|
||||
result['compare'] = {
|
||||
pageviews: result.pageviews.map(({ x }, i) => ({
|
||||
x,
|
||||
y: compare.pageviews[i]?.y,
|
||||
d: compare.pageviews[i]?.x,
|
||||
})),
|
||||
sessions: result.sessions.map(({ x }, i) => ({
|
||||
x,
|
||||
y: compare.sessions[i]?.y,
|
||||
d: compare.sessions[i]?.x,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return { pageviews: [], sessions: [] };
|
||||
}, [data, startDate, endDate, unit]);
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export default function WebsiteChartList({
|
|||
</Button>
|
||||
</Link>
|
||||
</WebsiteHeader>
|
||||
<WebsiteMetricsBar websiteId={id} showFilter={false} />
|
||||
<WebsiteMetricsBar websiteId={id} />
|
||||
{showCharts && <WebsiteChart websiteId={id} />}
|
||||
</div>
|
||||
) : null;
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
'use client';
|
||||
import { Loading } from 'react-basics';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Page from 'components/layout/Page';
|
||||
import FilterTags from 'components/metrics/FilterTags';
|
||||
import { useNavigation, useWebsite } from 'components/hooks';
|
||||
import WebsiteChart from './WebsiteChart';
|
||||
import WebsiteExpandedView from './WebsiteExpandedView';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
import WebsiteMetricsBar from './WebsiteMetricsBar';
|
||||
import WebsiteTableView from './WebsiteTableView';
|
||||
|
||||
export default function WebsiteDetails({ websiteId }: { websiteId: string }) {
|
||||
const { data: website, isLoading, error } = useWebsite(websiteId);
|
||||
const pathname = usePathname();
|
||||
const { query } = useNavigation();
|
||||
|
||||
if (isLoading || error) {
|
||||
return <Page isLoading={isLoading} error={error} />;
|
||||
}
|
||||
|
||||
const showLinks = !pathname.includes('/share/');
|
||||
const { view, ...params } = query;
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} showLinks={showLinks} />
|
||||
<FilterTags websiteId={websiteId} params={params} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} sticky={true} />
|
||||
<WebsiteChart websiteId={websiteId} />
|
||||
{!website && <Loading icon="dots" style={{ minHeight: 300 }} />}
|
||||
{website && (
|
||||
<>
|
||||
{!view && <WebsiteTableView websiteId={websiteId} domainName={website.domain} />}
|
||||
{view && <WebsiteExpandedView websiteId={websiteId} domainName={website.domain} />}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
37
src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx
Normal file
37
src/app/(main)/websites/[websiteId]/WebsiteDetailsPage.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
'use client';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import FilterTags from 'components/metrics/FilterTags';
|
||||
import { useNavigation } from 'components/hooks';
|
||||
import WebsiteChart from './WebsiteChart';
|
||||
import WebsiteExpandedView from './WebsiteExpandedView';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
import WebsiteMetricsBar from './WebsiteMetricsBar';
|
||||
import WebsiteTableView from './WebsiteTableView';
|
||||
import WebsiteProvider from './WebsiteProvider';
|
||||
import { FILTER_COLUMNS } from 'lib/constants';
|
||||
|
||||
export default function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
|
||||
const pathname = usePathname();
|
||||
const { query } = useNavigation();
|
||||
|
||||
const showLinks = !pathname.includes('/share/');
|
||||
const { view } = query;
|
||||
|
||||
const params = Object.keys(query).reduce((obj, key) => {
|
||||
if (FILTER_COLUMNS[key]) {
|
||||
obj[key] = query[key];
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<WebsiteHeader websiteId={websiteId} showLinks={showLinks} />
|
||||
<FilterTags websiteId={websiteId} params={params} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} sticky={true} />
|
||||
<WebsiteChart websiteId={websiteId} />
|
||||
{!view && <WebsiteTableView websiteId={websiteId} />}
|
||||
{view && <WebsiteExpandedView websiteId={websiteId} />}
|
||||
</WebsiteProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -19,6 +19,8 @@ import styles from './WebsiteExpandedView.module.css';
|
|||
|
||||
const views = {
|
||||
url: PagesTable,
|
||||
entry: PagesTable,
|
||||
exit: PagesTable,
|
||||
title: PagesTable,
|
||||
referrer: ReferrersTable,
|
||||
host: HostsTable,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import classNames from 'classnames';
|
||||
import { Button, Icon, Icons, Popup, PopupTrigger, Text } from 'react-basics';
|
||||
import PopupForm from 'app/(main)/reports/[reportId]/PopupForm';
|
||||
import FilterSelectForm from 'app/(main)/reports/[reportId]/FilterSelectForm';
|
||||
|
|
@ -9,14 +8,22 @@ import styles from './WebsiteFilterButton.module.css';
|
|||
export function WebsiteFilterButton({
|
||||
websiteId,
|
||||
className,
|
||||
position = 'bottom',
|
||||
alignment = 'end',
|
||||
showText = true,
|
||||
}: {
|
||||
websiteId: string;
|
||||
className?: string;
|
||||
position?: 'bottom' | 'top' | 'left' | 'right';
|
||||
alignment?: 'end' | 'center' | 'start';
|
||||
showText?: boolean;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { renderUrl, router } = useNavigation();
|
||||
const { fields } = useFields();
|
||||
const [{ startDate, endDate }] = useDateRange(websiteId);
|
||||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange(websiteId);
|
||||
|
||||
const handleAddFilter = ({ name, operator, value }) => {
|
||||
const prefix = OPERATOR_PREFIXES[operator];
|
||||
|
|
@ -25,14 +32,14 @@ export function WebsiteFilterButton({
|
|||
};
|
||||
|
||||
return (
|
||||
<PopupTrigger>
|
||||
<Button className={classNames(className, styles.button)} variant="quiet">
|
||||
<PopupTrigger className={className}>
|
||||
<Button className={styles.button} variant="quiet">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.filter)}</Text>
|
||||
{showText && <Text>{formatMessage(labels.filter)}</Text>}
|
||||
</Button>
|
||||
<Popup position="bottom" alignment="end">
|
||||
<Popup position={position} alignment={alignment}>
|
||||
{(close: () => void) => {
|
||||
return (
|
||||
<PopupForm>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ export function WebsiteHeader({
|
|||
icon: <Icons.Overview />,
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.compare),
|
||||
icon: <Icons.Compare />,
|
||||
path: '/compare',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.realtime),
|
||||
icon: <Icons.Clock />,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--base50);
|
||||
|
|
@ -11,10 +11,22 @@
|
|||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.vs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-basis: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
|
|
@ -38,9 +50,3 @@
|
|||
border-bottom: 1px solid var(--base300);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,96 +1,133 @@
|
|||
import classNames from 'classnames';
|
||||
import { useMessages, useSticky } from 'components/hooks';
|
||||
import { useDateRange, useMessages, useSticky } from 'components/hooks';
|
||||
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
|
||||
import MetricCard from 'components/metrics/MetricCard';
|
||||
import MetricsBar from 'components/metrics/MetricsBar';
|
||||
import { formatShortTime } from 'lib/format';
|
||||
import { formatShortTime, formatLongNumber } from 'lib/format';
|
||||
import WebsiteFilterButton from './WebsiteFilterButton';
|
||||
import styles from './WebsiteMetricsBar.module.css';
|
||||
import useWebsiteStats from 'components/hooks/queries/useWebsiteStats';
|
||||
import styles from './WebsiteMetricsBar.module.css';
|
||||
import { Dropdown, Item } from 'react-basics';
|
||||
import useStore, { setWebsiteDateCompare } from 'store/websites';
|
||||
|
||||
export function WebsiteMetricsBar({
|
||||
websiteId,
|
||||
showFilter = true,
|
||||
sticky,
|
||||
showChange = false,
|
||||
compareMode = false,
|
||||
showFilter = false,
|
||||
}: {
|
||||
websiteId: string;
|
||||
showFilter?: boolean;
|
||||
sticky?: boolean;
|
||||
showChange?: boolean;
|
||||
compareMode?: boolean;
|
||||
showFilter?: boolean;
|
||||
}) {
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const dateCompare = useStore(state => state[websiteId]?.dateCompare);
|
||||
const { ref, isSticky } = useSticky({ enabled: sticky });
|
||||
const { data, isLoading, isFetched, error } = useWebsiteStats(websiteId);
|
||||
const { data, isLoading, isFetched, error } = useWebsiteStats(
|
||||
websiteId,
|
||||
compareMode && dateCompare,
|
||||
);
|
||||
const isAllTime = dateRange.value === 'all';
|
||||
|
||||
const { pageviews, visitors, visits, bounces, totaltime } = data || {};
|
||||
const num = Math.min(data && visitors.value, data && bounces.value);
|
||||
const diffs = data && {
|
||||
pageviews: pageviews.value - pageviews.change,
|
||||
visitors: visitors.value - visitors.change,
|
||||
visits: visits.value - visits.change,
|
||||
bounces: bounces.value - bounces.change,
|
||||
totaltime: totaltime.value - totaltime.change,
|
||||
};
|
||||
|
||||
const metrics = data
|
||||
? [
|
||||
{
|
||||
...pageviews,
|
||||
label: formatMessage(labels.views),
|
||||
change: pageviews.value - pageviews.prev,
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
...visits,
|
||||
label: formatMessage(labels.visits),
|
||||
change: visits.value - visits.prev,
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
...visitors,
|
||||
label: formatMessage(labels.visitors),
|
||||
change: visitors.value - visitors.prev,
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.bounceRate),
|
||||
value: (Math.min(visits.value, bounces.value) / visits.value) * 100,
|
||||
prev: (Math.min(visits.prev, bounces.prev) / visits.prev) * 100,
|
||||
change:
|
||||
(Math.min(visits.value, bounces.value) / visits.value) * 100 -
|
||||
(Math.min(visits.prev, bounces.prev) / visits.prev) * 100,
|
||||
formatValue: n => Math.round(+n) + '%',
|
||||
reverseColors: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.visitDuration),
|
||||
value: totaltime.value / visits.value,
|
||||
prev: totaltime.prev / visits.prev,
|
||||
change: totaltime.value / visits.value - totaltime.prev / visits.prev,
|
||||
formatValue: n =>
|
||||
`${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const items = [
|
||||
{ label: formatMessage(labels.previousPeriod), value: 'prev' },
|
||||
{ label: formatMessage(labels.previousYear), value: 'yoy' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(styles.container, {
|
||||
[styles.sticky]: sticky,
|
||||
[styles.isSticky]: isSticky,
|
||||
[styles.isSticky]: sticky && isSticky,
|
||||
})}
|
||||
>
|
||||
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
|
||||
{pageviews && visitors && (
|
||||
<>
|
||||
<MetricCard
|
||||
label={formatMessage(labels.views)}
|
||||
value={pageviews.value}
|
||||
change={pageviews.change}
|
||||
/>
|
||||
<MetricCard
|
||||
label={formatMessage(labels.visits)}
|
||||
value={visits.value}
|
||||
change={visits.change}
|
||||
/>
|
||||
<MetricCard
|
||||
label={formatMessage(labels.visitors)}
|
||||
value={visitors.value}
|
||||
change={visitors.change}
|
||||
/>
|
||||
<MetricCard
|
||||
label={formatMessage(labels.bounceRate)}
|
||||
value={visitors.value ? (num / visitors.value) * 100 : 0}
|
||||
change={
|
||||
visitors.value && visitors.change
|
||||
? (num / visitors.value) * 100 -
|
||||
(Math.min(diffs.visitors, diffs.bounces) / diffs.visitors) * 100 || 0
|
||||
: 0
|
||||
}
|
||||
format={n => Number(n).toFixed(0) + '%'}
|
||||
reverseColors
|
||||
/>
|
||||
<MetricCard
|
||||
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>
|
||||
<div>
|
||||
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
|
||||
{metrics.map(({ label, value, prev, change, formatValue, reverseColors }) => {
|
||||
return (
|
||||
<MetricCard
|
||||
key={label}
|
||||
value={value}
|
||||
previousValue={prev}
|
||||
label={label}
|
||||
change={change}
|
||||
formatValue={formatValue}
|
||||
reverseColors={reverseColors}
|
||||
showChange={!isAllTime && (compareMode || showChange)}
|
||||
showPrevious={!isAllTime && compareMode}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MetricsBar>
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
{showFilter && <WebsiteFilterButton websiteId={websiteId} className={styles.button} />}
|
||||
<WebsiteDateFilter websiteId={websiteId} />
|
||||
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
|
||||
<WebsiteDateFilter websiteId={websiteId} showAllTime={!compareMode} />
|
||||
{compareMode && (
|
||||
<div className={styles.vs}>
|
||||
<b>VS</b>
|
||||
<Dropdown
|
||||
className={styles.dropdown}
|
||||
items={items}
|
||||
value={dateCompare || 'prev'}
|
||||
renderValue={value => items.find(i => i.value === value)?.label}
|
||||
alignment="end"
|
||||
onChange={(value: any) => setWebsiteDateCompare(websiteId, value)}
|
||||
>
|
||||
{items.map(({ label, value }) => (
|
||||
<Item key={value}>{label}</Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,17 +11,10 @@ import CountriesTable from 'components/metrics/CountriesTable';
|
|||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import EventsChart from 'components/metrics/EventsChart';
|
||||
|
||||
export default function WebsiteTableView({
|
||||
websiteId,
|
||||
domainName,
|
||||
}: {
|
||||
websiteId: string;
|
||||
domainName: string;
|
||||
}) {
|
||||
export default function WebsiteTableView({ websiteId }: { websiteId: string }) {
|
||||
const [countryData, setCountryData] = useState();
|
||||
const tableProps = {
|
||||
websiteId,
|
||||
domainName,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
'use client';
|
||||
import WebsiteHeader from '../WebsiteHeader';
|
||||
import WebsiteMetricsBar from '../WebsiteMetricsBar';
|
||||
import FilterTags from 'components/metrics/FilterTags';
|
||||
import { useNavigation } from 'components/hooks';
|
||||
import { FILTER_COLUMNS } from 'lib/constants';
|
||||
import WebsiteChart from '../WebsiteChart';
|
||||
import WebsiteCompareTables from './WebsiteCompareTables';
|
||||
import WebsiteProvider from '../WebsiteProvider';
|
||||
|
||||
export function WebsiteComparePage({ websiteId }) {
|
||||
const { query } = useNavigation();
|
||||
|
||||
const params = Object.keys(query).reduce((obj, key) => {
|
||||
if (FILTER_COLUMNS[key]) {
|
||||
obj[key] = query[key];
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<FilterTags websiteId={websiteId} params={params} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} showFilter={true} />
|
||||
<WebsiteChart websiteId={websiteId} compareMode={true} />
|
||||
<WebsiteCompareTables websiteId={websiteId} />
|
||||
</WebsiteProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteComparePage;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
.container {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: 200px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--base800);
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
import { useState } from 'react';
|
||||
import SideNav from 'components/layout/SideNav';
|
||||
import { useDateRange, useMessages, useNavigation } from 'components/hooks';
|
||||
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 ScreenTable from 'components/metrics/ScreenTable';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import RegionsTable from 'components/metrics/RegionsTable';
|
||||
import CitiesTable from 'components/metrics/CitiesTable';
|
||||
import LanguagesTable from 'components/metrics/LanguagesTable';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import QueryParametersTable from 'components/metrics/QueryParametersTable';
|
||||
import { Grid, GridRow } from 'components/layout/Grid';
|
||||
import MetricsTable from 'components/metrics/MetricsTable';
|
||||
import useStore from 'store/websites';
|
||||
import { getCompareDate } from 'lib/date';
|
||||
import { formatNumber } from 'lib/format';
|
||||
import ChangeLabel from 'components/metrics/ChangeLabel';
|
||||
import styles from './WebsiteCompareTables.module.css';
|
||||
|
||||
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 function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
|
||||
const [data, setData] = useState([]);
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const dateCompare = useStore(state => state[websiteId]?.dateCompare);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const {
|
||||
renderUrl,
|
||||
query: { view },
|
||||
} = useNavigation();
|
||||
const Component: typeof MetricsTable = views[view || 'url'] || (() => null);
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'url',
|
||||
label: formatMessage(labels.pages),
|
||||
url: renderUrl({ view: 'url' }),
|
||||
},
|
||||
{
|
||||
key: 'referrer',
|
||||
label: formatMessage(labels.referrers),
|
||||
url: renderUrl({ view: 'referrer' }),
|
||||
},
|
||||
{
|
||||
key: 'browser',
|
||||
label: formatMessage(labels.browsers),
|
||||
url: renderUrl({ view: 'browser' }),
|
||||
},
|
||||
{
|
||||
key: 'os',
|
||||
label: formatMessage(labels.os),
|
||||
url: renderUrl({ view: 'os' }),
|
||||
},
|
||||
{
|
||||
key: 'device',
|
||||
label: formatMessage(labels.devices),
|
||||
url: renderUrl({ view: 'device' }),
|
||||
},
|
||||
{
|
||||
key: 'country',
|
||||
label: formatMessage(labels.countries),
|
||||
url: renderUrl({ view: 'country' }),
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
label: formatMessage(labels.regions),
|
||||
url: renderUrl({ view: 'region' }),
|
||||
},
|
||||
{
|
||||
key: 'city',
|
||||
label: formatMessage(labels.cities),
|
||||
url: renderUrl({ view: 'city' }),
|
||||
},
|
||||
{
|
||||
key: 'language',
|
||||
label: formatMessage(labels.languages),
|
||||
url: renderUrl({ view: 'language' }),
|
||||
},
|
||||
{
|
||||
key: 'screen',
|
||||
label: formatMessage(labels.screens),
|
||||
url: renderUrl({ view: 'screen' }),
|
||||
},
|
||||
{
|
||||
key: 'event',
|
||||
label: formatMessage(labels.events),
|
||||
url: renderUrl({ view: 'event' }),
|
||||
},
|
||||
{
|
||||
key: 'query',
|
||||
label: formatMessage(labels.queryParameters),
|
||||
url: renderUrl({ view: 'query' }),
|
||||
},
|
||||
];
|
||||
|
||||
const renderChange = ({ x, y }) => {
|
||||
const prev = data.find(d => d.x === x)?.y;
|
||||
const value = y - prev;
|
||||
const change = Math.abs(((y - prev) / prev) * 100);
|
||||
|
||||
return !isNaN(change) && <ChangeLabel value={value}>{formatNumber(change)}%</ChangeLabel>;
|
||||
};
|
||||
|
||||
const { startDate, endDate } = getCompareDate(
|
||||
dateCompare,
|
||||
dateRange.startDate,
|
||||
dateRange.endDate,
|
||||
);
|
||||
|
||||
const params = {
|
||||
startAt: startDate.getTime(),
|
||||
endAt: endDate.getTime(),
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid className={styles.container}>
|
||||
<GridRow columns="compare">
|
||||
<SideNav className={styles.nav} items={items} selectedKey={view} shallow={true} />
|
||||
<div>
|
||||
<div className={styles.title}>{formatMessage(labels.previous)}</div>
|
||||
<Component
|
||||
websiteId={websiteId}
|
||||
limit={20}
|
||||
showMore={false}
|
||||
onDataLoad={setData}
|
||||
params={params}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.title}> {formatMessage(labels.current)}</div>
|
||||
<Component
|
||||
websiteId={websiteId}
|
||||
limit={20}
|
||||
showMore={false}
|
||||
renderChange={renderChange}
|
||||
/>
|
||||
</div>
|
||||
</GridRow>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebsiteCompareTables;
|
||||
10
src/app/(main)/websites/[websiteId]/compare/page.tsx
Normal file
10
src/app/(main)/websites/[websiteId]/compare/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import WebsiteComparePage from './WebsiteComparePage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function ({ params: { websiteId } }) {
|
||||
return <WebsiteComparePage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Website Comparison',
|
||||
};
|
||||
|
|
@ -7,7 +7,7 @@ import styles from './EventDataMetricsBar.module.css';
|
|||
export function EventDataMetricsBar({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { get, useQuery } = useApi();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
|
||||
const { data, error, isLoading, isFetched } = useQuery({
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useDateRange, useApi, useNavigation } from 'components/hooks';
|
|||
import styles from './WebsiteEventData.module.css';
|
||||
|
||||
function useData(websiteId: string, event: string) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, error, isLoading } = useQuery({
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import WebsiteDetails from './WebsiteDetails';
|
||||
import WebsiteDetailsPage from './WebsiteDetailsPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function WebsitePage({ params: { websiteId } }) {
|
||||
return <WebsiteDetails websiteId={websiteId} />;
|
||||
return <WebsiteDetailsPage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function RealtimeCountries({ data }) {
|
|||
({ x: code }) => (
|
||||
<span className={classNames(locale, styles.row)}>
|
||||
<img
|
||||
src={`${process.env.basePath}/images/flags/${code?.toLowerCase() || 'xx'}.png`}
|
||||
src={`${process.env.basePath || ''}/images/flags/${code?.toLowerCase() || 'xx'}.png`}
|
||||
alt={code}
|
||||
/>
|
||||
{countryNames[code]}
|
||||
|
|
|
|||
|
|
@ -14,25 +14,21 @@ export function RealtimeHeader({ data }: { data: RealtimeData }) {
|
|||
className={styles.card}
|
||||
label={formatMessage(labels.views)}
|
||||
value={pageviews?.length}
|
||||
hideComparison
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.visitors)}
|
||||
value={visitors?.length}
|
||||
hideComparison
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.events)}
|
||||
value={events?.length}
|
||||
hideComparison
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.countries)}
|
||||
value={countries?.length}
|
||||
hideComparison
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import WebsitesPage from './WebsitesPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function ({ params: { teamId, userId } }) {
|
||||
return <WebsitesPage teamId={teamId} userId={userId} />;
|
||||
export default function () {
|
||||
return <WebsitesPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue