a lot
This commit is contained in:
129
apps/web/src/components/overview/overview-filters-buttons.tsx
Normal file
129
apps/web/src/components/overview/overview-filters-buttons.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
import { Button } from '../ui/button';
|
||||
import { Combobox } from '../ui/combobox';
|
||||
import { Label } from '../ui/label';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
|
||||
export function OverviewFiltersButtons() {
|
||||
const options = useOverviewOptions();
|
||||
return (
|
||||
<>
|
||||
{options.referrer && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={X}
|
||||
onClick={() => options.setReferrer(null)}
|
||||
>
|
||||
{options.referrer}
|
||||
</Button>
|
||||
)}
|
||||
{options.device && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={X}
|
||||
onClick={() => options.setDevice(null)}
|
||||
>
|
||||
{options.device}
|
||||
</Button>
|
||||
)}
|
||||
{options.page && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={X}
|
||||
onClick={() => options.setPage(null)}
|
||||
>
|
||||
{options.page}
|
||||
</Button>
|
||||
)}
|
||||
{options.utmSource && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={X}
|
||||
onClick={() => options.setUtmSource(null)}
|
||||
>
|
||||
{options.utmSource}
|
||||
</Button>
|
||||
)}
|
||||
{options.utmMedium && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={X}
|
||||
onClick={() => options.setUtmMedium(null)}
|
||||
>
|
||||
{options.utmMedium}
|
||||
</Button>
|
||||
)}
|
||||
{options.utmCampaign && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={X}
|
||||
onClick={() => options.setUtmCampaign(null)}
|
||||
>
|
||||
{options.utmCampaign}
|
||||
</Button>
|
||||
)}
|
||||
{options.utmTerm && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={X}
|
||||
onClick={() => options.setUtmTerm(null)}
|
||||
>
|
||||
{options.utmTerm}
|
||||
</Button>
|
||||
)}
|
||||
{options.utmContent && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={X}
|
||||
onClick={() => options.setUtmContent(null)}
|
||||
>
|
||||
{options.utmContent}
|
||||
</Button>
|
||||
)}
|
||||
{options.country && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={X}
|
||||
onClick={() => options.setCountry(null)}
|
||||
>
|
||||
{options.country}
|
||||
</Button>
|
||||
)}
|
||||
{options.region && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={X}
|
||||
onClick={() => options.setRegion(null)}
|
||||
>
|
||||
{options.region}
|
||||
</Button>
|
||||
)}
|
||||
{options.city && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={X}
|
||||
onClick={() => options.setCity(null)}
|
||||
>
|
||||
{options.city}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
121
apps/web/src/components/overview/overview-filters.tsx
Normal file
121
apps/web/src/components/overview/overview-filters.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
'use client';
|
||||
|
||||
import { api } from '@/app/_trpc/client';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Combobox } from '../ui/combobox';
|
||||
import { Label } from '../ui/label';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
|
||||
export function OverviewFilters() {
|
||||
const { projectId } = useAppParams();
|
||||
const options = useOverviewOptions();
|
||||
|
||||
const { data: referrers } = api.chart.values.useQuery({
|
||||
projectId,
|
||||
property: 'referrer',
|
||||
event: 'session_start',
|
||||
});
|
||||
|
||||
const { data: devices } = api.chart.values.useQuery({
|
||||
projectId,
|
||||
property: 'device',
|
||||
event: 'session_start',
|
||||
});
|
||||
|
||||
const { data: pages } = api.chart.values.useQuery({
|
||||
projectId,
|
||||
property: 'path',
|
||||
event: 'screen_view',
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-medium mb-8">Overview filters</h2>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<Label className="flex justify-between">
|
||||
Referrer
|
||||
<button
|
||||
className={cn(
|
||||
'text-slate-500 transition-opacity opacity-100',
|
||||
options.referrer === null && 'opacity-0'
|
||||
)}
|
||||
onClick={() => options.setReferrer(null)}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</Label>
|
||||
<Combobox
|
||||
className="w-full"
|
||||
onChange={(value) => options.setReferrer(value)}
|
||||
label="Referrer"
|
||||
placeholder="Referrer"
|
||||
items={
|
||||
referrers?.values?.filter(Boolean)?.map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
})) ?? []
|
||||
}
|
||||
value={options.referrer}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="flex justify-between">
|
||||
Device
|
||||
<button
|
||||
className={cn(
|
||||
'opacity-100 text-slate-500 transition-opacity',
|
||||
options.device === null && 'opacity-0'
|
||||
)}
|
||||
onClick={() => options.setDevice(null)}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</Label>
|
||||
<Combobox
|
||||
className="w-full"
|
||||
onChange={(value) => options.setDevice(value)}
|
||||
label="Device"
|
||||
placeholder="Device"
|
||||
items={
|
||||
devices?.values?.filter(Boolean)?.map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
})) ?? []
|
||||
}
|
||||
value={options.device}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="flex justify-between">
|
||||
Page
|
||||
<button
|
||||
className={cn(
|
||||
'opacity-100 text-slate-500 transition-opacity',
|
||||
options.page === null && 'opacity-0'
|
||||
)}
|
||||
onClick={() => options.setPage(null)}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</Label>
|
||||
<Combobox
|
||||
className="w-full"
|
||||
onChange={(value) => options.setPage(value)}
|
||||
label="Page"
|
||||
placeholder="Page"
|
||||
items={
|
||||
pages?.values?.filter(Boolean)?.map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
})) ?? []
|
||||
}
|
||||
value={options.page}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
199
apps/web/src/components/overview/overview-top-devices.tsx
Normal file
199
apps/web/src/components/overview/overview-top-devices.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
import { useOverviewWidget } from './useOverviewWidget';
|
||||
|
||||
export default function OverviewTopDevices() {
|
||||
const { filters, interval, range, previous, setCountry, setRegion, setCity } =
|
||||
useOverviewOptions();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('tech', {
|
||||
devices: {
|
||||
title: 'Top devices',
|
||||
btn: 'Devices',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'user',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'device',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
title: 'Top browser',
|
||||
btn: 'Browser',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'user',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'browser',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
browser_version: {
|
||||
title: 'Top Browser Version',
|
||||
btn: 'Browser Version',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'user',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'browser_version',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
os: {
|
||||
title: 'Top OS',
|
||||
btn: 'OS',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'user',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'os',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
os_version: {
|
||||
title: 'Top OS version',
|
||||
btn: 'OS Version',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'user',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'os_version',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Widget className="col-span-6 md:col-span-3">
|
||||
<WidgetHead className="flex items-center justify-between">
|
||||
<div className="title">{widget.title}</div>
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
>
|
||||
{w.btn}
|
||||
</button>
|
||||
))}
|
||||
</WidgetButtons>
|
||||
</WidgetHead>
|
||||
<WidgetBody>
|
||||
<Chart
|
||||
hideID
|
||||
{...widget.chart}
|
||||
previous={false}
|
||||
onClick={(item) => {
|
||||
// switch (widget.key) {
|
||||
// case 'browser':
|
||||
// setWidget('browser_version');
|
||||
// // setCountry(item.name);
|
||||
// break;
|
||||
// case 'regions':
|
||||
// setWidget('cities');
|
||||
// setRegion(item.name);
|
||||
// break;
|
||||
// case 'cities':
|
||||
// setCity(item.name);
|
||||
// break;
|
||||
// }
|
||||
}}
|
||||
/>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</>
|
||||
);
|
||||
}
|
||||
75
apps/web/src/components/overview/overview-top-events.tsx
Normal file
75
apps/web/src/components/overview/overview-top-events.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
import { useOverviewWidget } from './useOverviewWidget';
|
||||
|
||||
export default function OverviewTopEvents() {
|
||||
const { filters, interval, range, previous } = useOverviewOptions();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('ev', {
|
||||
all: {
|
||||
title: 'Top events',
|
||||
btn: 'All',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters: [
|
||||
...filters,
|
||||
{
|
||||
id: 'ex_session',
|
||||
name: 'name',
|
||||
operator: 'isNot',
|
||||
value: ['session_start', 'session_end'],
|
||||
},
|
||||
],
|
||||
id: 'A',
|
||||
name: '*',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Widget className="col-span-6 md:col-span-3">
|
||||
<WidgetHead className="flex items-center justify-between">
|
||||
<div className="title">{widget.title}</div>
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
>
|
||||
{w.btn}
|
||||
</button>
|
||||
))}
|
||||
</WidgetButtons>
|
||||
</WidgetHead>
|
||||
<WidgetBody>
|
||||
<Chart hideID {...widget.chart} previous={false} />
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</>
|
||||
);
|
||||
}
|
||||
171
apps/web/src/components/overview/overview-top-geo.tsx
Normal file
171
apps/web/src/components/overview/overview-top-geo.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
import { useOverviewWidget } from './useOverviewWidget';
|
||||
|
||||
export default function OverviewTopGeo() {
|
||||
const { filters, interval, range, previous, setCountry, setRegion, setCity } =
|
||||
useOverviewOptions();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('geo', {
|
||||
map: {
|
||||
title: 'Map',
|
||||
btn: 'Map',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'country',
|
||||
},
|
||||
],
|
||||
chartType: 'map',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
countries: {
|
||||
title: 'Top countries',
|
||||
btn: 'Countries',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'country',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
regions: {
|
||||
title: 'Top regions',
|
||||
btn: 'Regions',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'region',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
cities: {
|
||||
title: 'Top cities',
|
||||
btn: 'Cities',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'city',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Widget className="col-span-6 md:col-span-3">
|
||||
<WidgetHead className="flex items-center justify-between">
|
||||
<div className="title">{widget.title}</div>
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
>
|
||||
{w.btn}
|
||||
</button>
|
||||
))}
|
||||
</WidgetButtons>
|
||||
</WidgetHead>
|
||||
<WidgetBody>
|
||||
<Chart
|
||||
hideID
|
||||
{...widget.chart}
|
||||
previous={false}
|
||||
onClick={(item) => {
|
||||
switch (widget.key) {
|
||||
case 'countries':
|
||||
setWidget('regions');
|
||||
setCountry(item.name);
|
||||
break;
|
||||
case 'regions':
|
||||
setWidget('cities');
|
||||
setRegion(item.name);
|
||||
break;
|
||||
case 'cities':
|
||||
setCity(item.name);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</>
|
||||
);
|
||||
}
|
||||
130
apps/web/src/components/overview/overview-top-pages.tsx
Normal file
130
apps/web/src/components/overview/overview-top-pages.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
import { useOverviewWidget } from './useOverviewWidget';
|
||||
|
||||
export default function OverviewTopPages() {
|
||||
const { filters, interval, range, previous, setPage } = useOverviewOptions();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('pages', {
|
||||
top: {
|
||||
title: 'Top pages',
|
||||
btn: 'Top pages',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'screen_view',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'path',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval,
|
||||
name: 'Top sources',
|
||||
range,
|
||||
previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
entries: {
|
||||
title: 'Entry Pages',
|
||||
btn: 'Entries',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'path',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval,
|
||||
name: 'Top sources',
|
||||
range,
|
||||
previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
exits: {
|
||||
title: 'Exit Pages',
|
||||
btn: 'Exits',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_end',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'path',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval,
|
||||
name: 'Top sources',
|
||||
range,
|
||||
previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Widget className="col-span-6 md:col-span-3">
|
||||
<WidgetHead className="flex items-center justify-between">
|
||||
<div className="title">{widget.title}</div>
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
>
|
||||
{w.btn}
|
||||
</button>
|
||||
))}
|
||||
</WidgetButtons>
|
||||
</WidgetHead>
|
||||
<WidgetBody>
|
||||
<Chart
|
||||
hideID
|
||||
{...widget.chart}
|
||||
previous={false}
|
||||
onClick={(item) => {
|
||||
setPage(item.name);
|
||||
}}
|
||||
/>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</>
|
||||
);
|
||||
}
|
||||
245
apps/web/src/components/overview/overview-top-sources.tsx
Normal file
245
apps/web/src/components/overview/overview-top-sources.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
'use client';
|
||||
|
||||
import { Chart } from '@/components/report/chart';
|
||||
import type { IChartInput } from '@/types';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import { Widget, WidgetBody } from '../Widget';
|
||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||
import { useOverviewOptions } from './useOverviewOptions';
|
||||
import { useOverviewWidget } from './useOverviewWidget';
|
||||
|
||||
export default function OverviewTopSources() {
|
||||
const {
|
||||
filters,
|
||||
interval,
|
||||
range,
|
||||
previous,
|
||||
setReferrer,
|
||||
setUtmSource,
|
||||
setUtmMedium,
|
||||
setUtmCampaign,
|
||||
setUtmTerm,
|
||||
setUtmContent,
|
||||
} = useOverviewOptions();
|
||||
const [widget, setWidget, widgets] = useOverviewWidget('sources', {
|
||||
all: {
|
||||
title: 'Top sources',
|
||||
btn: 'All',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters: filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'referrer',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
utm_source: {
|
||||
title: 'UTM Source',
|
||||
btn: 'Source',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'properties.query.utm_source',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
utm_medium: {
|
||||
title: 'UTM Medium',
|
||||
btn: 'Medium',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'properties.query.utm_medium',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
utm_campaign: {
|
||||
title: 'UTM Campaign',
|
||||
btn: 'Campaign',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'properties.query.utm_campaign',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
utm_term: {
|
||||
title: 'UTM Term',
|
||||
btn: 'Term',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'properties.query.utm_term',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
utm_content: {
|
||||
title: 'UTM Content',
|
||||
btn: 'Content',
|
||||
chart: {
|
||||
projectId: '',
|
||||
events: [
|
||||
{
|
||||
segment: 'event',
|
||||
filters,
|
||||
id: 'A',
|
||||
name: 'session_start',
|
||||
},
|
||||
],
|
||||
breakdowns: [
|
||||
{
|
||||
id: 'A',
|
||||
name: 'properties.query.utm_content',
|
||||
},
|
||||
],
|
||||
chartType: 'bar',
|
||||
lineType: 'monotone',
|
||||
interval: interval,
|
||||
name: 'Top sources',
|
||||
range: range,
|
||||
previous: previous,
|
||||
metric: 'sum',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Widget className="col-span-6 md:col-span-3">
|
||||
<WidgetHead className="flex items-center justify-between">
|
||||
<div className="title">{widget.title}</div>
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
>
|
||||
{w.btn}
|
||||
</button>
|
||||
))}
|
||||
</WidgetButtons>
|
||||
</WidgetHead>
|
||||
<WidgetBody>
|
||||
<Chart
|
||||
hideID
|
||||
{...widget.chart}
|
||||
previous={false}
|
||||
onClick={(item) => {
|
||||
switch (widget.key) {
|
||||
case 'all':
|
||||
setReferrer(item.name);
|
||||
break;
|
||||
case 'utm_source':
|
||||
setUtmSource(item.name);
|
||||
break;
|
||||
case 'utm_medium':
|
||||
setUtmMedium(item.name);
|
||||
break;
|
||||
case 'utm_campaign':
|
||||
setUtmCampaign(item.name);
|
||||
break;
|
||||
case 'utm_term':
|
||||
setUtmTerm(item.name);
|
||||
break;
|
||||
case 'utm_content':
|
||||
setUtmContent(item.name);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</>
|
||||
);
|
||||
}
|
||||
25
apps/web/src/components/overview/overview-widget.tsx
Normal file
25
apps/web/src/components/overview/overview-widget.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import type { WidgetHeadProps } from '../Widget';
|
||||
import { WidgetHead as WidgetHeadBase } from '../Widget';
|
||||
|
||||
export function WidgetHead({ className, ...props }: WidgetHeadProps) {
|
||||
return (
|
||||
<WidgetHeadBase
|
||||
className={cn('flex items-center justify-between', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function WidgetButtons({ className, ...props }: WidgetHeadProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex gap-2 [&_button]:text-xs [&_button]:opacity-50 [&_button.active]:opacity-100',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
234
apps/web/src/components/overview/useOverviewOptions.ts
Normal file
234
apps/web/src/components/overview/useOverviewOptions.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { IChartInput } from '@/types';
|
||||
import { getDefaultIntervalByRange, timeRanges } from '@/utils/constants';
|
||||
import { mapKeys } from '@/utils/validation';
|
||||
import {
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
parseAsString,
|
||||
parseAsStringEnum,
|
||||
useQueryState,
|
||||
} from 'nuqs';
|
||||
|
||||
const nuqsOptions = { history: 'push' } as const;
|
||||
|
||||
export function useOverviewOptions() {
|
||||
const [previous, setPrevious] = useQueryState(
|
||||
'name',
|
||||
parseAsBoolean.withDefault(true).withOptions(nuqsOptions)
|
||||
);
|
||||
const [range, setRange] = useQueryState(
|
||||
'range',
|
||||
parseAsStringEnum(mapKeys(timeRanges))
|
||||
.withDefault('7d')
|
||||
.withOptions(nuqsOptions)
|
||||
);
|
||||
const interval = getDefaultIntervalByRange(range);
|
||||
const [metric, setMetric] = useQueryState(
|
||||
'metric',
|
||||
parseAsInteger.withDefault(0).withOptions(nuqsOptions)
|
||||
);
|
||||
|
||||
// Filters
|
||||
const [referrer, setReferrer] = useQueryState(
|
||||
'referrer',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
);
|
||||
const [device, setDevice] = useQueryState(
|
||||
'device',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
);
|
||||
const [page, setPage] = useQueryState(
|
||||
'page',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
);
|
||||
|
||||
const [utmSource, setUtmSource] = useQueryState(
|
||||
'utm_source',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
);
|
||||
const [utmMedium, setUtmMedium] = useQueryState(
|
||||
'utm_medium',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
);
|
||||
const [utmCampaign, setUtmCampaign] = useQueryState(
|
||||
'utm_campaign',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
);
|
||||
const [utmContent, setUtmContent] = useQueryState(
|
||||
'utm_content',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
);
|
||||
const [utmTerm, setUtmTerm] = useQueryState(
|
||||
'utm_term',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
);
|
||||
|
||||
const [country, setCountry] = useQueryState(
|
||||
'country',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
);
|
||||
const [region, setRegion] = useQueryState(
|
||||
'region',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
);
|
||||
const [city, setCity] = useQueryState(
|
||||
'city',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
);
|
||||
|
||||
const filters = useMemo(() => {
|
||||
const filters: IChartInput['events'][number]['filters'] = [];
|
||||
if (referrer) {
|
||||
filters.push({
|
||||
id: 'referrer',
|
||||
operator: 'is',
|
||||
name: 'referrer',
|
||||
value: [referrer],
|
||||
});
|
||||
}
|
||||
|
||||
if (page) {
|
||||
filters.push({
|
||||
id: 'path',
|
||||
operator: 'is',
|
||||
name: 'path',
|
||||
value: [page],
|
||||
});
|
||||
}
|
||||
|
||||
if (device) {
|
||||
filters.push({
|
||||
id: 'device',
|
||||
operator: 'is',
|
||||
name: 'device',
|
||||
value: [device],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmSource) {
|
||||
filters.push({
|
||||
id: 'utm_source',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_source',
|
||||
value: [utmSource],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmMedium) {
|
||||
filters.push({
|
||||
id: 'utm_medium',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_medium',
|
||||
value: [utmMedium],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmCampaign) {
|
||||
filters.push({
|
||||
id: 'utm_campaign',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_campaign',
|
||||
value: [utmCampaign],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmContent) {
|
||||
filters.push({
|
||||
id: 'utm_content',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_content',
|
||||
value: [utmContent],
|
||||
});
|
||||
}
|
||||
|
||||
if (utmTerm) {
|
||||
filters.push({
|
||||
id: 'utm_term',
|
||||
operator: 'is',
|
||||
name: 'properties.query.utm_term',
|
||||
value: [utmTerm],
|
||||
});
|
||||
}
|
||||
|
||||
if (country) {
|
||||
filters.push({
|
||||
id: 'country',
|
||||
operator: 'is',
|
||||
name: 'country',
|
||||
value: [country],
|
||||
});
|
||||
}
|
||||
|
||||
if (region) {
|
||||
filters.push({
|
||||
id: 'region',
|
||||
operator: 'is',
|
||||
name: 'region',
|
||||
value: [region],
|
||||
});
|
||||
}
|
||||
|
||||
if (city) {
|
||||
filters.push({
|
||||
id: 'city',
|
||||
operator: 'is',
|
||||
name: 'city',
|
||||
value: [city],
|
||||
});
|
||||
}
|
||||
|
||||
return filters;
|
||||
}, [
|
||||
referrer,
|
||||
page,
|
||||
device,
|
||||
utmSource,
|
||||
utmMedium,
|
||||
utmCampaign,
|
||||
utmContent,
|
||||
utmTerm,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
]);
|
||||
|
||||
return {
|
||||
previous,
|
||||
setPrevious,
|
||||
range,
|
||||
setRange,
|
||||
metric,
|
||||
setMetric,
|
||||
referrer,
|
||||
setReferrer,
|
||||
device,
|
||||
setDevice,
|
||||
page,
|
||||
setPage,
|
||||
|
||||
// Computed
|
||||
interval,
|
||||
filters,
|
||||
|
||||
// UTM
|
||||
utmSource,
|
||||
setUtmSource,
|
||||
utmMedium,
|
||||
setUtmMedium,
|
||||
utmCampaign,
|
||||
setUtmCampaign,
|
||||
utmContent,
|
||||
setUtmContent,
|
||||
utmTerm,
|
||||
setUtmTerm,
|
||||
|
||||
// GEO
|
||||
country,
|
||||
setCountry,
|
||||
region,
|
||||
setRegion,
|
||||
city,
|
||||
setCity,
|
||||
};
|
||||
}
|
||||
27
apps/web/src/components/overview/useOverviewWidget.tsx
Normal file
27
apps/web/src/components/overview/useOverviewWidget.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { IChartInput } from '@/types';
|
||||
import { mapKeys } from '@/utils/validation';
|
||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||
|
||||
export function useOverviewWidget<T extends string>(
|
||||
key: string,
|
||||
widgets: Record<T, { title: string; btn: string; chart: IChartInput }>
|
||||
) {
|
||||
const keys = Object.keys(widgets) as T[];
|
||||
const [widget, setWidget] = useQueryState<T>(
|
||||
key,
|
||||
parseAsStringEnum(keys)
|
||||
.withDefault(keys[0]!)
|
||||
.withOptions({ history: 'push' })
|
||||
);
|
||||
return [
|
||||
{
|
||||
...widgets[widget]!,
|
||||
key: widget,
|
||||
},
|
||||
setWidget,
|
||||
mapKeys(widgets).map((key) => ({
|
||||
...widgets[key],
|
||||
key,
|
||||
})),
|
||||
] as const;
|
||||
}
|
||||
Reference in New Issue
Block a user