Compare commits

...

7 commits

Author SHA1 Message Date
Mike Cao
03adb6b7e1 Added website check for cloud.
Some checks failed
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
Create docker images / Build, push, and deploy (push) Has been cancelled
2025-10-04 00:38:10 -07:00
Mike Cao
ed013d5d58 Fixed spacing issue in confirmation form. 2025-10-04 00:04:11 -07:00
Mike Cao
eabfec9075 Updated website edit. 2025-10-03 23:05:15 -07:00
Mike Cao
fdfa8b08f9 Added expanded view for pixels/links. Made filter form into a popover. 2025-10-03 20:25:06 -07:00
Mike Cao
8971f23e72 Should clear all parameter. 2025-10-03 18:08:01 -07:00
Mike Cao
904c313a64 Always cache prisma. Renamed WebsiteMonthSelect to MonthFilter. 2025-10-03 18:06:18 -07:00
Mike Cao
92ee44756c Refactored useDateRange to always use query string. Fixed all time filter. 2025-10-03 17:55:39 -07:00
45 changed files with 290 additions and 268 deletions

View file

@ -11,7 +11,7 @@
}, },
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "next dev -p 3001 --turbopack", "dev": "next dev -p 3001 --turbo",
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app", "build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
"start": "next start", "start": "next start",
"build-docker": "npm-run-all build-db build-tracker build-geo build-app", "build-docker": "npm-run-all build-db build-tracker build-geo build-app",

View file

