diff --git a/package.json b/package.json index 71960f91..afc08e6b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff4f606a..19596963 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx index 879b9935..c9bc87af 100644 --- a/src/app/(main)/SideNav.tsx +++ b/src/app/(main)/SideNav.tsx @@ -19,32 +19,32 @@ export function SideNav(props: any) { const links = [ { label: formatMessage(labels.websites), - href: renderUrl('/websites'), + href: '/websites', icon: , }, { label: formatMessage(labels.boards), - href: renderUrl('/boards'), + href: '/boards', icon: , }, { label: formatMessage(labels.links), - href: renderUrl('/links'), + href: '/links', icon: , }, { label: formatMessage(labels.pixels), - href: renderUrl('/pixels'), + href: '/pixels', icon: , }, { label: formatMessage(labels.settings), - href: renderUrl('/settings'), + href: '/settings', icon: , }, { label: formatMessage(labels.admin), - href: renderUrl('/admin'), + href: '/admin', icon: , }, ].filter(n => n); @@ -57,7 +57,7 @@ export function SideNav(props: any) { {links.map(({ href, label, icon }) => { return ( - + ); diff --git a/src/app/(main)/settings/profile/DateRangeSetting.tsx b/src/app/(main)/settings/profile/DateRangeSetting.tsx index 3fa8f149..8a957a22 100644 --- a/src/app/(main)/settings/profile/DateRangeSetting.tsx +++ b/src/app/(main)/settings/profile/DateRangeSetting.tsx @@ -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 ( diff --git a/src/app/(main)/settings/profile/ProfileSettings.tsx b/src/app/(main)/settings/profile/ProfileSettings.tsx index 9f8a0e13..40471c75 100644 --- a/src/app/(main)/settings/profile/ProfileSettings.tsx +++ b/src/app/(main)/settings/profile/ProfileSettings.tsx @@ -33,7 +33,7 @@ export function ProfileSettings() { }; return ( - + {username} diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx index 6a0b164e..a7d19745 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx @@ -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 ( <> }> - + @@ -45,10 +41,10 @@ export function WebsiteSettings({ - + - + diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteShareForm.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteShareForm.tsx index 60c71e22..6a9bbbb7 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteShareForm.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteShareForm.tsx @@ -22,7 +22,7 @@ const generateId = () => getRandomChars(16); export interface WebsiteShareFormProps { websiteId: string; - shareId: string; + shareId?: string; onSave?: () => void; onClose?: () => void; } diff --git a/src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteTrackingCode.tsx similarity index 63% rename from src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx rename to src/app/(main)/settings/websites/[websiteId]/WebsiteTrackingCode.tsx index 0b844c07..75d69555 100644 --- a/src/app/(main)/settings/websites/[websiteId]/TrackingCode.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteTrackingCode.tsx @@ -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 = ``; return ( - <> -

{formatMessage(messages.trackingCode)}

- - + + {formatMessage(messages.trackingCode)} + + ); } diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx index 444f0524..d0a17b7f 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx @@ -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 ( } showBorder={false}> - +
); diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index 9cc1ccad..6a6e16d4 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -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); } }; diff --git a/src/components/input/DateFilter.tsx b/src/components/input/DateFilter.tsx index 15b3edd3..72be6c6d 100644 --- a/src/components/input/DateFilter.tsx +++ b/src/components/input/DateFilter.tsx @@ -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 ? ( ) : ( defaultChildren diff --git a/src/components/input/WebsiteDateFilter.tsx b/src/components/input/WebsiteDateFilter.tsx index 6842f2d5..afb58631 100644 --- a/src/components/input/WebsiteDateFilter.tsx +++ b/src/components/input/WebsiteDateFilter.tsx @@ -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 && ( diff --git a/src/components/messages.ts b/src/components/messages.ts index ab3779d3..a35d79ce 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -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({ diff --git a/src/components/metrics/ActiveUsers.tsx b/src/components/metrics/ActiveUsers.tsx index 024f6856..0627020e 100644 --- a/src/components/metrics/ActiveUsers.tsx +++ b/src/components/metrics/ActiveUsers.tsx @@ -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 ( - - {formatMessage(messages.numberOfUsers, { x: count })} + + {count} {formatMessage(labels.online)} ); diff --git a/src/index.ts b/src/index.ts index 6988166b..2a077a12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 19006492..4a79843c 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -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; diff --git a/src/lib/date.ts b/src/lib/date.ts index 6c0d77fc..79d9952e 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -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, }; } } diff --git a/src/queries/sql/getActiveVisitors.ts b/src/queries/sql/getActiveVisitors.ts index e0225f3a..9dc52a50 100644 --- a/src/queries/sql/getActiveVisitors.ts +++ b/src/queries/sql/getActiveVisitors.ts @@ -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; diff --git a/src/store/app.ts b/src/store/app.ts index 7713d495..56bd589e 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -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;