Updated date range handling.

This commit is contained in:
Mike Cao 2025-06-25 14:27:17 -07:00
parent 6d1603fa28
commit 5ca51b3e8f
19 changed files with 101 additions and 99 deletions

View file

@ -80,7 +80,7 @@
"@react-spring/web": "^9.7.3",
"@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.80.10",
"@umami/react-zen": "^0.139.0",
"@umami/react-zen": "^0.142.0",
"@umami/redis-client": "^0.27.0",
"bcryptjs": "^2.4.3",
"chalk": "^4.1.1",

10
pnpm-lock.yaml generated
View file

@ -45,8 +45,8 @@ importers:
specifier: ^5.80.10
version: 5.80.10(react@19.1.0)
'@umami/react-zen':
specifier: ^0.139.0
version: 0.139.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
specifier: ^0.142.0
version: 0.142.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))
'@umami/redis-client':
specifier: ^0.27.0
version: 0.27.0
@ -2549,8 +2549,8 @@ packages:
resolution: {integrity: sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@umami/react-zen@0.139.0':
resolution: {integrity: sha512-NRf27+05z78DLFxK3aQUBfhZW7covl6qtS4OcaBUbZ71VZ7eeRVg7SU7Cn3NvkXlcI16t6bbLXGW4HjvfBhXsw==}
'@umami/react-zen@0.142.0':
resolution: {integrity: sha512-xD0O96c1AsztIbD8DZOszBeXAmEhVJ1isqsms+Nu/Kzf4vkWhEPpu7dbhntroHiA7JLK/uI1bgYHQMJFMM1f4w==}
'@umami/redis-client@0.27.0':
resolution: {integrity: sha512-SbHTpxhgeZyTBUSp2zdZM+XUtpsaSL4Tad8QXIEhEtjWhvvfoornyT5kLuyYCVtzSAT4daALeGmOO1z6EE1KcA==}
@ -9733,7 +9733,7 @@ snapshots:
'@typescript-eslint/types': 8.34.1
eslint-visitor-keys: 4.2.1
'@umami/react-zen@0.139.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
'@umami/react-zen@0.142.0(@babel/core@7.27.1)(@types/react@19.1.8)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@9.0.21)(use-sync-external-store@1.5.0(react@19.1.0))':
dependencies:
'@fontsource/jetbrains-mono': 5.2.6
'@internationalized/date': 3.8.2

View file

@ -19,32 +19,32 @@ export function SideNav(props: any) {
const links = [
{
label: formatMessage(labels.websites),
href: renderUrl('/websites'),
href: '/websites',
icon: <Globe />,
},
{
label: formatMessage(labels.boards),
href: renderUrl('/boards'),
href: '/boards',
icon: <LayoutDashboard />,
},
{
label: formatMessage(labels.links),
href: renderUrl('/links'),
href: '/links',
icon: <LinkIcon />,
},
{
label: formatMessage(labels.pixels),
href: renderUrl('/pixels'),
href: '/pixels',
icon: <Grid2X2 />,
},
{
label: formatMessage(labels.settings),
href: renderUrl('/settings'),
href: '/settings',
icon: <Settings />,
},
{
label: formatMessage(labels.admin),
href: renderUrl('/admin'),
href: '/admin',
icon: <LockKeyhole />,
},
].filter(n => n);
@ -57,7 +57,7 @@ export function SideNav(props: any) {
<SidebarSection>
{links.map(({ href, label, icon }) => {
return (
<Link key={href} href={href} role="button">
<Link key={href} href={renderUrl(href, false)} role="button">
<SidebarItem label={label} icon={icon} isSelected={pathname.startsWith(href)} />
</Link>
);

View file

@ -1,16 +1,15 @@
import { DateFilter } from '@/components/input/DateFilter';
import { Button, Row } from '@umami/react-zen';
import { useDateRange, useMessages } from '@/components/hooks';
import { DEFAULT_DATE_RANGE } from '@/lib/constants';
import { DateRange } from '@/lib/types';
import { DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
export function DateRangeSetting() {
const { formatMessage, labels } = useMessages();
const { dateRange, saveDateRange } = useDateRange();
const { value } = dateRange;
const handleChange = (value: string | DateRange) => saveDateRange(value);
const handleReset = () => saveDateRange(DEFAULT_DATE_RANGE);
const handleChange = (value: string) => saveDateRange(value);
const handleReset = () => saveDateRange(DEFAULT_DATE_RANGE_VALUE);
return (
<Row gap="3">

View file

@ -33,7 +33,7 @@ export function ProfileSettings() {
};
return (
<Column gap="6">
<Column width="400px" gap="6">
<Column>
<Label>{formatMessage(labels.username)}</Label>
{username}

View file

@ -5,7 +5,7 @@ import { useMessages } from '@/components/hooks';
import { Globe, Arrow } from '@/components/icons';
import { SectionHeader } from '@/components/common/SectionHeader';
import { WebsiteShareForm } from './WebsiteShareForm';
import { TrackingCode } from './TrackingCode';
import { WebsiteTrackingCode } from './WebsiteTrackingCode';
import { WebsiteData } from './WebsiteData';
import { WebsiteEditForm } from './WebsiteEditForm';
import { LinkButton } from '@/components/common/LinkButton';
@ -23,11 +23,7 @@ export function WebsiteSettings({
return (
<>
<SectionHeader title={website?.name} icon={<Globe />}>
<LinkButton
variant="primary"
href={`/websites/${websiteId}`}
target={openExternal ? '_blank' : null}
>
<LinkButton href={`/websites/${websiteId}`} target={openExternal ? '_blank' : null}>
<Icon>
<Arrow />
</Icon>
@ -45,10 +41,10 @@ export function WebsiteSettings({
<WebsiteEditForm websiteId={websiteId} />
</TabPanel>
<TabPanel id="tracking">
<TrackingCode websiteId={websiteId} />
<WebsiteTrackingCode websiteId={websiteId} />
</TabPanel>
<TabPanel id="share">
<WebsiteShareForm />
<WebsiteShareForm websiteId={websiteId} />
</TabPanel>
<TabPanel id="data">
<WebsiteData websiteId={websiteId} />

View file

@ -22,7 +22,7 @@ const generateId = () => getRandomChars(16);
export interface WebsiteShareFormProps {
websiteId: string;
shareId: string;
shareId?: string;
onSave?: () => void;
onClose?: () => void;
}

View file

@ -1,9 +1,15 @@
import { TextField } from '@umami/react-zen';
import { TextField, Text, Column } from '@umami/react-zen';
import { useMessages, useConfig } from '@/components/hooks';
const SCRIPT_NAME = 'script.js';
export function TrackingCode({ websiteId, hostUrl }: { websiteId: string; hostUrl?: string }) {
export function WebsiteTrackingCode({
websiteId,
hostUrl,
}: {
websiteId: string;
hostUrl?: string;
}) {
const { formatMessage, messages } = useMessages();
const config = useConfig();
@ -19,9 +25,9 @@ export function TrackingCode({ websiteId, hostUrl }: { websiteId: string; hostUr
const code = `<script defer src="${url}" data-website-id="${websiteId}"></script>`;
return (
<>
<p>{formatMessage(messages.trackingCode)}</p>
<TextField value={code} isReadOnly allowCopy asTextArea />
</>
<Column gap>
<Text>{formatMessage(messages.trackingCode)}</Text>
<TextField value={code} isReadOnly allowCopy asTextArea resize="none" />
</Column>
);
}

View file

@ -5,22 +5,24 @@ import { Share, Edit } from '@/components/icons';
import { Favicon } from '@/components/common/Favicon';
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
import { WebsiteShareForm } from '@/app/(main)/settings/websites/[websiteId]/WebsiteShareForm';
import { useMessages } from '@/components/hooks';
import { useMessages, useNavigation } from '@/components/hooks';
import { LinkButton } from '@/components/common/LinkButton';
export function WebsiteHeader() {
const website = useWebsite();
const { renderUrl } = useNavigation();
return (
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} showBorder={false}>
<Row alignItems="center" gap>
<ActiveUsers websiteId={website.id} />
<ShareButton websiteId={website.id} shareId={website.shareId} />
<Button>
<LinkButton href={renderUrl(`/settings/websites/${website.id}`)}>
<Icon>
<Edit />
</Icon>
<Text>Edit</Text>
</Button>
</LinkButton>
</Row>
</PageHeader>
);

View file

@ -1,55 +1,53 @@
import { getMinimumUnit, parseDateRange } from '@/lib/date';
import { getMinimumUnit, parseDateRange, getOffsetDateRange } from '@/lib/date';
import { setItem } from '@/lib/storage';
import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE } from '@/lib/constants';
import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
import { setWebsiteDateCompare, setWebsiteDateRange, useWebsites } from '@/store/websites';
import { setDateRange, useApp } from '@/store/app';
import { DateRange } from '@/lib/types';
import { setDateRangeValue, useApp } from '@/store/app';
import { useLocale } from './useLocale';
import { useApi } from './useApi';
import { useNavigation } from './useNavigation';
import { useMemo } from 'react';
export function useDateRange(websiteId?: string) {
const { get } = useApi();
const { locale } = useLocale();
const {
query: { date },
query: { date, offset = 0 },
} = useNavigation();
const websiteConfig = useWebsites(state => state[websiteId]?.dateRange);
const globalConfig = useApp(state => state.dateRange);
const dateRange = parseDateRange(
date || websiteConfig || globalConfig || DEFAULT_DATE_RANGE,
const globalConfig = useApp(state => state.dateRangeValue);
const dateRangeObject = parseDateRange(
date || websiteConfig?.value || globalConfig || DEFAULT_DATE_RANGE_VALUE,
locale,
);
const dateRange = useMemo(
() => (offset ? getOffsetDateRange(dateRangeObject, +offset) : dateRangeObject),
[date, offset],
);
const dateCompare = useWebsites(state => state[websiteId]?.dateCompare || DEFAULT_DATE_COMPARE);
const saveDateRange = async (value: DateRange | string) => {
const saveDateRange = async (value: string) => {
if (websiteId) {
let dateRange: DateRange | string = value;
if (value === 'all') {
const result: any = await get(`/websites/${websiteId}/daterange`);
const { mindate, maxdate } = result;
if (typeof value === 'string') {
if (value === 'all') {
const result: any = await get(`/websites/${websiteId}/daterange`);
const { mindate, maxdate } = result;
const startDate = new Date(mindate);
const endDate = new Date(maxdate);
const unit = getMinimumUnit(startDate, endDate);
const startDate = new Date(mindate);
const endDate = new Date(maxdate);
const unit = getMinimumUnit(startDate, endDate);
dateRange = {
startDate,
endDate,
unit,
value,
};
} else {
dateRange = parseDateRange(value, locale);
}
setWebsiteDateRange(websiteId, {
startDate,
endDate,
unit,
value,
});
} else {
setWebsiteDateRange(websiteId, parseDateRange(value, locale));
}
setWebsiteDateRange(websiteId, dateRange as DateRange);
} else {
setItem(DATE_RANGE_CONFIG, value);
setDateRange(value);
setDateRangeValue(value);
}
};

View file

@ -11,6 +11,7 @@ export interface DateFilterProps {
endDate: Date;
onChange?: (value: string) => void;
showAllTime?: boolean;
renderDate?: boolean;
}
export function DateFilter({
@ -18,7 +19,8 @@ export function DateFilter({
startDate,
endDate,
onChange,
showAllTime = false,
showAllTime,
renderDate,
}: DateFilterProps) {
const { formatMessage, labels } = useMessages();
const [showPicker, setShowPicker] = useState(false);
@ -89,7 +91,7 @@ export function DateFilter({
};
const renderValue = ({ defaultChildren }) => {
return value?.startsWith('range') ? (
return value?.startsWith('range') || renderDate ? (
<DateDisplay startDate={startDate} endDate={endDate} />
) : (
defaultChildren

View file

@ -11,7 +11,6 @@ import {
import { isAfter } from 'date-fns';
import { Chevron, Close, Compare } from '@/components/icons';
import { useDateRange, useMessages, useNavigation } from '@/components/hooks';
import { getOffsetDateRange } from '@/lib/date';
import { DateFilter } from './DateFilter';
export function WebsiteDateFilter({
@ -26,13 +25,13 @@ export function WebsiteDateFilter({
showButtons?: boolean;
allowCompare?: boolean;
}) {
const { dateRange, saveDateRange } = useDateRange(websiteId);
const { value, startDate, endDate, offset } = dateRange;
const { dateRange } = useDateRange(websiteId);
const { value, startDate, endDate } = dateRange;
const { formatMessage, labels } = useMessages();
const {
router,
updateParams,
query: { compare },
query: { compare, offset = 0 },
} = useNavigation();
const isAllTime = value === 'all';
const isCustomRange = value.startsWith('range');
@ -40,13 +39,11 @@ export function WebsiteDateFilter({
const disableForward = value === 'all' || isAfter(endDate, new Date());
const handleChange = (date: string) => {
router.push(updateParams({ date }));
saveDateRange(date);
router.push(updateParams({ date, offset: undefined }));
};
const handleIncrement = (increment: number) => {
router.push(updateParams({ offset: offset + increment }));
saveDateRange(getOffsetDateRange(dateRange, increment));
router.push(updateParams({ offset: +offset + increment }));
};
const handleSelect = (compare: any) => {
@ -79,6 +76,7 @@ export function WebsiteDateFilter({
endDate={endDate}
onChange={handleChange}
showAllTime={showAllTime}
renderDate={+offset !== 0}
/>
{!isAllTime && compare && (
<Row alignItems="center" gap>

View file

@ -330,6 +330,7 @@ export const labels = defineMessages({
conversion: { id: 'label.conversion', defaultMessage: 'Conversion' },
firstClick: { id: 'label.first-click', defaultMessage: 'First click' },
lastClick: { id: 'label.last-click', defaultMessage: 'Last click' },
online: { id: 'label.online', defaultMessage: 'Online' },
});
export const messages = defineMessages({

View file

@ -11,7 +11,7 @@ export function ActiveUsers({
value?: number;
refetchInterval?: number;
}) {
const { formatMessage, messages } = useMessages();
const { formatMessage, labels } = useMessages();
const { data } = useActyiveUsersQuery(websiteId, { refetchInterval });
const count = useMemo(() => {
@ -28,8 +28,8 @@ export function ActiveUsers({
return (
<StatusLight variant="success">
<Text size="2" weight="bold">
{formatMessage(messages.numberOfUsers, { x: count })}
<Text size="2" weight="medium">
{count} {formatMessage(labels.online)}
</Text>
</StatusLight>
);

View file

@ -27,7 +27,7 @@ export * from '@/app/(main)/settings/teams/TeamsTable';
export * from '@/app/(main)/settings/teams/WebsiteTags';
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteShareForm';
export * from '@/app/(main)/settings/websites/[websiteId]/TrackingCode';
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteTrackingCode';
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteData';
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteDeleteForm';
export * from '@/app/(main)/settings/websites/[websiteId]/WebsiteEditForm';

View file

@ -16,7 +16,7 @@ export const FAVICON_URL = 'https://icons.duckduckgo.com/ip3/{{domain}}.ico';
export const DEFAULT_LOCALE = process.env.defaultLocale || 'en-US';
export const DEFAULT_THEME = 'light';
export const DEFAULT_ANIMATION_DURATION = 300;
export const DEFAULT_DATE_RANGE = '24hour';
export const DEFAULT_DATE_RANGE_VALUE = '24hour';
export const DEFAULT_WEBSITE_LIMIT = 10;
export const DEFAULT_RESET_DATE = '2000-01-01';
export const DEFAULT_PAGE_SIZE = 10;

View file

@ -126,9 +126,9 @@ export function parseDateValue(value: string) {
return { num: +num, unit };
}
export function parseDateRange(value: string | object, locale = 'en-US'): DateRange {
export function parseDateRange(value: string, locale = 'en-US'): DateRange {
if (typeof value !== 'string') {
return value as DateRange;
return null;
}
if (value === 'all') {
@ -151,14 +151,13 @@ export function parseDateRange(value: string | object, locale = 'en-US'): DateRa
endDate,
value,
...parseDateValue(value),
offset: 0,
unit,
};
}
const now = new Date();
const dateLocale = getDateLocale(locale);
const { num, unit } = parseDateValue(value);
const { num = 1, unit } = parseDateValue(value);
switch (unit) {
case 'hour':
@ -211,10 +210,14 @@ export function parseDateRange(value: string | object, locale = 'en-US'): DateRa
}
}
export function getOffsetDateRange(dateRange: DateRange, increment: number) {
const { startDate, endDate, unit, num, offset, value } = dateRange;
export function getOffsetDateRange(dateRange: DateRange, offset: number) {
if (offset === 0) {
return dateRange;
}
const change = num * increment;
const { startDate, endDate, unit, num, value } = dateRange;
const change = num * offset;
const { add } = DATE_FUNCTIONS[unit];
const { unit: originalUnit } = parseDateValue(value) || {};
@ -224,28 +227,24 @@ export function getOffsetDateRange(dateRange: DateRange, increment: number) {
...dateRange,
startDate: addDays(startDate, change),
endDate: addDays(endDate, change),
offset: offset + increment,
};
case 'week':
return {
...dateRange,
startDate: addWeeks(startDate, change),
endDate: addWeeks(endDate, change),
offset: offset + increment,
};
case 'month':
return {
...dateRange,
startDate: addMonths(startDate, change),
endDate: addMonths(endDate, change),
offset: offset + increment,
};
case 'year':
return {
...dateRange,
startDate: addYears(startDate, change),
endDate: addYears(endDate, change),
offset: offset + increment,
};
default:
return {
@ -254,7 +253,6 @@ export function getOffsetDateRange(dateRange: DateRange, increment: number) {
value,
unit,
num,
offset: offset + increment,
};
}
}

View file

@ -12,6 +12,7 @@ export async function getActiveVisitors(...args: [websiteId: string]) {
async function relationalQuery(websiteId: string) {
const { rawQuery } = prisma;
const startDate = subMinutes(new Date(), 5);
const result = await rawQuery(
`
@ -20,7 +21,7 @@ async function relationalQuery(websiteId: string) {
where website_id = {{websiteId::uuid}}
and created_at >= {{startDate}}
`,
{ websiteId, startDate: subMinutes(new Date(), 5) },
{ websiteId, startDate },
);
return result[0] ?? null;
@ -28,6 +29,7 @@ async function relationalQuery(websiteId: string) {
async function clickhouseQuery(websiteId: string): Promise<{ x: number }> {
const { rawQuery } = clickhouse;
const startDate = subMinutes(new Date(), 5);
const result = await rawQuery(
`
@ -37,7 +39,7 @@ async function clickhouseQuery(websiteId: string): Promise<{ x: number }> {
where website_id = {websiteId:UUID}
and created_at >= {startDate:DateTime64}
`,
{ websiteId, startDate: subMinutes(new Date(), 5) },
{ websiteId, startDate },
);
return result[0] ?? null;

View file

@ -1,7 +1,7 @@
import { create } from 'zustand';
import {
DATE_RANGE_CONFIG,
DEFAULT_DATE_RANGE,
DEFAULT_DATE_RANGE_VALUE,
DEFAULT_LOCALE,
DEFAULT_THEME,
LOCALE_CONFIG,
@ -23,7 +23,7 @@ const initialState = {
locale: getItem(LOCALE_CONFIG) || DEFAULT_LOCALE,
theme: getItem(THEME_CONFIG) || getDefaultTheme() || DEFAULT_THEME,
timezone: getItem(TIMEZONE_CONFIG) || getTimezone(),
dateRange: getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE,
dateRangeValue: getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
shareToken: null,
user: null,
config: null,
@ -51,8 +51,8 @@ export function setConfig(config: object) {
store.setState({ config });
}
export function setDateRange(dateRange: string | object) {
store.setState({ dateRange });
export function setDateRangeValue(dateRangeValue: string) {
store.setState({ dateRangeValue });
}
export const useApp = store;