@ -1,8 +1,8 @@
import { Column, Row } from '@umami/react-zen'; import { Column, Row } from '@umami/react-zen';
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { FilterBar } from '@/components/input/FilterBar'; import { FilterBar } from '@/components/input/FilterBar';
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect'; import { MonthFilter } from '@/components/input/MonthFilter';
import { ExportButton } from '@/components/input/ExportButton'; import { ExportButton } from '@/components/input/ExportButton';
export function LinkControls({ export function LinkControls({
@ -24,7 +24,7 @@ export function LinkControls({
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />} {allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} showAllTime={false} />} {allowDateFilter && <WebsiteDateFilter websiteId={websiteId} showAllTime={false} />}
{allowDownload && <ExportButton websiteId={websiteId} />} {allowDownload && <ExportButton websiteId={websiteId} />}
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />} {allowMonthFilter && <MonthFilter />}
</Row> </Row>
{allowFilter && <FilterBar websiteId={websiteId} />} {allowFilter && <FilterBar websiteId={websiteId} />}
</Column> </Column>

View file

@ -12,10 +12,9 @@ export function LinkMetricsBar({
showChange?: boolean; showChange?: boolean;
compareMode?: boolean; compareMode?: boolean;
}) { }) {
const { dateRange } = useDateRange(linkId); const { isAllTime } = useDateRange();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(linkId); const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(linkId);
const isAllTime = dateRange.value === 'all';
const { pageviews, visitors, visits, comparison } = data || {}; const { pageviews, visitors, visits, comparison } = data || {};

View file

@ -7,9 +7,30 @@ import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar'; import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar';
import { LinkControls } from '@/app/(main)/links/[linkId]/LinkControls'; import { LinkControls } from '@/app/(main)/links/[linkId]/LinkControls';
import { LinkPanels } from '@/app/(main)/links/[linkId]/LinkPanels'; import { LinkPanels } from '@/app/(main)/links/[linkId]/LinkPanels';
import { Column, Grid } from '@umami/react-zen'; import { Column, Dialog, Grid, Modal } from '@umami/react-zen';
import { WebsiteExpandedView } from '@/app/(main)/websites/[websiteId]/WebsiteExpandedView';
import { useNavigation } from '@/components/hooks';
const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
export function LinkPage({ linkId }: { linkId: string }) { export function LinkPage({ linkId }: { linkId: string }) {
const {
router,
query: { view },
updateParams,
} = useNavigation();
const handleClose = (close: () => void) => {
router.push(updateParams({ view: undefined }));
close();
};
const handleOpenChange = (isOpen: boolean) => {
if (!isOpen) {
router.push(updateParams({ view: undefined }));
}
};
return ( return (
<LinkProvider linkId={linkId}> <LinkProvider linkId={linkId}>
<Grid width="100%" height="100%"> <Grid width="100%" height="100%">
@ -23,6 +44,19 @@ export function LinkPage({ linkId }: { linkId: string }) {
</Panel> </Panel>
<LinkPanels linkId={linkId} /> <LinkPanels linkId={linkId} />
</PageBody> </PageBody>
<Modal isOpen={!!view} onOpenChange={handleOpenChange} isDismissable>
<Dialog style={{ maxWidth: 1320, width: '100vw', height: 'calc(100vh - 40px)' }}>
{({ close }) => {
return (
<WebsiteExpandedView
websiteId={linkId}
excludedIds={excludedIds}
onClose={() => handleClose(close)}
/>
);
}}
</Dialog>
</Modal>
</Column> </Column>
</Grid> </Grid>
</LinkProvider> </LinkProvider>

View file

@ -1,8 +1,8 @@
import { Column, Row } from '@umami/react-zen'; import { Column, Row } from '@umami/react-zen';
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { FilterBar } from '@/components/input/FilterBar'; import { FilterBar } from '@/components/input/FilterBar';
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect'; import { MonthFilter } from '@/components/input/MonthFilter';
import { ExportButton } from '@/components/input/ExportButton'; import { ExportButton } from '@/components/input/ExportButton';
export function PixelControls({ export function PixelControls({
@ -24,7 +24,7 @@ export function PixelControls({
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />} {allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} showAllTime={false} />} {allowDateFilter && <WebsiteDateFilter websiteId={websiteId} showAllTime={false} />}
{allowDownload && <ExportButton websiteId={websiteId} />} {allowDownload && <ExportButton websiteId={websiteId} />}
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />} {allowMonthFilter && <MonthFilter />}
</Row> </Row>
{allowFilter && <FilterBar websiteId={websiteId} />} {allowFilter && <FilterBar websiteId={websiteId} />}
</Column> </Column>

View file

@ -12,10 +12,9 @@ export function PixelMetricsBar({
showChange?: boolean; showChange?: boolean;
compareMode?: boolean; compareMode?: boolean;
}) { }) {
const { dateRange } = useDateRange(pixelId); const { isAllTime } = useDateRange();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId); const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId);
const isAllTime = dateRange.value === 'all';
const { pageviews, visitors, visits, comparison } = data || {}; const { pageviews, visitors, visits, comparison } = data || {};

View file

@ -7,9 +7,30 @@ import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar'; import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar';
import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls'; import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls';
import { PixelPanels } from '@/app/(main)/pixels/[pixelId]/PixelPanels'; import { PixelPanels } from '@/app/(main)/pixels/[pixelId]/PixelPanels';
import { Column, Grid } from '@umami/react-zen'; import { Column, Dialog, Grid, Modal } from '@umami/react-zen';
import { WebsiteExpandedView } from '@/app/(main)/websites/[websiteId]/WebsiteExpandedView';
import { useNavigation } from '@/components/hooks';
const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
export function PixelPage({ pixelId }: { pixelId: string }) { export function PixelPage({ pixelId }: { pixelId: string }) {
const {
router,
query: { view },
updateParams,
} = useNavigation();
const handleClose = (close: () => void) => {
router.push(updateParams({ view: undefined }));
close();
};
const handleOpenChange = (isOpen: boolean) => {
if (!isOpen) {
router.push(updateParams({ view: undefined }));
}
};
return ( return (
<PixelProvider pixelId={pixelId}> <PixelProvider pixelId={pixelId}>
<Grid width="100%" height="100%"> <Grid width="100%" height="100%">
@ -23,6 +44,19 @@ export function PixelPage({ pixelId }: { pixelId: string }) {
</Panel> </Panel>
<PixelPanels pixelId={pixelId} /> <PixelPanels pixelId={pixelId} />
</PageBody> </PageBody>
<Modal isOpen={!!view} onOpenChange={handleOpenChange} isDismissable>
<Dialog style={{ maxWidth: 1320, width: '100vw', height: 'calc(100vh - 40px)' }}>
{({ close }) => {
return (
<WebsiteExpandedView
websiteId={pixelId}
excludedIds={excludedIds}
onClose={() => handleClose(close)}
/>
);
}}
</Dialog>
</Modal>
</Column> </Column>
</Grid> </Grid>
</PixelProvider> </PixelProvider>

View file

@ -1,5 +1,4 @@
'use client'; 'use client';
import { Column } from '@umami/react-zen';
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider'; import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
import { WebsiteSettings } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings'; import { WebsiteSettings } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettings';
import { WebsiteSettingsHeader } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader'; import { WebsiteSettingsHeader } from '@/app/(main)/websites/[websiteId]/settings/WebsiteSettingsHeader';
@ -7,10 +6,8 @@ import { WebsiteSettingsHeader } from '@/app/(main)/websites/[websiteId]/setting
export function WebsiteSettingsPage({ websiteId }: { websiteId: string }) { export function WebsiteSettingsPage({ websiteId }: { websiteId: string }) {
return ( return (
<WebsiteProvider websiteId={websiteId}> <WebsiteProvider websiteId={websiteId}>
<Column gap="6" margin="2"> <WebsiteSettingsHeader />
<WebsiteSettingsHeader /> <WebsiteSettings websiteId={websiteId} />
<WebsiteSettings websiteId={websiteId} />
</Column>
</WebsiteProvider> </WebsiteProvider>
); );
} }

View file

@ -1,15 +1,13 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Row, Text, Icon, DataTable, DataColumn, MenuItem } from '@umami/react-zen'; import { Icon, DataTable, DataColumn } from '@umami/react-zen';
import { LinkButton } from '@/components/common/LinkButton';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import { MenuButton } from '@/components/input/MenuButton'; import { SquarePen } from '@/components/icons';
import { Eye, SquarePen } from '@/components/icons';
import { Empty } from '@/components/common/Empty'; import { Empty } from '@/components/common/Empty';
export function WebsitesTable({ export function WebsitesTable({
data = [], data = [],
showActions, showActions,
allowEdit,
allowView,
renderLink, renderLink,
}: { }: {
data: Record<string, any>[]; data: Record<string, any>[];
@ -37,28 +35,11 @@ export function WebsitesTable({
const websiteId = row.id; const websiteId = row.id;
return ( return (
<MenuButton> <LinkButton href={renderUrl(`/websites/${websiteId}/settings`)} variant="quiet">
{allowView && ( <Icon>
<MenuItem href={renderUrl(`/websites/${websiteId}`)}> <SquarePen />
<Row alignItems="center" gap> </Icon>
<Icon data-test="link-button-view"> </LinkButton>
<Eye />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Row>
</MenuItem>
)}
{allowEdit && (
<MenuItem href={renderUrl(`/websites/${websiteId}/settings`)}>
<Row alignItems="center" gap>
<Icon data-test="link-button-edit">
<SquarePen />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Row>
</MenuItem>
)}
</MenuButton>
); );
}} }}
</DataColumn> </DataColumn>

View file

@ -12,7 +12,7 @@ export function AttributionPage({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { const {
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
} = useDateRange(websiteId); } = useDateRange();
return ( return (
<Column gap="6"> <Column gap="6">

View file

@ -1,26 +1,23 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Button, Column, Box, Text, Icon, DialogTrigger, Modal, Dialog } from '@umami/react-zen'; import { Button, Column, Box, DialogTrigger, Popover, Dialog, IconLabel } from '@umami/react-zen';
import { useDateRange, useMessages } from '@/components/hooks'; import { useDateRange, useMessages } from '@/components/hooks';
import { ListCheck } from '@/components/icons'; import { ListCheck } from '@/components/icons';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { Breakdown } from './Breakdown'; import { Breakdown } from './Breakdown';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
import { FieldSelectForm } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm'; import { FieldSelectForm } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm';
import { SectionHeader } from '@/components/common/SectionHeader';
export function BreakdownPage({ websiteId }: { websiteId: string }) { export function BreakdownPage({ websiteId }: { websiteId: string }) {
const { const {
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
} = useDateRange(websiteId); } = useDateRange();
const [fields, setFields] = useState(['path']); const [fields, setFields] = useState(['path']);
return ( return (
<Column gap> <Column gap>
<WebsiteControls websiteId={websiteId} /> <WebsiteControls websiteId={websiteId} />
<SectionHeader> <FieldsButton value={fields} onChange={setFields} />
<FieldsButton value={fields} onChange={setFields} />
</SectionHeader>
<Panel height="900px" overflow="auto" allowFullscreen> <Panel height="900px" overflow="auto" allowFullscreen>
<Breakdown <Breakdown
websiteId={websiteId} websiteId={websiteId}
@ -39,19 +36,16 @@ const FieldsButton = ({ value, onChange }) => {
return ( return (
<Box> <Box>
<DialogTrigger> <DialogTrigger>
<Button variant="primary"> <Button>
<Icon> <IconLabel icon={<ListCheck />}>{formatMessage(labels.fields)}</IconLabel>
<ListCheck />
</Icon>
<Text>Fields</Text>
</Button> </Button>
<Modal> <Popover>
<Dialog title={formatMessage(labels.fields)} style={{ width: 400 }}> <Dialog title={formatMessage(labels.fields)} style={{ width: 400 }}>
{({ close }) => ( {({ close }) => (
<FieldSelectForm selectedFields={value} onChange={onChange} onClose={close} /> <FieldSelectForm selectedFields={value} onChange={onChange} onClose={close} />
)} )}
</Dialog> </Dialog>
</Modal> </Popover>
</DialogTrigger> </DialogTrigger>
</Box> </Box>
); );

View file

@ -12,7 +12,7 @@ export function FunnelsPage({ websiteId }: { websiteId: string }) {
const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'funnel' }); const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'funnel' });
const { const {
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
} = useDateRange(websiteId); } = useDateRange();
return ( return (
<Column gap> <Column gap>

View file

@ -12,7 +12,7 @@ export function GoalsPage({ websiteId }: { websiteId: string }) {
const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'goal' }); const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'goal' });
const { const {
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
} = useDateRange(websiteId); } = useDateRange();
return ( return (
<Column gap> <Column gap>

View file

@ -13,7 +13,7 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { const {
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
} = useDateRange(websiteId); } = useDateRange();
const [steps, setSteps] = useState(DEFAULT_STEP); const [steps, setSteps] = useState(DEFAULT_STEP);
const [startStep, setStartStep] = useState(''); const [startStep, setStartStep] = useState('');
const [endStep, setEndStep] = useState(''); const [endStep, setEndStep] = useState('');

View file

@ -7,7 +7,7 @@ import { useDateRange } from '@/components/hooks';
export function RevenuePage({ websiteId }: { websiteId: string }) { export function RevenuePage({ websiteId }: { websiteId: string }) {
const { const {
dateRange: { startDate, endDate, unit }, dateRange: { startDate, endDate, unit },
} = useDateRange(websiteId); } = useDateRange();
return ( return (
<Column gap> <Column gap>

View file

@ -7,7 +7,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
export function UTMPage({ websiteId }: { websiteId: string }) { export function UTMPage({ websiteId }: { websiteId: string }) {
const { const {
dateRange: { startDate, endDate }, dateRange: { startDate, endDate },
} = useDateRange(websiteId); } = useDateRange();
return ( return (
<Column gap> <Column gap>

View file

@ -11,7 +11,7 @@ export function WebsiteChart({
websiteId: string; websiteId: string;
compareMode?: boolean; compareMode?: boolean;
}) { }) {
const { dateRange, dateCompare } = useDateRange(websiteId); const { dateRange, dateCompare } = useDateRange();
const { startDate, endDate, unit, value } = dateRange; const { startDate, endDate, unit, value } = dateRange;
const { data, isLoading, isFetching, error } = useWebsitePageviewsQuery({ const { data, isLoading, isFetching, error } = useWebsitePageviewsQuery({
websiteId, websiteId,

View file

@ -1,8 +1,8 @@
import { Column, Row } from '@umami/react-zen'; import { Column, Row } from '@umami/react-zen';
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { FilterBar } from '@/components/input/FilterBar'; import { FilterBar } from '@/components/input/FilterBar';
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect'; import { MonthFilter } from '@/components/input/MonthFilter';
import { ExportButton } from '@/components/input/ExportButton'; import { ExportButton } from '@/components/input/ExportButton';
export function WebsiteControls({ export function WebsiteControls({
@ -26,7 +26,7 @@ export function WebsiteControls({
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />} {allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />} {allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />}
{allowDownload && <ExportButton websiteId={websiteId} />} {allowDownload && <ExportButton websiteId={websiteId} />}
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />} {allowMonthFilter && <MonthFilter />}
</Row> </Row>
{allowFilter && <FilterBar websiteId={websiteId} />} {allowFilter && <FilterBar websiteId={websiteId} />}
</Column> </Column>

View file

@ -26,9 +26,11 @@ import { Lightning } from '@/components/svg';
export function WebsiteExpandedView({ export function WebsiteExpandedView({
websiteId, websiteId,
excludedIds = [],
onClose, onClose,
}: { }: {
websiteId: string; websiteId: string;
excludedIds?: string[];
onClose?: () => void; onClose?: () => void;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -37,9 +39,11 @@ export function WebsiteExpandedView({
query: { view }, query: { view },
} = useNavigation(); } = useNavigation();
const filterExcluded = (item: { id: string }) => !excludedIds.includes(item.id);
const items = [ const items = [
{ {
label: formatMessage(labels.pages), label: 'URL',
items: [ items: [
{ {
id: 'path', id: 'path',
@ -71,7 +75,7 @@ export function WebsiteExpandedView({
path: updateParams({ view: 'query' }), path: updateParams({ view: 'query' }),
icon: <Search />, icon: <Search />,
}, },
], ].filter(filterExcluded),
}, },
{ {
label: formatMessage(labels.sources), label: formatMessage(labels.sources),
@ -94,7 +98,7 @@ export function WebsiteExpandedView({
path: updateParams({ view: 'domain' }), path: updateParams({ view: 'domain' }),
icon: <Globe />, icon: <Globe />,
}, },
], ].filter(filterExcluded),
}, },
{ {
label: formatMessage(labels.location), label: formatMessage(labels.location),
@ -117,7 +121,7 @@ export function WebsiteExpandedView({
path: updateParams({ view: 'city' }), path: updateParams({ view: 'city' }),
icon: <Landmark />, icon: <Landmark />,
}, },
], ].filter(filterExcluded),
}, },
{ {
label: formatMessage(labels.environment), label: formatMessage(labels.environment),
@ -152,7 +156,7 @@ export function WebsiteExpandedView({
path: updateParams({ view: 'screen' }), path: updateParams({ view: 'screen' }),
icon: <Monitor />, icon: <Monitor />,
}, },
], ].filter(filterExcluded),
}, },
{ {
label: formatMessage(labels.other), label: formatMessage(labels.other),
@ -175,7 +179,7 @@ export function WebsiteExpandedView({
path: updateParams({ view: 'tag' }), path: updateParams({ view: 'tag' }),
icon: <Tag />, icon: <Tag />,
}, },
], ].filter(filterExcluded),
}, },
]; ];

View file

@ -2,17 +2,24 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Column, Grid } from '@umami/react-zen'; import { Column, Grid } from '@umami/react-zen';
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider'; import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
import { useNavigation } from '@/components/hooks';
import { PageBody } from '@/components/common/PageBody'; import { PageBody } from '@/components/common/PageBody';
import { WebsiteHeader } from './WebsiteHeader'; import { WebsiteHeader } from './WebsiteHeader';
import { WebsiteNav } from './WebsiteNav'; import { WebsiteNav } from './WebsiteNav';
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) { export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
const { pathname } = useNavigation();
const isSettings = pathname.endsWith('/settings');
return ( return (
<WebsiteProvider websiteId={websiteId}> <WebsiteProvider websiteId={websiteId}>
<Grid columns="auto 1fr" width="100%" height="100%"> <Grid columns={isSettings ? '1fr' : 'auto 1fr'} width="100%" height="100%">
<Column height="100%" border="right" backgroundColor marginRight="2"> {!isSettings && (
<WebsiteNav websiteId={websiteId} /> <Column height="100%" border="right" backgroundColor marginRight="2">
</Column> <WebsiteNav websiteId={websiteId} />
</Column>
)}
<PageBody gap> <PageBody gap>
<WebsiteHeader /> <WebsiteHeader />
<Column>{children}</Column> <Column>{children}</Column>

View file

@ -12,10 +12,9 @@ export function WebsiteMetricsBar({
showChange?: boolean; showChange?: boolean;
compareMode?: boolean; compareMode?: boolean;
}) { }) {
const { dateRange } = useDateRange(websiteId); const { isAllTime } = useDateRange();
const { formatMessage, labels, getErrorMessage } = useMessages(); const { formatMessage, labels, getErrorMessage } = useMessages();
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId); const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId);
const isAllTime = dateRange.value === 'all';
const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {}; const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {};

View file

@ -1,9 +1,18 @@
import { Eye, User, Clock, Sheet, Tag, ChartPie, UserPlus } from '@/components/icons'; import { Text } from '@umami/react-zen';
import { Lightning, Path, Money, Compare, Target, Funnel, Magnet, Network } from '@/components/svg'; import {
Eye,
User,
Clock,
Sheet,
Tag,
ChartPie,
UserPlus,
GitCompareArrows,
} from '@/components/icons';
import { Lightning, Path, Money, Target, Funnel, Magnet, Network } from '@/components/svg';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import { SideMenu } from '@/components/common/SideMenu'; import { SideMenu } from '@/components/common/SideMenu';
import { WebsiteSelect } from '@/components/input/WebsiteSelect'; import { WebsiteSelect } from '@/components/input/WebsiteSelect';
import { Text } from '@umami/react-zen';
export function WebsiteNav({ websiteId }: { websiteId: string }) { export function WebsiteNav({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -47,7 +56,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
{ {
id: 'compare', id: 'compare',
label: formatMessage(labels.compare), label: formatMessage(labels.compare),
icon: <Compare />, icon: <GitCompareArrows />,
path: renderPath('/compare'), path: renderPath('/compare'),
}, },
{ {

View file

@ -9,7 +9,7 @@ import { useState } from 'react';
export function CompareTables({ websiteId }: { websiteId: string }) { export function CompareTables({ websiteId }: { websiteId: string }) {
const [data, setData] = useState([]); const [data, setData] = useState([]);
const { dateRange, dateCompare } = useDateRange(websiteId); const { dateRange, dateCompare } = useDateRange();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { const {
router, router,

View file

@ -1,9 +1,22 @@
import Link from 'next/link';
import { PageHeader } from '@/components/common/PageHeader'; import { PageHeader } from '@/components/common/PageHeader';
import { Globe } from '@/components/icons'; import { Globe, ArrowLeft } from '@/components/icons';
import { useWebsite } from '@/components/hooks'; import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
import { IconLabel, Row } from '@umami/react-zen';
export function WebsiteSettingsHeader() { export function WebsiteSettingsHeader() {
const website = useWebsite(); const website = useWebsite();
const { formatMessage, labels } = useMessages();
const { renderUrl } = useNavigation();
return <PageHeader title={website?.name} icon={<Globe />} />; return (
<>
<Row marginTop="6">
<Link href={renderUrl(`/websites/${website.id}`)}>
<IconLabel icon={<ArrowLeft />} label={formatMessage(labels.website)} />
</Link>
</Row>
<PageHeader title={website?.name} description={website?.domain} icon={<Globe />} />
</>
);
} }

View file

@ -1,60 +0,0 @@
import { z } from 'zod';
import { json, unauthorized } from '@/lib/response';
import { getAllUserWebsitesIncludingTeamOwner } from '@/queries/prisma/website';
import { getEventUsage } from '@/queries/sql/events/getEventUsage';
import { getEventDataUsage } from '@/queries/sql/events/getEventDataUsage';
import { parseRequest, getQueryFilters } from '@/lib/request';
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
const schema = z.object({
startAt: z.coerce.number().int(),
endAt: z.coerce.number().int(),
});
const { auth, query, error } = await parseRequest(request, schema);
if (error) {
return error();
}
if (!auth.user.isAdmin) {
return unauthorized();
}
const { userId } = await params;
const filters = await getQueryFilters(query);
const websites = await getAllUserWebsitesIncludingTeamOwner(userId);
const websiteIds = websites.map(a => a.id);
const websiteEventUsage = await getEventUsage(websiteIds, filters);
const eventDataUsage = await getEventDataUsage(websiteIds, filters);
const websiteUsage = websites.map(a => ({
websiteId: a.id,
websiteName: a.name,
websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0,
eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0,
deletedAt: a.deletedAt,
}));
const usage = websiteUsage.reduce(
(acc, cv) => {
acc.websiteEventUsage += cv.websiteEventUsage;
acc.eventDataUsage += cv.eventDataUsage;
return acc;
},
{ websiteEventUsage: 0, eventDataUsage: 0 },
);
const filteredWebsiteUsage = websiteUsage.filter(
a => !a.deletedAt && (a.websiteEventUsage > 0 || a.eventDataUsage > 0),
);
return json({
...usage,
websites: filteredWebsiteUsage,
});
}

View file

@ -4,9 +4,11 @@ import { json, unauthorized } from '@/lib/response';
import { uuid } from '@/lib/crypto'; import { uuid } from '@/lib/crypto';
import { getQueryFilters, parseRequest } from '@/lib/request'; import { getQueryFilters, parseRequest } from '@/lib/request';
import { pagingParams, searchParams } from '@/lib/schema'; import { pagingParams, searchParams } from '@/lib/schema';
import { createWebsite } from '@/queries/prisma'; import { createWebsite, getWebsiteCount } from '@/queries/prisma';
import { getAllUserWebsitesIncludingTeamOwner, getUserWebsites } from '@/queries/prisma/website'; import { getAllUserWebsitesIncludingTeamOwner, getUserWebsites } from '@/queries/prisma/website';
const CLOUD_WEBSITE_LIMIT = 3;
export async function GET(request: Request) { export async function GET(request: Request) {
const schema = z.object({ const schema = z.object({
...pagingParams, ...pagingParams,
@ -36,7 +38,7 @@ export async function POST(request: Request) {
name: z.string().max(100), name: z.string().max(100),
domain: z.string().max(500), domain: z.string().max(500),
shareId: z.string().max(50).nullable().optional(), shareId: z.string().max(50).nullable().optional(),
teamId: z.string().nullable().optional(), teamId: z.uuid().nullable().optional(),
id: z.uuid().nullable().optional(), id: z.uuid().nullable().optional(),
}); });
@ -48,6 +50,14 @@ export async function POST(request: Request) {
const { id, name, domain, shareId, teamId } = body; const { id, name, domain, shareId, teamId } = body;
if (process.env.CLOUD_MODE && !teamId && !auth.user.hasSubscription) {
const count = await getWebsiteCount(auth.user.id);
if (count >= CLOUD_WEBSITE_LIMIT) {
return unauthorized({ message: 'Website limit reached.' });
}
}
if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) { if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) {
return unauthorized(); return unauthorized();
} }

View file

@ -1,5 +1,5 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Row, Button, FormSubmitButton, Form, FormButtons } from '@umami/react-zen'; import { Box, Button, FormSubmitButton, Form, FormButtons } from '@umami/react-zen';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
export interface ConfirmationFormProps { export interface ConfirmationFormProps {
@ -25,7 +25,7 @@ export function ConfirmationForm({
return ( return (
<Form onSubmit={onConfirm} error={getErrorMessage(error)}> <Form onSubmit={onConfirm} error={getErrorMessage(error)}>
<Row marginY="4">{message}</Row> <Box marginY="4">{message}</Box>
<FormButtons> <FormButtons>
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button> <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
<FormSubmitButton <FormSubmitButton

View file

@ -24,6 +24,7 @@ export function PageHeader({
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"
paddingY="6" paddingY="6"
marginBottom="6"
border={showBorder ? 'bottom' : undefined} border={showBorder ? 'bottom' : undefined}
width="100%" width="100%"
{...props} {...props}

View file

@ -1,14 +1,13 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { import {
Text,
Heading, Heading,
NavMenu, NavMenu,
NavMenuItem, NavMenuItem,
Icon,
Row, Row,
Column, Column,
NavMenuGroup, NavMenuGroup,
NavMenuProps, NavMenuProps,
IconLabel,
} from '@umami/react-zen'; } from '@umami/react-zen';
import Link from 'next/link'; import Link from 'next/link';
@ -47,10 +46,7 @@ export function SideMenu({
return ( return (
<Link key={id} href={path}> <Link key={id} href={path}>
<NavMenuItem isSelected={isSelected}> <NavMenuItem isSelected={isSelected}>
<Row alignItems="center" gap> <IconLabel icon={icon}>{label}</IconLabel>
<Icon>{icon}</Icon>
<Text>{label}</Text>
</Row>
</NavMenuItem> </NavMenuItem>
</Link> </Link>
); );

View file

@ -77,7 +77,6 @@ export * from './useModified';
export * from './useNavigation'; export * from './useNavigation';
export * from './usePagedQuery'; export * from './usePagedQuery';
export * from './usePageParameters'; export * from './usePageParameters';
export * from './useQueryStringDate';
export * from './useRegionNames'; export * from './useRegionNames';
export * from './useSlug'; export * from './useSlug';
export * from './useSticky'; export * from './useSticky';

View file

@ -1,12 +1,23 @@
import { useApi } from '../useApi'; import { useApi } from '../useApi';
import { ReactQueryOptions } from '@/lib/types'; import { ReactQueryOptions } from '@/lib/types';
type DateRange = {
startDate?: string;
endDate?: string;
};
export function useDateRangeQuery(websiteId: string, options?: ReactQueryOptions) { export function useDateRangeQuery(websiteId: string, options?: ReactQueryOptions) {
const { get, useQuery } = useApi(); const { get, useQuery } = useApi();
return useQuery<any>({
const { data } = useQuery<DateRange>({
queryKey: ['date-range', websiteId], queryKey: ['date-range', websiteId],
queryFn: () => get(`/websites/${websiteId}/daterange`), queryFn: () => get(`/websites/${websiteId}/daterange`),
enabled: !!websiteId, enabled: !!websiteId,
...options, ...options,
}); });
return {
startDate: data?.startDate ? new Date(data.startDate) : null,
endDate: data?.endDate ? new Date(data.endDate) : null,
};
} }

View file

@ -1,10 +1,10 @@
import { useDateRange } from './useDateRange'; import { useDateRange } from './useDateRange';
import { useTimezone } from './useTimezone'; import { useTimezone } from './useTimezone';
export function useDateParameters(websiteId: string) { export function useDateParameters() {
const { const {
dateRange: { startDate, endDate, unit }, dateRange: { startDate, endDate, unit },
} = useDateRange(websiteId); } = useDateRange();
const { timezone, toUtc } = useTimezone(); const { timezone, toUtc } = useTimezone();
return { return {

View file

@ -1,35 +1,32 @@
import { getMinimumUnit, parseDateRange } from '@/lib/date'; import { useNavigation } from '@/components/hooks/useNavigation';
import { useMemo } from 'react';
import { getCompareDate, getOffsetDateRange, parseDateRange } from '@/lib/date';
import { useLocale } from '@/components/hooks/useLocale'; import { useLocale } from '@/components/hooks/useLocale';
import { useApi } from '@/components/hooks//useApi'; import { DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
import { useQueryStringDate } from '@/components/hooks/useQueryStringDate';
import { useGlobalState } from '@/components/hooks/useGlobalState';
export function useDateRange(websiteId: string) { export function useDateRange(options: { ignoreOffset?: boolean } = {}) {
const { get } = useApi(); const {
query: { date = DEFAULT_DATE_RANGE_VALUE, offset = 0, compare = 'prev', all },
} = useNavigation();
const { locale } = useLocale(); const { locale } = useLocale();
const { dateRange: defaultDateRange, dateCompare } = useQueryStringDate();
const [dateRange, setDateRange] = useGlobalState(`date-range:${websiteId}`, defaultDateRange); const dateRange = useMemo(() => {
const dateRangeObject = parseDateRange(date, locale);
const setDateRangeValue = async (value: string) => { return !options.ignoreOffset && offset
if (value === 'all') { ? getOffsetDateRange(dateRangeObject, +offset)
const result = await get(`/websites/${websiteId}/daterange`); : dateRangeObject;
const { mindate, maxdate } = result; }, [date, offset, options]);
const startDate = new Date(mindate); const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate);
const endDate = new Date(maxdate);
const unit = getMinimumUnit(startDate, endDate);
setDateRange({ return {
startDate, date,
endDate, offset,
unit, compare,
value, isAllTime: !!all,
}); isCustomRange: date.startsWith('range:'),
} else { dateRange,
setDateRange(parseDateRange(value, locale)); dateCompare,
}
}; };
return { dateRange, dateCompare, setDateRange, setDateRangeValue };
} }

View file

@ -1,24 +0,0 @@
import { useNavigation } from '@/components/hooks/useNavigation';
import { useMemo } from 'react';
import { getCompareDate, getOffsetDateRange, parseDateRange } from '@/lib/date';
import { useLocale } from '@/components/hooks/useLocale';
import { DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
export function useQueryStringDate(options: { ignoreOffset?: boolean } = {}) {
const {
query: { date = DEFAULT_DATE_RANGE_VALUE, offset = 0, compare = 'prev' },
} = useNavigation();
const { locale } = useLocale();
const dateRange = useMemo(() => {
const dateRangeObject = parseDateRange(date, locale);
return !options.ignoreOffset && offset
? getOffsetDateRange(dateRangeObject, +offset)
: dateRangeObject;
}, [date, offset, options]);
const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate);
return { date, offset, dateRange, dateCompare };
}

View file

@ -0,0 +1,18 @@
import { useDateRange, useNavigation } from '@/components/hooks';
import { getMonthDateRangeValue } from '@/lib/date';
import { MonthSelect } from './MonthSelect';
export function MonthFilter() {
const { router, updateParams } = useNavigation();
const {
dateRange: { startDate },
} = useDateRange();
const handleMonthSelect = (date: Date) => {
const range = getMonthDateRangeValue(date);
router.push(updateParams({ date: range, offset: undefined }));
};
return <MonthSelect date={startDate} onChange={handleMonthSelect} />;
}

View file

@ -1,7 +1,7 @@
import { LoadingButton, Icon, Tooltip, TooltipTrigger } from '@umami/react-zen'; import { LoadingButton, Icon, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { setWebsiteDateRange } from '@/store/websites'; import { setWebsiteDateRange } from '@/store/websites';
import { useDateRange } from '@/components/hooks'; import { useDateRange } from '@/components/hooks';
import { Refresh } from '@/components/icons'; import { RefreshCw } from '@/components/icons';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
export function RefreshButton({ export function RefreshButton({
@ -12,7 +12,7 @@ export function RefreshButton({
isLoading?: boolean; isLoading?: boolean;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { dateRange } = useDateRange(websiteId); const { dateRange } = useDateRange();
function handleClick() { function handleClick() {
if (!isLoading && dateRange) { if (!isLoading && dateRange) {
@ -24,7 +24,7 @@ export function RefreshButton({
<TooltipTrigger> <TooltipTrigger>
<LoadingButton isLoading={isLoading} onPress={handleClick}> <LoadingButton isLoading={isLoading} onPress={handleClick}>
<Icon> <Icon>
<Refresh /> <RefreshCw />
</Icon> </Icon>
</LoadingButton> </LoadingButton>
<Tooltip>{formatMessage(labels.refresh)}</Tooltip> <Tooltip>{formatMessage(labels.refresh)}</Tooltip>

View file

@ -1,7 +1,8 @@
import { List, ListItem } from '@umami/react-zen'; import { IconLabel, List, ListItem } from '@umami/react-zen';
import { useWebsiteSegmentsQuery } from '@/components/hooks'; import { useWebsiteSegmentsQuery } from '@/components/hooks';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Empty } from '@/components/common/Empty'; import { Empty } from '@/components/common/Empty';
import { ChartPie, UserPlus } from '@/components/icons';
export interface SegmentFiltersProps { export interface SegmentFiltersProps {
websiteId: string; websiteId: string;
@ -29,7 +30,9 @@ export function SegmentFilters({
{data?.data?.map(item => { {data?.data?.map(item => {
return ( return (
<ListItem key={item.id} id={item.id}> <ListItem key={item.id} id={item.id}>
{item.name} <IconLabel icon={type === 'segment' ? <ChartPie /> : <UserPlus />}>
{item.name}
</IconLabel>
</ListItem> </ListItem>
); );
})} })}

View file

@ -1,10 +1,10 @@
import { useCallback, useMemo } from 'react';
import { Button, Icon, Row, Text, Select, ListItem } from '@umami/react-zen'; import { Button, Icon, Row, Text, Select, ListItem } from '@umami/react-zen';
import { isAfter } from 'date-fns'; import { isAfter } from 'date-fns';
import { ChevronRight } from '@/components/icons'; import { ChevronRight } from '@/components/icons';
import { useDateRange, useMessages, useNavigation } from '@/components/hooks'; import { useDateRange, useDateRangeQuery, useMessages, useNavigation } from '@/components/hooks';
import { getDateRangeValue } from '@/lib/date';
import { DateFilter } from './DateFilter'; import { DateFilter } from './DateFilter';
import { getOffsetDateRange } from '@/lib/date';
import { useCallback } from 'react';
export interface WebsiteDateFilterProps { export interface WebsiteDateFilterProps {
websiteId: string; websiteId: string;
@ -20,30 +20,33 @@ export function WebsiteDateFilter({
showButtons = true, showButtons = true,
allowCompare, allowCompare,
}: WebsiteDateFilterProps) { }: WebsiteDateFilterProps) {
const { dateRange, setDateRange, setDateRangeValue } = useDateRange(websiteId); const { dateRange, isAllTime, isCustomRange } = useDateRange();
const { value, endDate } = dateRange;
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { const {
router, router,
updateParams, updateParams,
query: { compare = 'prev', offset = 0 }, query: { compare = 'prev', offset = 0 },
} = useNavigation(); } = useNavigation();
const isAllTime = value === 'all'; const disableForward = isAllTime || isAfter(dateRange.endDate, new Date());
const isCustomRange = value.startsWith('range'); const websiteDateRange = useDateRangeQuery(websiteId);
const disableForward = value === 'all' || isAfter(endDate, new Date());
const handleChange = (date: string) => { const handleChange = (date: string) => {
setDateRangeValue(date); if (date === 'all') {
router.push(updateParams({ date, offset: undefined })); router.push(
updateParams({
date: getDateRangeValue(websiteDateRange.startDate, websiteDateRange.endDate),
offset: undefined,
all: 1,
}),
);
} else {
router.push(updateParams({ date, offset: undefined, all: undefined }));
}
}; };
const handleIncrement = useCallback( const handleIncrement = useCallback(
(increment: number) => { (increment: number) => {
const offsetDate = getOffsetDateRange(dateRange, +offset + increment);
setDateRange(offsetDate);
router.push(updateParams({ offset: +offset + increment })); router.push(updateParams({ offset: +offset + increment }));
}, },
[offset], [offset],
@ -53,6 +56,12 @@ export function WebsiteDateFilter({
router.push(updateParams({ compare })); router.push(updateParams({ compare }));
}; };
const dateValue = useMemo(() => {
return offset !== 0
? getDateRangeValue(dateRange.startDate, dateRange.endDate)
: dateRange.value;
}, [dateRange]);
return ( return (
<Row gap> <Row gap>
{showButtons && !isAllTime && !isCustomRange && ( {showButtons && !isAllTime && !isCustomRange && (
@ -71,7 +80,7 @@ export function WebsiteDateFilter({
)} )}
<Row minWidth="200px"> <Row minWidth="200px">
<DateFilter <DateFilter
value={value} value={dateValue}
onChange={handleChange} onChange={handleChange}
showAllTime={showAllTime} showAllTime={showAllTime}
renderDate={+offset !== 0} renderDate={+offset !== 0}

View file

@ -1,4 +1,4 @@
import { Button, Icon, DialogTrigger, Dialog, Modal, Text } from '@umami/react-zen'; import { Button, Icon, DialogTrigger, Dialog, Popover, Text } from '@umami/react-zen';
import { ListFilter } from '@/components/icons'; import { ListFilter } from '@/components/icons';
import { FilterEditForm } from '@/components/input/FilterEditForm'; import { FilterEditForm } from '@/components/input/FilterEditForm';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
@ -32,13 +32,13 @@ export function WebsiteFilterButton({
</Icon> </Icon>
{showText && <Text>{formatMessage(labels.filter)}</Text>} {showText && <Text>{formatMessage(labels.filter)}</Text>}
</Button> </Button>
<Modal> <Popover placement="bottom start">
<Dialog title={formatMessage(labels.filters)} style={{ width: 800, minHeight: 600 }}> <Dialog title={formatMessage(labels.filters)} style={{ width: 800, minHeight: 600 }}>
{({ close }) => { {({ close }) => {
return <FilterEditForm websiteId={websiteId} onChange={handleChange} onClose={close} />; return <FilterEditForm websiteId={websiteId} onChange={handleChange} onClose={close} />;
}} }}
</Dialog> </Dialog>
</Modal> </Popover>
</DialogTrigger> </DialogTrigger>
); );
} }

View file

@ -1,17 +0,0 @@
import { useDateRange } from '@/components/hooks';
import { dateToRangeValue } from '@/lib/date';
import { MonthSelect } from './MonthSelect';
export function WebsiteMonthSelect({ websiteId }: { websiteId: string }) {
const {
dateRange: { startDate },
saveDateRange,
} = useDateRange(websiteId);
const handleMonthSelect = (date: Date) => {
const range = dateToRangeValue(date);
saveDateRange(range);
};
return <MonthSelect date={startDate} onChange={handleMonthSelect} />;
}

View file

@ -14,7 +14,7 @@ export interface EventsChartProps extends BarChartProps {
export function EventsChart({ websiteId, focusLabel }: EventsChartProps) { export function EventsChart({ websiteId, focusLabel }: EventsChartProps) {
const { const {
dateRange: { startDate, endDate, unit }, dateRange: { startDate, endDate, unit },
} = useDateRange(websiteId); } = useDateRange();
const { locale } = useLocale(); const { locale } = useLocale();
const { data, isLoading, error } = useWebsiteEventsSeriesQuery(websiteId); const { data, isLoading, error } = useWebsiteEventsSeriesQuery(websiteId);
const [label, setLabel] = useState<string>(focusLabel); const [label, setLabel] = useState<string>(focusLabel);

View file

@ -131,14 +131,6 @@ export function parseDateRange(value: string, locale = 'en-US'): DateRange {
return null; return null;
} }
if (value === 'all') {
return {
startDate: new Date(0),
endDate: new Date(1),
value,
};
}
if (value.startsWith('range')) { if (value.startsWith('range')) {
const [, startTime, endTime] = value.split(':'); const [, startTime, endTime] = value.split(':');
@ -225,24 +217,28 @@ export function getOffsetDateRange(dateRange: DateRange, offset: number) {
case 'day': case 'day':
return { return {
...dateRange, ...dateRange,
offset,
startDate: addDays(startDate, change), startDate: addDays(startDate, change),
endDate: addDays(endDate, change), endDate: addDays(endDate, change),
}; };
case 'week': case 'week':
return { return {
...dateRange, ...dateRange,
offset,
startDate: addWeeks(startDate, change), startDate: addWeeks(startDate, change),
endDate: addWeeks(endDate, change), endDate: addWeeks(endDate, change),
}; };
case 'month': case 'month':
return { return {
...dateRange, ...dateRange,
offset,
startDate: addMonths(startDate, change), startDate: addMonths(startDate, change),
endDate: addMonths(endDate, change), endDate: addMonths(endDate, change),
}; };
case 'year': case 'year':
return { return {
...dateRange, ...dateRange,
offset,
startDate: addYears(startDate, change), startDate: addYears(startDate, change),
endDate: addYears(endDate, change), endDate: addYears(endDate, change),
}; };
@ -250,6 +246,7 @@ export function getOffsetDateRange(dateRange: DateRange, offset: number) {
return { return {
startDate: add(startDate, change), startDate: add(startDate, change),
endDate: add(endDate, change), endDate: add(endDate, change),
offset,
value, value,
unit, unit,
num, num,
@ -354,6 +351,10 @@ export function generateTimeSeries(
}); });
} }
export function dateToRangeValue(date: Date) { export function getDateRangeValue(startDate: Date, endDate: Date) {
return `range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`; return `range:${startDate.getTime()}:${endDate.getTime()}`;
}
export function getMonthDateRangeValue(date: Date) {
return getDateRangeValue(startOfMonth(date), endOfMonth(date));
} }

View file

@ -284,7 +284,7 @@ function getClient() {
replicaUrl: process.env.DATABASE_REPLICA_URL, replicaUrl: process.env.DATABASE_REPLICA_URL,
}); });
if (process.env.NODE_ENV !== 'production') { if (!globalThis[PRISMA]) {
globalThis[PRISMA] = prisma.client; globalThis[PRISMA] = prisma.client;
} }

View file

@ -203,3 +203,11 @@ export async function deleteWebsite(websiteId: string) {
return data; return data;
}); });
} }
export async function getWebsiteCount(userId: string) {
return prisma.client.website.count({
where: {
userId,
},
});
}

View file

@ -20,8 +20,8 @@ async function relationalQuery(websiteId: string) {
const result = await rawQuery( const result = await rawQuery(
` `
select select
min(created_at) as mindate, min(created_at) as startDate,
max(created_at) as maxdate max(created_at) as endDate
from website_event from website_event
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and created_at >= {{startDate}} and created_at >= {{startDate}}
@ -42,8 +42,8 @@ async function clickhouseQuery(websiteId: string) {
const result = await rawQuery( const result = await rawQuery(
` `
select select
min(created_at) as mindate, min(created_at) as startDate,
max(created_at) as maxdate max(created_at) as endDate
from website_event_stats_hourly from website_event_stats_hourly
where website_id = {websiteId:UUID} where website_id = {websiteId:UUID}
and created_at >= {startDate:DateTime64} and created_at >= {startDate:DateTime64}