aboutsummaryrefslogtreecommitdiff
path: root/src/components/hooks
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-24 13:09:50 +0000
committerFuwn <[email protected]>2026-01-24 13:09:50 +0000
commit396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b (patch)
treeb9df4ca6a70db45cfffbae6fdd7252e20fb8e93c /src/components/hooks
downloadumami-396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b.tar.xz
umami-396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b.zip
Initial commitHEADmain
Created from https://vercel.com/new
Diffstat (limited to 'src/components/hooks')
-rw-r--r--src/components/hooks/context/useLink.ts6
-rw-r--r--src/components/hooks/context/usePixel.ts6
-rw-r--r--src/components/hooks/context/useTeam.ts6
-rw-r--r--src/components/hooks/context/useUser.ts6
-rw-r--r--src/components/hooks/context/useWebsite.ts6
-rw-r--r--src/components/hooks/index.ts84
-rw-r--r--src/components/hooks/queries/useActiveUsersQuery.ts12
-rw-r--r--src/components/hooks/queries/useDateRangeQuery.ts23
-rw-r--r--src/components/hooks/queries/useDeleteQuery.ts12
-rw-r--r--src/components/hooks/queries/useEventDataEventsQuery.ts27
-rw-r--r--src/components/hooks/queries/useEventDataPropertiesQuery.ts27
-rw-r--r--src/components/hooks/queries/useEventDataQuery.ts27
-rw-r--r--src/components/hooks/queries/useEventDataValuesQuery.ts34
-rw-r--r--src/components/hooks/queries/useLinkQuery.ts15
-rw-r--r--src/components/hooks/queries/useLinksQuery.ts17
-rw-r--r--src/components/hooks/queries/useLoginQuery.ts23
-rw-r--r--src/components/hooks/queries/usePixelQuery.ts15
-rw-r--r--src/components/hooks/queries/usePixelsQuery.ts17
-rw-r--r--src/components/hooks/queries/useRealtimeQuery.ts17
-rw-r--r--src/components/hooks/queries/useReportQuery.ts15
-rw-r--r--src/components/hooks/queries/useReportsQuery.ts19
-rw-r--r--src/components/hooks/queries/useResultQuery.ts44
-rw-r--r--src/components/hooks/queries/useSessionActivityQuery.ts21
-rw-r--r--src/components/hooks/queries/useSessionDataPropertiesQuery.ts27
-rw-r--r--src/components/hooks/queries/useSessionDataQuery.ts12
-rw-r--r--src/components/hooks/queries/useSessionDataValuesQuery.ts32
-rw-r--r--src/components/hooks/queries/useShareTokenQuery.ts25
-rw-r--r--src/components/hooks/queries/useTeamMembersQuery.ts16
-rw-r--r--src/components/hooks/queries/useTeamQuery.ts17
-rw-r--r--src/components/hooks/queries/useTeamWebsitesQuery.ts15
-rw-r--r--src/components/hooks/queries/useTeamsQuery.ts20
-rw-r--r--src/components/hooks/queries/useUpdateQuery.ts15
-rw-r--r--src/components/hooks/queries/useUserQuery.ts17
-rw-r--r--src/components/hooks/queries/useUserTeamsQuery.ts15
-rw-r--r--src/components/hooks/queries/useUserWebsitesQuery.ts31
-rw-r--r--src/components/hooks/queries/useUsersQuery.ts17
-rw-r--r--src/components/hooks/queries/useWebsiteCohortQuery.ts21
-rw-r--r--src/components/hooks/queries/useWebsiteCohortsQuery.ts25
-rw-r--r--src/components/hooks/queries/useWebsiteEventsQuery.ts39
-rw-r--r--src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts18
-rw-r--r--src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts51
-rw-r--r--src/components/hooks/queries/useWebsiteMetricsQuery.ts47
-rw-r--r--src/components/hooks/queries/useWebsitePageviewsQuery.ts36
-rw-r--r--src/components/hooks/queries/useWebsiteQuery.ts17
-rw-r--r--src/components/hooks/queries/useWebsiteSegmentQuery.ts21
-rw-r--r--src/components/hooks/queries/useWebsiteSegmentsQuery.ts24
-rw-r--r--src/components/hooks/queries/useWebsiteSessionQuery.ts13
-rw-r--r--src/components/hooks/queries/useWebsiteSessionStatsQuery.ts17
-rw-r--r--src/components/hooks/queries/useWebsiteSessionsQuery.ts34
-rw-r--r--src/components/hooks/queries/useWebsiteStatsQuery.ts36
-rw-r--r--src/components/hooks/queries/useWebsiteValuesQuery.ts62
-rw-r--r--src/components/hooks/queries/useWebsitesQuery.ts20
-rw-r--r--src/components/hooks/queries/useWeeklyTrafficQuery.ts28
-rw-r--r--src/components/hooks/useApi.ts67
-rw-r--r--src/components/hooks/useConfig.ts33
-rw-r--r--src/components/hooks/useCountryNames.ts32
-rw-r--r--src/components/hooks/useDateParameters.ts18
-rw-r--r--src/components/hooks/useDateRange.ts37
-rw-r--r--src/components/hooks/useDocumentClick.ts13
-rw-r--r--src/components/hooks/useEscapeKey.ts19
-rw-r--r--src/components/hooks/useFields.ts23
-rw-r--r--src/components/hooks/useFilterParameters.ts70
-rw-r--r--src/components/hooks/useFilters.ts99
-rw-r--r--src/components/hooks/useForceUpdate.ts9
-rw-r--r--src/components/hooks/useFormat.ts74
-rw-r--r--src/components/hooks/useGlobalState.ts13
-rw-r--r--src/components/hooks/useLanguageNames.ts32
-rw-r--r--src/components/hooks/useLocale.ts60
-rw-r--r--src/components/hooks/useMessages.ts48
-rw-r--r--src/components/hooks/useMobile.ts9
-rw-r--r--src/components/hooks/useModified.ts13
-rw-r--r--src/components/hooks/useNavigation.ts43
-rw-r--r--src/components/hooks/usePageParameters.ts16
-rw-r--r--src/components/hooks/usePagedQuery.ts27
-rw-r--r--src/components/hooks/useRegionNames.ts22
-rw-r--r--src/components/hooks/useSlug.ts14
-rw-r--r--src/components/hooks/useSticky.ts25
-rw-r--r--src/components/hooks/useTimezone.ts95
78 files changed, 2158 insertions, 0 deletions
diff --git a/src/components/hooks/context/useLink.ts b/src/components/hooks/context/useLink.ts
new file mode 100644
index 0000000..8766bbb
--- /dev/null
+++ b/src/components/hooks/context/useLink.ts
@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import { LinkContext } from '@/app/(main)/links/LinkProvider';
+
+export function useLink() {
+ return useContext(LinkContext);
+}
diff --git a/src/components/hooks/context/usePixel.ts b/src/components/hooks/context/usePixel.ts
new file mode 100644
index 0000000..69cad6f
--- /dev/null
+++ b/src/components/hooks/context/usePixel.ts
@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import { PixelContext } from '@/app/(main)/pixels/PixelProvider';
+
+export function usePixel() {
+ return useContext(PixelContext);
+}
diff --git a/src/components/hooks/context/useTeam.ts b/src/components/hooks/context/useTeam.ts
new file mode 100644
index 0000000..95ff4be
--- /dev/null
+++ b/src/components/hooks/context/useTeam.ts
@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import { TeamContext } from '@/app/(main)/teams/TeamProvider';
+
+export function useTeam() {
+ return useContext(TeamContext);
+}
diff --git a/src/components/hooks/context/useUser.ts b/src/components/hooks/context/useUser.ts
new file mode 100644
index 0000000..fa97ea9
--- /dev/null
+++ b/src/components/hooks/context/useUser.ts
@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import { UserContext } from '@/app/(main)/admin/users/[userId]/UserProvider';
+
+export function useUser() {
+ return useContext(UserContext);
+}
diff --git a/src/components/hooks/context/useWebsite.ts b/src/components/hooks/context/useWebsite.ts
new file mode 100644
index 0000000..3d4be27
--- /dev/null
+++ b/src/components/hooks/context/useWebsite.ts
@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import { WebsiteContext } from '@/app/(main)/websites/WebsiteProvider';
+
+export function useWebsite() {
+ return useContext(WebsiteContext);
+}
diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts
new file mode 100644
index 0000000..e8e5c13
--- /dev/null
+++ b/src/components/hooks/index.ts
@@ -0,0 +1,84 @@
+'use client';
+
+// Context hooks
+export * from './context/useLink';
+export * from './context/usePixel';
+export * from './context/useTeam';
+export * from './context/useUser';
+export * from './context/useWebsite';
+
+// Query hooks
+export * from './queries/useActiveUsersQuery';
+export * from './queries/useDateRangeQuery';
+export * from './queries/useDeleteQuery';
+export * from './queries/useEventDataEventsQuery';
+export * from './queries/useEventDataPropertiesQuery';
+export * from './queries/useEventDataQuery';
+export * from './queries/useEventDataValuesQuery';
+export * from './queries/useLinkQuery';
+export * from './queries/useLinksQuery';
+export * from './queries/useLoginQuery';
+export * from './queries/usePixelQuery';
+export * from './queries/usePixelsQuery';
+export * from './queries/useRealtimeQuery';
+export * from './queries/useReportQuery';
+export * from './queries/useReportsQuery';
+export * from './queries/useResultQuery';
+export * from './queries/useSessionActivityQuery';
+export * from './queries/useSessionDataPropertiesQuery';
+export * from './queries/useSessionDataQuery';
+export * from './queries/useSessionDataValuesQuery';
+export * from './queries/useShareTokenQuery';
+export * from './queries/useTeamMembersQuery';
+export * from './queries/useTeamQuery';
+export * from './queries/useTeamsQuery';
+export * from './queries/useTeamWebsitesQuery';
+export * from './queries/useUpdateQuery';
+export * from './queries/useUserQuery';
+export * from './queries/useUsersQuery';
+export * from './queries/useUserTeamsQuery';
+export * from './queries/useUserWebsitesQuery';
+export * from './queries/useWebsiteCohortQuery';
+export * from './queries/useWebsiteCohortsQuery';
+export * from './queries/useWebsiteEventsQuery';
+export * from './queries/useWebsiteEventsSeriesQuery';
+export * from './queries/useWebsiteExpandedMetricsQuery';
+export * from './queries/useWebsiteMetricsQuery';
+export * from './queries/useWebsitePageviewsQuery';
+export * from './queries/useWebsiteQuery';
+export * from './queries/useWebsiteSegmentQuery';
+export * from './queries/useWebsiteSegmentsQuery';
+export * from './queries/useWebsiteSessionQuery';
+export * from './queries/useWebsiteSessionStatsQuery';
+export * from './queries/useWebsiteSessionsQuery';
+export * from './queries/useWebsiteStatsQuery';
+export * from './queries/useWebsitesQuery';
+export * from './queries/useWebsiteValuesQuery';
+export * from './queries/useWeeklyTrafficQuery';
+
+// Regular hooks
+export * from './useApi';
+export * from './useConfig';
+export * from './useCountryNames';
+export * from './useDateParameters';
+export * from './useDateRange';
+export * from './useDocumentClick';
+export * from './useEscapeKey';
+export * from './useFields';
+export * from './useFilterParameters';
+export * from './useFilters';
+export * from './useForceUpdate';
+export * from './useFormat';
+export * from './useGlobalState';
+export * from './useLanguageNames';
+export * from './useLocale';
+export * from './useMessages';
+export * from './useMobile';
+export * from './useModified';
+export * from './useNavigation';
+export * from './usePagedQuery';
+export * from './usePageParameters';
+export * from './useRegionNames';
+export * from './useSlug';
+export * from './useSticky';
+export * from './useTimezone';
diff --git a/src/components/hooks/queries/useActiveUsersQuery.ts b/src/components/hooks/queries/useActiveUsersQuery.ts
new file mode 100644
index 0000000..42867c1
--- /dev/null
+++ b/src/components/hooks/queries/useActiveUsersQuery.ts
@@ -0,0 +1,12 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+
+export function useActyiveUsersQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ return useQuery<any>({
+ queryKey: ['websites:active', websiteId],
+ queryFn: () => get(`/websites/${websiteId}/active`),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useDateRangeQuery.ts b/src/components/hooks/queries/useDateRangeQuery.ts
new file mode 100644
index 0000000..84b7eec
--- /dev/null
+++ b/src/components/hooks/queries/useDateRangeQuery.ts
@@ -0,0 +1,23 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+
+type DateRange = {
+ startDate?: string;
+ endDate?: string;
+};
+
+export function useDateRangeQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+
+ const { data } = useQuery<DateRange>({
+ queryKey: ['date-range', websiteId],
+ queryFn: () => get(`/websites/${websiteId}/daterange`),
+ enabled: !!websiteId,
+ ...options,
+ });
+
+ return {
+ startDate: data?.startDate ? new Date(data.startDate) : null,
+ endDate: data?.endDate ? new Date(data.endDate) : null,
+ };
+}
diff --git a/src/components/hooks/queries/useDeleteQuery.ts b/src/components/hooks/queries/useDeleteQuery.ts
new file mode 100644
index 0000000..556231a
--- /dev/null
+++ b/src/components/hooks/queries/useDeleteQuery.ts
@@ -0,0 +1,12 @@
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useDeleteQuery(path: string, params?: Record<string, any>) {
+ const { del, useMutation } = useApi();
+ const query = useMutation({
+ mutationFn: () => del(path, params),
+ });
+ const { touch } = useModified();
+
+ return { ...query, touch };
+}
diff --git a/src/components/hooks/queries/useEventDataEventsQuery.ts b/src/components/hooks/queries/useEventDataEventsQuery.ts
new file mode 100644
index 0000000..1401989
--- /dev/null
+++ b/src/components/hooks/queries/useEventDataEventsQuery.ts
@@ -0,0 +1,27 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useEventDataEventsQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'websites:event-data:events',
+ { websiteId, startAt, endAt, unit, timezone, ...filters },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/event-data/events`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useEventDataPropertiesQuery.ts b/src/components/hooks/queries/useEventDataPropertiesQuery.ts
new file mode 100644
index 0000000..dfa6e92
--- /dev/null
+++ b/src/components/hooks/queries/useEventDataPropertiesQuery.ts
@@ -0,0 +1,27 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useEventDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery<any>({
+ queryKey: [
+ 'websites:event-data:properties',
+ { websiteId, startAt, endAt, unit, timezone, ...filters },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/event-data/properties`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useEventDataQuery.ts b/src/components/hooks/queries/useEventDataQuery.ts
new file mode 100644
index 0000000..2ccbd63
--- /dev/null
+++ b/src/components/hooks/queries/useEventDataQuery.ts
@@ -0,0 +1,27 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useEventDataQuery(websiteId: string, eventId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const params = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'websites:event-data',
+ { websiteId, eventId, startAt, endAt, unit, timezone, ...params },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/event-data/${eventId}`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...params,
+ }),
+ enabled: !!(websiteId && eventId),
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useEventDataValuesQuery.ts b/src/components/hooks/queries/useEventDataValuesQuery.ts
new file mode 100644
index 0000000..6529e14
--- /dev/null
+++ b/src/components/hooks/queries/useEventDataValuesQuery.ts
@@ -0,0 +1,34 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useEventDataValuesQuery(
+ websiteId: string,
+ event: string,
+ propertyName: string,
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery<any>({
+ queryKey: [
+ 'websites:event-data:values',
+ { websiteId, event, propertyName, startAt, endAt, unit, timezone, ...filters },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/event-data/values`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ event,
+ propertyName,
+ }),
+ enabled: !!(websiteId && propertyName),
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useLinkQuery.ts b/src/components/hooks/queries/useLinkQuery.ts
new file mode 100644
index 0000000..2a5d4a9
--- /dev/null
+++ b/src/components/hooks/queries/useLinkQuery.ts
@@ -0,0 +1,15 @@
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useLinkQuery(linkId: string) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`link:${linkId}`);
+
+ return useQuery({
+ queryKey: ['link', { linkId, modified }],
+ queryFn: () => {
+ return get(`/links/${linkId}`);
+ },
+ enabled: !!linkId,
+ });
+}
diff --git a/src/components/hooks/queries/useLinksQuery.ts b/src/components/hooks/queries/useLinksQuery.ts
new file mode 100644
index 0000000..ebf945f
--- /dev/null
+++ b/src/components/hooks/queries/useLinksQuery.ts
@@ -0,0 +1,17 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useLinksQuery({ teamId }: { teamId?: string }, options?: ReactQueryOptions) {
+ const { modified } = useModified('links');
+ const { get } = useApi();
+
+ return usePagedQuery({
+ queryKey: ['links', { teamId, modified }],
+ queryFn: pageParams => {
+ return get(teamId ? `/teams/${teamId}/links` : '/links', pageParams);
+ },
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useLoginQuery.ts b/src/components/hooks/queries/useLoginQuery.ts
new file mode 100644
index 0000000..a64b784
--- /dev/null
+++ b/src/components/hooks/queries/useLoginQuery.ts
@@ -0,0 +1,23 @@
+import { setUser, useApp } from '@/store/app';
+import { useApi } from '../useApi';
+
+const selector = (state: { user: any }) => state.user;
+
+export function useLoginQuery() {
+ const { post, useQuery } = useApi();
+ const user = useApp(selector);
+
+ const query = useQuery({
+ queryKey: ['login'],
+ queryFn: async () => {
+ const data = await post('/auth/verify');
+
+ setUser(data);
+
+ return data;
+ },
+ enabled: !user,
+ });
+
+ return { user, setUser, ...query };
+}
diff --git a/src/components/hooks/queries/usePixelQuery.ts b/src/components/hooks/queries/usePixelQuery.ts
new file mode 100644
index 0000000..7fd83c2
--- /dev/null
+++ b/src/components/hooks/queries/usePixelQuery.ts
@@ -0,0 +1,15 @@
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function usePixelQuery(pixelId: string) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`pixel:${pixelId}`);
+
+ return useQuery({
+ queryKey: ['pixel', { pixelId, modified }],
+ queryFn: () => {
+ return get(`/pixels/${pixelId}`);
+ },
+ enabled: !!pixelId,
+ });
+}
diff --git a/src/components/hooks/queries/usePixelsQuery.ts b/src/components/hooks/queries/usePixelsQuery.ts
new file mode 100644
index 0000000..c431179
--- /dev/null
+++ b/src/components/hooks/queries/usePixelsQuery.ts
@@ -0,0 +1,17 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function usePixelsQuery({ teamId }: { teamId?: string }, options?: ReactQueryOptions) {
+ const { modified } = useModified('pixels');
+ const { get } = useApi();
+
+ return usePagedQuery({
+ queryKey: ['pixels', { teamId, modified }],
+ queryFn: pageParams => {
+ return get(teamId ? `/teams/${teamId}/pixels` : '/pixels', pageParams);
+ },
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useRealtimeQuery.ts b/src/components/hooks/queries/useRealtimeQuery.ts
new file mode 100644
index 0000000..1a5bd1c
--- /dev/null
+++ b/src/components/hooks/queries/useRealtimeQuery.ts
@@ -0,0 +1,17 @@
+import { REALTIME_INTERVAL } from '@/lib/constants';
+import type { RealtimeData } from '@/lib/types';
+import { useApi } from '../useApi';
+
+export function useRealtimeQuery(websiteId: string) {
+ const { get, useQuery } = useApi();
+ const { data, isLoading, error } = useQuery<RealtimeData>({
+ queryKey: ['realtime', { websiteId }],
+ queryFn: async () => {
+ return get(`/realtime/${websiteId}`);
+ },
+ enabled: !!websiteId,
+ refetchInterval: REALTIME_INTERVAL,
+ });
+
+ return { data, isLoading, error };
+}
diff --git a/src/components/hooks/queries/useReportQuery.ts b/src/components/hooks/queries/useReportQuery.ts
new file mode 100644
index 0000000..6973e2d
--- /dev/null
+++ b/src/components/hooks/queries/useReportQuery.ts
@@ -0,0 +1,15 @@
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useReportQuery(reportId: string) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`report:${reportId}`);
+
+ return useQuery({
+ queryKey: ['report', { reportId, modified }],
+ queryFn: () => {
+ return get(`/reports/${reportId}`);
+ },
+ enabled: !!reportId,
+ });
+}
diff --git a/src/components/hooks/queries/useReportsQuery.ts b/src/components/hooks/queries/useReportsQuery.ts
new file mode 100644
index 0000000..ba1bdd4
--- /dev/null
+++ b/src/components/hooks/queries/useReportsQuery.ts
@@ -0,0 +1,19 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useReportsQuery(
+ { websiteId, type }: { websiteId: string; type?: string },
+ options?: ReactQueryOptions,
+) {
+ const { modified } = useModified(`reports:${type}`);
+ const { get } = useApi();
+
+ return usePagedQuery({
+ queryKey: ['reports', { websiteId, type, modified }],
+ queryFn: async () => get('/reports', { websiteId, type }),
+ enabled: !!websiteId && !!type,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useResultQuery.ts b/src/components/hooks/queries/useResultQuery.ts
new file mode 100644
index 0000000..c6fce12
--- /dev/null
+++ b/src/components/hooks/queries/useResultQuery.ts
@@ -0,0 +1,44 @@
+import { useDateParameters } from '@/components/hooks/useDateParameters';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useResultQuery<T = any>(
+ type: string,
+ params?: Record<string, any>,
+ options?: ReactQueryOptions<T>,
+) {
+ const { websiteId, ...parameters } = params;
+ const { post, useQuery } = useApi();
+ const { startDate, endDate, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery<T>({
+ queryKey: [
+ 'reports',
+ {
+ type,
+ websiteId,
+ startDate,
+ endDate,
+ timezone,
+ ...params,
+ ...filters,
+ },
+ ],
+ queryFn: () =>
+ post(`/reports/${type}`, {
+ websiteId,
+ type,
+ filters,
+ parameters: {
+ startDate,
+ endDate,
+ timezone,
+ ...parameters,
+ },
+ }),
+ enabled: !!type,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useSessionActivityQuery.ts b/src/components/hooks/queries/useSessionActivityQuery.ts
new file mode 100644
index 0000000..d8d34ac
--- /dev/null
+++ b/src/components/hooks/queries/useSessionActivityQuery.ts
@@ -0,0 +1,21 @@
+import { useApi } from '../useApi';
+
+export function useSessionActivityQuery(
+ websiteId: string,
+ sessionId: string,
+ startDate: Date,
+ endDate: Date,
+) {
+ const { get, useQuery } = useApi();
+
+ return useQuery({
+ queryKey: ['session:activity', { websiteId, sessionId, startDate, endDate }],
+ queryFn: () => {
+ return get(`/websites/${websiteId}/sessions/${sessionId}/activity`, {
+ startAt: +new Date(startDate),
+ endAt: +new Date(endDate),
+ });
+ },
+ enabled: Boolean(websiteId && sessionId && startDate && endDate),
+ });
+}
diff --git a/src/components/hooks/queries/useSessionDataPropertiesQuery.ts b/src/components/hooks/queries/useSessionDataPropertiesQuery.ts
new file mode 100644
index 0000000..ac651bb
--- /dev/null
+++ b/src/components/hooks/queries/useSessionDataPropertiesQuery.ts
@@ -0,0 +1,27 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useSessionDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery<any>({
+ queryKey: [
+ 'websites:session-data:properties',
+ { websiteId, startAt, endAt, unit, timezone, ...filters },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/session-data/properties`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useSessionDataQuery.ts b/src/components/hooks/queries/useSessionDataQuery.ts
new file mode 100644
index 0000000..62b5398
--- /dev/null
+++ b/src/components/hooks/queries/useSessionDataQuery.ts
@@ -0,0 +1,12 @@
+import { useApi } from '../useApi';
+
+export function useSessionDataQuery(websiteId: string, sessionId: string) {
+ const { get, useQuery } = useApi();
+
+ return useQuery({
+ queryKey: ['session:data', { websiteId, sessionId }],
+ queryFn: () => {
+ return get(`/websites/${websiteId}/sessions/${sessionId}/properties`, { websiteId });
+ },
+ });
+}
diff --git a/src/components/hooks/queries/useSessionDataValuesQuery.ts b/src/components/hooks/queries/useSessionDataValuesQuery.ts
new file mode 100644
index 0000000..d5e180b
--- /dev/null
+++ b/src/components/hooks/queries/useSessionDataValuesQuery.ts
@@ -0,0 +1,32 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useSessionDataValuesQuery(
+ websiteId: string,
+ propertyName: string,
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery<any>({
+ queryKey: [
+ 'websites:session-data:values',
+ { websiteId, propertyName, startAt, endAt, unit, timezone, ...filters },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/session-data/values`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ propertyName,
+ }),
+ enabled: !!(websiteId && propertyName),
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useShareTokenQuery.ts b/src/components/hooks/queries/useShareTokenQuery.ts
new file mode 100644
index 0000000..dbad3dc
--- /dev/null
+++ b/src/components/hooks/queries/useShareTokenQuery.ts
@@ -0,0 +1,25 @@
+import { setShareToken, useApp } from '@/store/app';
+import { useApi } from '../useApi';
+
+const selector = (state: { shareToken: string }) => state.shareToken;
+
+export function useShareTokenQuery(shareId: string): {
+ shareToken: any;
+ isLoading?: boolean;
+ error?: Error;
+} {
+ const shareToken = useApp(selector);
+ const { get, useQuery } = useApi();
+ const { isLoading, error } = useQuery({
+ queryKey: ['share', shareId],
+ queryFn: async () => {
+ const data = await get(`/share/${shareId}`);
+
+ setShareToken(data);
+
+ return data;
+ },
+ });
+
+ return { shareToken, isLoading, error };
+}
diff --git a/src/components/hooks/queries/useTeamMembersQuery.ts b/src/components/hooks/queries/useTeamMembersQuery.ts
new file mode 100644
index 0000000..6f6f815
--- /dev/null
+++ b/src/components/hooks/queries/useTeamMembersQuery.ts
@@ -0,0 +1,16 @@
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useTeamMembersQuery(teamId: string) {
+ const { get } = useApi();
+ const { modified } = useModified(`teams:members`);
+
+ return usePagedQuery({
+ queryKey: ['teams:members', { teamId, modified }],
+ queryFn: (params: any) => {
+ return get(`/teams/${teamId}/users`, params);
+ },
+ enabled: !!teamId,
+ });
+}
diff --git a/src/components/hooks/queries/useTeamQuery.ts b/src/components/hooks/queries/useTeamQuery.ts
new file mode 100644
index 0000000..c076a6a
--- /dev/null
+++ b/src/components/hooks/queries/useTeamQuery.ts
@@ -0,0 +1,17 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useTeamQuery(teamId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`teams:${teamId}`);
+
+ return useQuery({
+ queryKey: ['teams', { teamId, modified }],
+ queryFn: () => get(`/teams/${teamId}`),
+ enabled: !!teamId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useTeamWebsitesQuery.ts b/src/components/hooks/queries/useTeamWebsitesQuery.ts
new file mode 100644
index 0000000..ffe601b
--- /dev/null
+++ b/src/components/hooks/queries/useTeamWebsitesQuery.ts
@@ -0,0 +1,15 @@
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useTeamWebsitesQuery(teamId: string) {
+ const { get } = useApi();
+ const { modified } = useModified(`websites`);
+
+ return usePagedQuery({
+ queryKey: ['teams:websites', { teamId, modified }],
+ queryFn: (params: any) => {
+ return get(`/teams/${teamId}/websites`, params);
+ },
+ });
+}
diff --git a/src/components/hooks/queries/useTeamsQuery.ts b/src/components/hooks/queries/useTeamsQuery.ts
new file mode 100644
index 0000000..f1a09f4
--- /dev/null
+++ b/src/components/hooks/queries/useTeamsQuery.ts
@@ -0,0 +1,20 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useTeamsQuery(params?: Record<string, any>, options?: ReactQueryOptions) {
+ const { get } = useApi();
+ const { modified } = useModified(`teams`);
+
+ return usePagedQuery({
+ queryKey: ['teams:admin', { modified, ...params }],
+ queryFn: pageParams => {
+ return get(`/admin/teams`, {
+ ...pageParams,
+ ...params,
+ });
+ },
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useUpdateQuery.ts b/src/components/hooks/queries/useUpdateQuery.ts
new file mode 100644
index 0000000..85a9442
--- /dev/null
+++ b/src/components/hooks/queries/useUpdateQuery.ts
@@ -0,0 +1,15 @@
+import { useToast } from '@umami/react-zen';
+import type { ApiError } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useUpdateQuery(path: string, params?: Record<string, any>) {
+ const { post, useMutation } = useApi();
+ const query = useMutation<any, ApiError, Record<string, any>>({
+ mutationFn: (data: Record<string, any>) => post(path, { ...data, ...params }),
+ });
+ const { touch } = useModified();
+ const { toast } = useToast();
+
+ return { ...query, touch, toast };
+}
diff --git a/src/components/hooks/queries/useUserQuery.ts b/src/components/hooks/queries/useUserQuery.ts
new file mode 100644
index 0000000..07e23f0
--- /dev/null
+++ b/src/components/hooks/queries/useUserQuery.ts
@@ -0,0 +1,17 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useUserQuery(userId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`user:${userId}`);
+
+ return useQuery({
+ queryKey: ['users', { userId, modified }],
+ queryFn: () => get(`/users/${userId}`),
+ enabled: !!userId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useUserTeamsQuery.ts b/src/components/hooks/queries/useUserTeamsQuery.ts
new file mode 100644
index 0000000..82f6549
--- /dev/null
+++ b/src/components/hooks/queries/useUserTeamsQuery.ts
@@ -0,0 +1,15 @@
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useUserTeamsQuery(userId: string) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`teams`);
+
+ return useQuery({
+ queryKey: ['teams', { userId, modified }],
+ queryFn: () => {
+ return get(`/users/${userId}/teams`);
+ },
+ enabled: !!userId,
+ });
+}
diff --git a/src/components/hooks/queries/useUserWebsitesQuery.ts b/src/components/hooks/queries/useUserWebsitesQuery.ts
new file mode 100644
index 0000000..f98eaff
--- /dev/null
+++ b/src/components/hooks/queries/useUserWebsitesQuery.ts
@@ -0,0 +1,31 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useUserWebsitesQuery(
+ { userId, teamId }: { userId?: string; teamId?: string },
+ params?: Record<string, any>,
+ options?: ReactQueryOptions,
+) {
+ const { get } = useApi();
+ const { modified } = useModified(`websites`);
+
+ return usePagedQuery({
+ queryKey: ['websites', { userId, teamId, modified, ...params }],
+ queryFn: pageParams => {
+ return get(
+ teamId
+ ? `/teams/${teamId}/websites`
+ : userId
+ ? `/users/${userId}/websites`
+ : '/me/websites',
+ {
+ ...pageParams,
+ ...params,
+ },
+ );
+ },
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useUsersQuery.ts b/src/components/hooks/queries/useUsersQuery.ts
new file mode 100644
index 0000000..d87900b
--- /dev/null
+++ b/src/components/hooks/queries/useUsersQuery.ts
@@ -0,0 +1,17 @@
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useUsersQuery() {
+ const { get } = useApi();
+ const { modified } = useModified(`users`);
+
+ return usePagedQuery({
+ queryKey: ['users:admin', { modified }],
+ queryFn: (pageParams: any) => {
+ return get('/admin/users', {
+ ...pageParams,
+ });
+ },
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteCohortQuery.ts b/src/components/hooks/queries/useWebsiteCohortQuery.ts
new file mode 100644
index 0000000..975766e
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteCohortQuery.ts
@@ -0,0 +1,21 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useWebsiteCohortQuery(
+ websiteId: string,
+ cohortId: string,
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`cohorts`);
+
+ return useQuery({
+ queryKey: ['website:cohorts', { websiteId, cohortId, modified }],
+ queryFn: () => get(`/websites/${websiteId}/segments/${cohortId}`),
+ enabled: !!(websiteId && cohortId),
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteCohortsQuery.ts b/src/components/hooks/queries/useWebsiteCohortsQuery.ts
new file mode 100644
index 0000000..e0cbf4c
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteCohortsQuery.ts
@@ -0,0 +1,25 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import { useFilterParameters } from '@/components/hooks/useFilterParameters';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useWebsiteCohortsQuery(
+ websiteId: string,
+ params?: Record<string, string>,
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`cohorts`);
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: ['website:cohorts', { websiteId, modified, ...filters, ...params }],
+ queryFn: pageParams => {
+ return get(`/websites/${websiteId}/segments`, { ...pageParams, ...filters, ...params });
+ },
+ enabled: !!websiteId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteEventsQuery.ts b/src/components/hooks/queries/useWebsiteEventsQuery.ts
new file mode 100644
index 0000000..fc4dad5
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteEventsQuery.ts
@@ -0,0 +1,39 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+import { usePagedQuery } from '../usePagedQuery';
+
+const EVENT_TYPES = {
+ views: 1,
+ events: 2,
+};
+
+export function useWebsiteEventsQuery(
+ websiteId: string,
+ params?: Record<string, any>,
+ options?: ReactQueryOptions,
+) {
+ const { get } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return usePagedQuery({
+ queryKey: [
+ 'websites:events',
+ { websiteId, startAt, endAt, unit, timezone, ...filters, ...params },
+ ],
+ queryFn: pageParams =>
+ get(`/websites/${websiteId}/events`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ ...pageParams,
+ eventType: EVENT_TYPES[params.view],
+ }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts b/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts
new file mode 100644
index 0000000..6c1d112
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts
@@ -0,0 +1,18 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useWebsiteEventsSeriesQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: ['websites:events:series', { websiteId, startAt, endAt, unit, timezone, ...filters }],
+ queryFn: () =>
+ get(`/websites/${websiteId}/events/series`, { startAt, endAt, unit, timezone, ...filters }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts b/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts
new file mode 100644
index 0000000..b2e9019
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteExpandedMetricsQuery.ts
@@ -0,0 +1,51 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export type WebsiteExpandedMetricsData = {
+ name: string;
+ pageviews: number;
+ visitors: number;
+ visits: number;
+ bounces: number;
+ totaltime: number;
+}[];
+
+export function useWebsiteExpandedMetricsQuery(
+ websiteId: string,
+ params: { type: string; limit?: number; search?: string },
+ options?: ReactQueryOptions<WebsiteExpandedMetricsData>,
+) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery<WebsiteExpandedMetricsData>({
+ queryKey: [
+ 'websites:metrics:expanded',
+ {
+ websiteId,
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ ...params,
+ },
+ ],
+ queryFn: async () =>
+ get(`/websites/${websiteId}/metrics/expanded`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ ...params,
+ }),
+ enabled: !!websiteId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteMetricsQuery.ts b/src/components/hooks/queries/useWebsiteMetricsQuery.ts
new file mode 100644
index 0000000..67c5e4d
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteMetricsQuery.ts
@@ -0,0 +1,47 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export type WebsiteMetricsData = {
+ x: string;
+ y: number;
+}[];
+
+export function useWebsiteMetricsQuery(
+ websiteId: string,
+ params: { type: string; limit?: number; search?: string },
+ options?: ReactQueryOptions<WebsiteMetricsData>,
+) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery<WebsiteMetricsData>({
+ queryKey: [
+ 'websites:metrics',
+ {
+ websiteId,
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ ...params,
+ },
+ ],
+ queryFn: async () =>
+ get(`/websites/${websiteId}/metrics`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ ...params,
+ }),
+ enabled: !!websiteId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsitePageviewsQuery.ts b/src/components/hooks/queries/useWebsitePageviewsQuery.ts
new file mode 100644
index 0000000..b35c820
--- /dev/null
+++ b/src/components/hooks/queries/useWebsitePageviewsQuery.ts
@@ -0,0 +1,36 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export interface WebsitePageviewsData {
+ pageviews: { x: string; y: number }[];
+ sessions: { x: string; y: number }[];
+}
+
+export function useWebsitePageviewsQuery(
+ { websiteId, compare }: { websiteId: string; compare?: string },
+ options?: ReactQueryOptions<WebsitePageviewsData>,
+) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const queryParams = useFilterParameters();
+
+ return useQuery<WebsitePageviewsData>({
+ queryKey: [
+ 'websites:pageviews',
+ { websiteId, compare, startAt, endAt, unit, timezone, ...queryParams },
+ ],
+ queryFn: () =>
+ get(`/websites/${websiteId}/pageviews`, {
+ compare,
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...queryParams,
+ }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteQuery.ts b/src/components/hooks/queries/useWebsiteQuery.ts
new file mode 100644
index 0000000..b9a5533
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteQuery.ts
@@ -0,0 +1,17 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useWebsiteQuery(websiteId: string, options?: ReactQueryOptions) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`website:${websiteId}`);
+
+ return useQuery({
+ queryKey: ['website', { websiteId, modified }],
+ queryFn: () => get(`/websites/${websiteId}`),
+ enabled: !!websiteId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteSegmentQuery.ts b/src/components/hooks/queries/useWebsiteSegmentQuery.ts
new file mode 100644
index 0000000..1923fbd
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteSegmentQuery.ts
@@ -0,0 +1,21 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useWebsiteSegmentQuery(
+ websiteId: string,
+ segmentId: string,
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`segments`);
+
+ return useQuery({
+ queryKey: ['website:segments', { websiteId, segmentId, modified }],
+ queryFn: () => get(`/websites/${websiteId}/segments/${segmentId}`),
+ enabled: !!(websiteId && segmentId),
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteSegmentsQuery.ts b/src/components/hooks/queries/useWebsiteSegmentsQuery.ts
new file mode 100644
index 0000000..8d3af96
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteSegmentsQuery.ts
@@ -0,0 +1,24 @@
+import { keepPreviousData } from '@tanstack/react-query';
+import { useFilterParameters } from '@/components/hooks/useFilterParameters';
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+
+export function useWebsiteSegmentsQuery(
+ websiteId: string,
+ params?: Record<string, string>,
+ options?: ReactQueryOptions,
+) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`segments`);
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: ['website:segments', { websiteId, modified, ...filters, ...params }],
+ queryFn: pageParams =>
+ get(`/websites/${websiteId}/segments`, { ...pageParams, ...filters, ...params }),
+ enabled: !!websiteId,
+ placeholderData: keepPreviousData,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteSessionQuery.ts b/src/components/hooks/queries/useWebsiteSessionQuery.ts
new file mode 100644
index 0000000..21e9491
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteSessionQuery.ts
@@ -0,0 +1,13 @@
+import { useApi } from '../useApi';
+
+export function useWebsiteSessionQuery(websiteId: string, sessionId: string) {
+ const { get, useQuery } = useApi();
+
+ return useQuery({
+ queryKey: ['session', { websiteId, sessionId }],
+ queryFn: () => {
+ return get(`/websites/${websiteId}/sessions/${sessionId}`);
+ },
+ enabled: Boolean(websiteId && sessionId),
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteSessionStatsQuery.ts b/src/components/hooks/queries/useWebsiteSessionStatsQuery.ts
new file mode 100644
index 0000000..bac9fc9
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteSessionStatsQuery.ts
@@ -0,0 +1,17 @@
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+
+export function useWebsiteSessionStatsQuery(websiteId: string, options?: Record<string, string>) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: ['sessions:stats', { websiteId, startAt, endAt, unit, timezone, ...filters }],
+ queryFn: () =>
+ get(`/websites/${websiteId}/sessions/stats`, { startAt, endAt, unit, timezone, ...filters }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteSessionsQuery.ts b/src/components/hooks/queries/useWebsiteSessionsQuery.ts
new file mode 100644
index 0000000..31906be
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteSessionsQuery.ts
@@ -0,0 +1,34 @@
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useFilterParameters } from '../useFilterParameters';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useWebsiteSessionsQuery(
+ websiteId: string,
+ params?: Record<string, string | number>,
+) {
+ const { get } = useApi();
+ const { modified } = useModified(`sessions`);
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return usePagedQuery({
+ queryKey: [
+ 'sessions',
+ { websiteId, modified, startAt, endAt, unit, timezone, ...params, ...filters },
+ ],
+ queryFn: pageParams => {
+ return get(`/websites/${websiteId}/sessions`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...filters,
+ ...pageParams,
+ ...params,
+ pageSize: 20,
+ });
+ },
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteStatsQuery.ts b/src/components/hooks/queries/useWebsiteStatsQuery.ts
new file mode 100644
index 0000000..e9a0c48
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteStatsQuery.ts
@@ -0,0 +1,36 @@
+import type { UseQueryOptions } from '@tanstack/react-query';
+import { useDateParameters } from '@/components/hooks/useDateParameters';
+import { useApi } from '../useApi';
+import { useFilterParameters } from '../useFilterParameters';
+
+export interface WebsiteStatsData {
+ pageviews: number;
+ visitors: number;
+ visits: number;
+ bounces: number;
+ totaltime: number;
+ comparison: {
+ pageviews: number;
+ visitors: number;
+ visits: number;
+ bounces: number;
+ totaltime: number;
+ };
+}
+
+export function useWebsiteStatsQuery(
+ websiteId: string,
+ options?: UseQueryOptions<WebsiteStatsData, Error, WebsiteStatsData>,
+) {
+ const { get, useQuery } = useApi();
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery<WebsiteStatsData>({
+ queryKey: ['websites:stats', { websiteId, startAt, endAt, unit, timezone, ...filters }],
+ queryFn: () =>
+ get(`/websites/${websiteId}/stats`, { startAt, endAt, unit, timezone, ...filters }),
+ enabled: !!websiteId,
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWebsiteValuesQuery.ts b/src/components/hooks/queries/useWebsiteValuesQuery.ts
new file mode 100644
index 0000000..1e09736
--- /dev/null
+++ b/src/components/hooks/queries/useWebsiteValuesQuery.ts
@@ -0,0 +1,62 @@
+import { useCountryNames } from '@/components/hooks/useCountryNames';
+import { useRegionNames } from '@/components/hooks/useRegionNames';
+import { useApi } from '../useApi';
+import { useLocale } from '../useLocale';
+
+export function useWebsiteValuesQuery({
+ websiteId,
+ type,
+ startDate,
+ endDate,
+ search,
+}: {
+ websiteId: string;
+ type: string;
+ startDate: Date;
+ endDate: Date;
+ search?: string;
+}) {
+ const { get, useQuery } = useApi();
+ const { locale } = useLocale();
+ const { countryNames } = useCountryNames(locale);
+ const { regionNames } = useRegionNames(locale);
+
+ const names = {
+ country: countryNames,
+ region: regionNames,
+ };
+
+ const getSearch = (type: string, value: string) => {
+ if (value) {
+ const values = names[type];
+
+ if (values) {
+ return (
+ Object.keys(values)
+ .reduce((arr: string[], key: string) => {
+ if (values[key].toLowerCase().includes(value.toLowerCase())) {
+ return arr.concat(key);
+ }
+ return arr;
+ }, [])
+ .slice(0, 5)
+ .join(',') || value
+ );
+ }
+
+ return value;
+ }
+ };
+
+ return useQuery({
+ queryKey: ['websites:values', { websiteId, type, startDate, endDate, search }],
+ queryFn: () =>
+ get(`/websites/${websiteId}/values`, {
+ type,
+ startAt: +startDate,
+ endAt: +endDate,
+ search: getSearch(type, search),
+ }),
+ enabled: !!(websiteId && type && startDate && endDate),
+ });
+}
diff --git a/src/components/hooks/queries/useWebsitesQuery.ts b/src/components/hooks/queries/useWebsitesQuery.ts
new file mode 100644
index 0000000..a7b6618
--- /dev/null
+++ b/src/components/hooks/queries/useWebsitesQuery.ts
@@ -0,0 +1,20 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useWebsitesQuery(params?: Record<string, any>, options?: ReactQueryOptions) {
+ const { get } = useApi();
+ const { modified } = useModified(`websites`);
+
+ return usePagedQuery({
+ queryKey: ['websites:admin', { modified, ...params }],
+ queryFn: pageParams => {
+ return get(`/admin/websites`, {
+ ...pageParams,
+ ...params,
+ });
+ },
+ ...options,
+ });
+}
diff --git a/src/components/hooks/queries/useWeeklyTrafficQuery.ts b/src/components/hooks/queries/useWeeklyTrafficQuery.ts
new file mode 100644
index 0000000..a76ebb3
--- /dev/null
+++ b/src/components/hooks/queries/useWeeklyTrafficQuery.ts
@@ -0,0 +1,28 @@
+import { useFilterParameters } from '@/components/hooks/useFilterParameters';
+import { useApi } from '../useApi';
+import { useDateParameters } from '../useDateParameters';
+import { useModified } from '../useModified';
+
+export function useWeeklyTrafficQuery(websiteId: string, params?: Record<string, string | number>) {
+ const { get, useQuery } = useApi();
+ const { modified } = useModified(`sessions`);
+ const { startAt, endAt, unit, timezone } = useDateParameters();
+ const filters = useFilterParameters();
+
+ return useQuery({
+ queryKey: [
+ 'sessions',
+ { websiteId, modified, startAt, endAt, unit, timezone, ...params, ...filters },
+ ],
+ queryFn: () => {
+ return get(`/websites/${websiteId}/sessions/weekly`, {
+ startAt,
+ endAt,
+ unit,
+ timezone,
+ ...params,
+ ...filters,
+ });
+ },
+ });
+}
diff --git a/src/components/hooks/useApi.ts b/src/components/hooks/useApi.ts
new file mode 100644
index 0000000..35cabd5
--- /dev/null
+++ b/src/components/hooks/useApi.ts
@@ -0,0 +1,67 @@
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { useCallback } from 'react';
+import { getClientAuthToken } from '@/lib/client';
+import { SHARE_TOKEN_HEADER } from '@/lib/constants';
+import { type FetchResponse, httpDelete, httpGet, httpPost, httpPut } from '@/lib/fetch';
+import { useApp } from '@/store/app';
+
+const selector = (state: { shareToken: { token?: string } }) => state.shareToken;
+
+async function handleResponse(res: FetchResponse): Promise<any> {
+ if (!res.ok) {
+ const { message, code, status } = res?.data?.error || {};
+
+ return Promise.reject(Object.assign(new Error(message), { code, status }));
+ }
+ return Promise.resolve(res.data);
+}
+
+export function useApi() {
+ const shareToken = useApp(selector);
+
+ const defaultHeaders = {
+ authorization: `Bearer ${getClientAuthToken()}`,
+ [SHARE_TOKEN_HEADER]: shareToken?.token,
+ };
+ const basePath = process.env.basePath;
+
+ const getUrl = (url: string) => {
+ return url.startsWith('http') ? url : `${basePath || ''}/api${url}`;
+ };
+
+ const getHeaders = (headers: any = {}) => {
+ return { ...defaultHeaders, ...headers };
+ };
+
+ return {
+ get: useCallback(
+ async (url: string, params: object = {}, headers: object = {}) => {
+ return httpGet(getUrl(url), params, getHeaders(headers)).then(handleResponse);
+ },
+ [httpGet],
+ ),
+
+ post: useCallback(
+ async (url: string, params: object = {}, headers: object = {}) => {
+ return httpPost(getUrl(url), params, getHeaders(headers)).then(handleResponse);
+ },
+ [httpPost],
+ ),
+
+ put: useCallback(
+ async (url: string, params: object = {}, headers: object = {}) => {
+ return httpPut(getUrl(url), params, getHeaders(headers)).then(handleResponse);
+ },
+ [httpPut],
+ ),
+
+ del: useCallback(
+ async (url: string, params: object = {}, headers: object = {}) => {
+ return httpDelete(getUrl(url), params, getHeaders(headers)).then(handleResponse);
+ },
+ [httpDelete],
+ ),
+ useQuery,
+ useMutation,
+ };
+}
diff --git a/src/components/hooks/useConfig.ts b/src/components/hooks/useConfig.ts
new file mode 100644
index 0000000..c1cdcaf
--- /dev/null
+++ b/src/components/hooks/useConfig.ts
@@ -0,0 +1,33 @@
+import { useEffect } from 'react';
+import { useApi } from '@/components/hooks/useApi';
+import { setConfig, useApp } from '@/store/app';
+
+export type Config = {
+ cloudMode: boolean;
+ faviconUrl?: string;
+ linksUrl?: string;
+ pixelsUrl?: string;
+ privateMode: boolean;
+ telemetryDisabled: boolean;
+ trackerScriptName?: string;
+ updatesDisabled: boolean;
+};
+
+export function useConfig(): Config {
+ const { config } = useApp();
+ const { get } = useApi();
+
+ async function loadConfig() {
+ const data = await get(`/config`);
+
+ setConfig(data);
+ }
+
+ useEffect(() => {
+ if (!config) {
+ loadConfig();
+ }
+ }, []);
+
+ return config;
+}
diff --git a/src/components/hooks/useCountryNames.ts b/src/components/hooks/useCountryNames.ts
new file mode 100644
index 0000000..1ec9fc1
--- /dev/null
+++ b/src/components/hooks/useCountryNames.ts
@@ -0,0 +1,32 @@
+import { useEffect, useState } from 'react';
+import { httpGet } from '@/lib/fetch';
+import enUS from '../../../public/intl/country/en-US.json';
+
+const countryNames = {
+ 'en-US': enUS,
+};
+
+export function useCountryNames(locale: string) {
+ const [list, setList] = useState(countryNames[locale] || enUS);
+
+ async function loadData(locale: string) {
+ const { data } = await httpGet(`${process.env.basePath || ''}/intl/country/${locale}.json`);
+
+ if (data) {
+ countryNames[locale] = data;
+ setList(countryNames[locale]);
+ } else {
+ setList(enUS);
+ }
+ }
+
+ useEffect(() => {
+ if (!countryNames[locale]) {
+ loadData(locale);
+ } else {
+ setList(countryNames[locale]);
+ }
+ }, [locale]);
+
+ return { countryNames: list };
+}
diff --git a/src/components/hooks/useDateParameters.ts b/src/components/hooks/useDateParameters.ts
new file mode 100644
index 0000000..d84b423
--- /dev/null
+++ b/src/components/hooks/useDateParameters.ts
@@ -0,0 +1,18 @@
+import { useDateRange } from './useDateRange';
+import { useTimezone } from './useTimezone';
+
+export function useDateParameters() {
+ const {
+ dateRange: { startDate, endDate, unit },
+ } = useDateRange();
+ const { timezone, localToUtc, canonicalizeTimezone } = useTimezone();
+
+ return {
+ startAt: +localToUtc(startDate),
+ endAt: +localToUtc(endDate),
+ startDate: localToUtc(startDate).toISOString(),
+ endDate: localToUtc(endDate).toISOString(),
+ unit,
+ timezone: canonicalizeTimezone(timezone),
+ };
+}
diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts
new file mode 100644
index 0000000..755f36e
--- /dev/null
+++ b/src/components/hooks/useDateRange.ts
@@ -0,0 +1,37 @@
+import { useMemo } from 'react';
+import { useLocale } from '@/components/hooks/useLocale';
+import { useNavigation } from '@/components/hooks/useNavigation';
+import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
+import { getCompareDate, getOffsetDateRange, parseDateRange } from '@/lib/date';
+import { getItem } from '@/lib/storage';
+
+export function useDateRange(options: { ignoreOffset?: boolean; timezone?: string } = {}) {
+ const {
+ query: { date = '', offset = 0, compare = 'prev' },
+ } = useNavigation();
+ const { locale } = useLocale();
+
+ const dateRange = useMemo(() => {
+ const dateRangeObject = parseDateRange(
+ date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
+ locale,
+ options.timezone,
+ );
+
+ return !options.ignoreOffset && offset
+ ? getOffsetDateRange(dateRangeObject, +offset)
+ : dateRangeObject;
+ }, [date, offset, options]);
+
+ const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate);
+
+ return {
+ date,
+ offset,
+ compare,
+ isAllTime: date.endsWith(`:all`),
+ isCustomRange: date.startsWith('range:'),
+ dateRange,
+ dateCompare,
+ };
+}
diff --git a/src/components/hooks/useDocumentClick.ts b/src/components/hooks/useDocumentClick.ts
new file mode 100644
index 0000000..611f628
--- /dev/null
+++ b/src/components/hooks/useDocumentClick.ts
@@ -0,0 +1,13 @@
+import { useEffect } from 'react';
+
+export function useDocumentClick(handler: (event: MouseEvent) => any) {
+ useEffect(() => {
+ document.addEventListener('click', handler);
+
+ return () => {
+ document.removeEventListener('click', handler);
+ };
+ }, [handler]);
+
+ return null;
+}
diff --git a/src/components/hooks/useEscapeKey.ts b/src/components/hooks/useEscapeKey.ts
new file mode 100644
index 0000000..cc1d308
--- /dev/null
+++ b/src/components/hooks/useEscapeKey.ts
@@ -0,0 +1,19 @@
+import { type KeyboardEvent, useCallback, useEffect } from 'react';
+
+export function useEscapeKey(handler: (event: KeyboardEvent) => void) {
+ const escFunction = useCallback((event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ handler(event);
+ }
+ }, []);
+
+ useEffect(() => {
+ document.addEventListener('keydown', escFunction as any, false);
+
+ return () => {
+ document.removeEventListener('keydown', escFunction as any, false);
+ };
+ }, [escFunction]);
+
+ return null;
+}
diff --git a/src/components/hooks/useFields.ts b/src/components/hooks/useFields.ts
new file mode 100644
index 0000000..22a1dcf
--- /dev/null
+++ b/src/components/hooks/useFields.ts
@@ -0,0 +1,23 @@
+import { useMessages } from './useMessages';
+
+export function useFields() {
+ const { formatMessage, labels } = useMessages();
+
+ const fields = [
+ { name: 'path', type: 'string', label: formatMessage(labels.path) },
+ { name: 'query', type: 'string', label: formatMessage(labels.query) },
+ { name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
+ { name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
+ { name: 'browser', type: 'string', label: formatMessage(labels.browser) },
+ { name: 'os', type: 'string', label: formatMessage(labels.os) },
+ { name: 'device', type: 'string', label: formatMessage(labels.device) },
+ { name: 'country', type: 'string', label: formatMessage(labels.country) },
+ { name: 'region', type: 'string', label: formatMessage(labels.region) },
+ { name: 'city', type: 'string', label: formatMessage(labels.city) },
+ { name: 'hostname', type: 'string', label: formatMessage(labels.hostname) },
+ { name: 'tag', type: 'string', label: formatMessage(labels.tag) },
+ { name: 'event', type: 'string', label: formatMessage(labels.event) },
+ ];
+
+ return { fields };
+}
diff --git a/src/components/hooks/useFilterParameters.ts b/src/components/hooks/useFilterParameters.ts
new file mode 100644
index 0000000..5403212
--- /dev/null
+++ b/src/components/hooks/useFilterParameters.ts
@@ -0,0 +1,70 @@
+import { useMemo } from 'react';
+import { useNavigation } from './useNavigation';
+
+export function useFilterParameters() {
+ const {
+ query: {
+ path,
+ referrer,
+ title,
+ query,
+ host,
+ os,
+ browser,
+ device,
+ country,
+ region,
+ city,
+ event,
+ tag,
+ hostname,
+ page,
+ pageSize,
+ search,
+ segment,
+ cohort,
+ },
+ } = useNavigation();
+
+ return useMemo(() => {
+ return {
+ path,
+ referrer,
+ title,
+ query,
+ host,
+ os,
+ browser,
+ device,
+ country,
+ region,
+ city,
+ event,
+ tag,
+ hostname,
+ search,
+ segment,
+ cohort,
+ };
+ }, [
+ path,
+ referrer,
+ title,
+ query,
+ host,
+ os,
+ browser,
+ device,
+ country,
+ region,
+ city,
+ event,
+ tag,
+ hostname,
+ page,
+ pageSize,
+ search,
+ segment,
+ cohort,
+ ]);
+}
diff --git a/src/components/hooks/useFilters.ts b/src/components/hooks/useFilters.ts
new file mode 100644
index 0000000..850e2af
--- /dev/null
+++ b/src/components/hooks/useFilters.ts
@@ -0,0 +1,99 @@
+import { FILTER_COLUMNS, OPERATORS } from '@/lib/constants';
+import { safeDecodeURIComponent } from '@/lib/url';
+import { useFields } from './useFields';
+import { useMessages } from './useMessages';
+import { useNavigation } from './useNavigation';
+
+export function useFilters() {
+ const { formatMessage, labels } = useMessages();
+ const { query } = useNavigation();
+ const { fields } = useFields();
+
+ const operators = [
+ { name: 'eq', type: 'string', label: formatMessage(labels.is) },
+ { name: 'neq', type: 'string', label: formatMessage(labels.isNot) },
+ { name: 'c', type: 'string', label: formatMessage(labels.contains) },
+ { name: 'dnc', type: 'string', label: formatMessage(labels.doesNotContain) },
+ { name: 'i', type: 'array', label: formatMessage(labels.includes) },
+ { name: 'dni', type: 'array', label: formatMessage(labels.doesNotInclude) },
+ { name: 't', type: 'boolean', label: formatMessage(labels.isTrue) },
+ { name: 'f', type: 'boolean', label: formatMessage(labels.isFalse) },
+ { name: 'eq', type: 'number', label: formatMessage(labels.is) },
+ { name: 'neq', type: 'number', label: formatMessage(labels.isNot) },
+ { name: 'gt', type: 'number', label: formatMessage(labels.greaterThan) },
+ { name: 'lt', type: 'number', label: formatMessage(labels.lessThan) },
+ { name: 'gte', type: 'number', label: formatMessage(labels.greaterThanEquals) },
+ { name: 'lte', type: 'number', label: formatMessage(labels.lessThanEquals) },
+ { name: 'bf', type: 'date', label: formatMessage(labels.before) },
+ { name: 'af', type: 'date', label: formatMessage(labels.after) },
+ { name: 'eq', type: 'uuid', label: formatMessage(labels.is) },
+ ];
+
+ const operatorLabels = {
+ [OPERATORS.equals]: formatMessage(labels.is),
+ [OPERATORS.notEquals]: formatMessage(labels.isNot),
+ [OPERATORS.set]: formatMessage(labels.isSet),
+ [OPERATORS.notSet]: formatMessage(labels.isNotSet),
+ [OPERATORS.contains]: formatMessage(labels.contains),
+ [OPERATORS.doesNotContain]: formatMessage(labels.doesNotContain),
+ [OPERATORS.true]: formatMessage(labels.true),
+ [OPERATORS.false]: formatMessage(labels.false),
+ [OPERATORS.greaterThan]: formatMessage(labels.greaterThan),
+ [OPERATORS.lessThan]: formatMessage(labels.lessThan),
+ [OPERATORS.greaterThanEquals]: formatMessage(labels.greaterThanEquals),
+ [OPERATORS.lessThanEquals]: formatMessage(labels.lessThanEquals),
+ [OPERATORS.before]: formatMessage(labels.before),
+ [OPERATORS.after]: formatMessage(labels.after),
+ };
+
+ const typeFilters = {
+ string: [OPERATORS.equals, OPERATORS.notEquals, OPERATORS.contains, OPERATORS.doesNotContain],
+ array: [OPERATORS.contains, OPERATORS.doesNotContain],
+ boolean: [OPERATORS.true, OPERATORS.false],
+ number: [
+ OPERATORS.equals,
+ OPERATORS.notEquals,
+ OPERATORS.greaterThan,
+ OPERATORS.lessThan,
+ OPERATORS.greaterThanEquals,
+ OPERATORS.lessThanEquals,
+ ],
+ date: [OPERATORS.before, OPERATORS.after],
+ uuid: [OPERATORS.equals],
+ };
+
+ const filters = Object.keys(query).reduce((arr, key) => {
+ if (FILTER_COLUMNS[key]) {
+ let operator = 'eq';
+ let value = safeDecodeURIComponent(query[key]);
+ const label = fields.find(({ name }) => name === key)?.label;
+
+ const match = value.match(/^([a-z]+)\.(.*)/);
+
+ if (match) {
+ operator = match[1];
+ value = match[2];
+ }
+
+ return arr.concat({
+ name: key,
+ operator,
+ value,
+ label,
+ });
+ }
+ return arr;
+ }, []);
+
+ const getFilters = (type: string) => {
+ return (
+ typeFilters[type]?.map((key: string | number) => ({
+ type,
+ value: key,
+ label: operatorLabels[key],
+ })) ?? []
+ );
+ };
+
+ return { fields, operators, filters, operatorLabels, typeFilters, getFilters };
+}
diff --git a/src/components/hooks/useForceUpdate.ts b/src/components/hooks/useForceUpdate.ts
new file mode 100644
index 0000000..550cc5c
--- /dev/null
+++ b/src/components/hooks/useForceUpdate.ts
@@ -0,0 +1,9 @@
+import { useCallback, useState } from 'react';
+
+export function useForceUpdate() {
+ const [, update] = useState(Object.create(null));
+
+ return useCallback(() => {
+ update(Object.create(null));
+ }, [update]);
+}
diff --git a/src/components/hooks/useFormat.ts b/src/components/hooks/useFormat.ts
new file mode 100644
index 0000000..896fa07
--- /dev/null
+++ b/src/components/hooks/useFormat.ts
@@ -0,0 +1,74 @@
+import { BROWSERS, OS_NAMES } from '@/lib/constants';
+import regions from '../../../public/iso-3166-2.json';
+import { useCountryNames } from './useCountryNames';
+import { useLanguageNames } from './useLanguageNames';
+import { useLocale } from './useLocale';
+import { useMessages } from './useMessages';
+
+export function useFormat() {
+ const { formatMessage, labels } = useMessages();
+ const { locale } = useLocale();
+ const { countryNames } = useCountryNames(locale);
+ const { languageNames } = useLanguageNames(locale);
+
+ const formatOS = (value: string): string => {
+ return OS_NAMES[value] || value;
+ };
+
+ const formatBrowser = (value: string): string => {
+ return BROWSERS[value] || value;
+ };
+
+ const formatDevice = (value: string): string => {
+ return formatMessage(labels[value] || labels.unknown);
+ };
+
+ const formatCountry = (value: string): string => {
+ return countryNames[value] || value;
+ };
+
+ const formatRegion = (value?: string): string => {
+ const [country] = value?.split('-') || [];
+ return regions[value] ? `${regions[value]}, ${countryNames[country]}` : value;
+ };
+
+ const formatCity = (value: string, country?: string): string => {
+ return countryNames[country] ? `${value}, ${countryNames[country]}` : value;
+ };
+
+ const formatLanguage = (value: string): string => {
+ return languageNames[value?.split('-')[0]] || value;
+ };
+
+ const formatValue = (value: string, type: string, data?: Record<string, any>): string => {
+ switch (type) {
+ case 'os':
+ return formatOS(value);
+ case 'browser':
+ return formatBrowser(value);
+ case 'device':
+ return formatDevice(value);
+ case 'country':
+ return formatCountry(value);
+ case 'region':
+ return formatRegion(value);
+ case 'city':
+ return formatCity(value, data?.country);
+ case 'language':
+ return formatLanguage(value);
+ default:
+ return typeof value === 'string' ? value : undefined;
+ }
+ };
+
+ return {
+ formatOS,
+ formatBrowser,
+ formatDevice,
+ formatCountry,
+ formatRegion,
+ formatCity,
+ formatLanguage,
+ formatValue,
+ };
+}
diff --git a/src/components/hooks/useGlobalState.ts b/src/components/hooks/useGlobalState.ts
new file mode 100644
index 0000000..6f21226
--- /dev/null
+++ b/src/components/hooks/useGlobalState.ts
@@ -0,0 +1,13 @@
+import { create } from 'zustand';
+
+const store = create(() => ({}));
+
+const useGlobalState = (key: string, value?: any) => {
+ if (value !== undefined && store.getState()[key] === undefined) {
+ store.setState({ [key]: value });
+ }
+
+ return [store(state => state[key]), (value: any) => store.setState({ [key]: value })];
+};
+
+export { useGlobalState };
diff --git a/src/components/hooks/useLanguageNames.ts b/src/components/hooks/useLanguageNames.ts
new file mode 100644
index 0000000..0cc03d7
--- /dev/null
+++ b/src/components/hooks/useLanguageNames.ts
@@ -0,0 +1,32 @@
+import { useEffect, useState } from 'react';
+import { httpGet } from '@/lib/fetch';
+import enUS from '../../../public/intl/language/en-US.json';
+
+const languageNames = {
+ 'en-US': enUS,
+};
+
+export function useLanguageNames(locale) {
+ const [list, setList] = useState(languageNames[locale] || enUS);
+
+ async function loadData(locale) {
+ const { data } = await httpGet(`${process.env.basePath || ''}/intl/language/${locale}.json`);
+
+ if (data) {
+ languageNames[locale] = data;
+ setList(languageNames[locale]);
+ } else {
+ setList(enUS);
+ }
+ }
+
+ useEffect(() => {
+ if (!languageNames[locale]) {
+ loadData(locale);
+ } else {
+ setList(languageNames[locale]);
+ }
+ }, [locale]);
+
+ return { languageNames: list };
+}
diff --git a/src/components/hooks/useLocale.ts b/src/components/hooks/useLocale.ts
new file mode 100644
index 0000000..3eb669e
--- /dev/null
+++ b/src/components/hooks/useLocale.ts
@@ -0,0 +1,60 @@
+import { useEffect } from 'react';
+import { LOCALE_CONFIG } from '@/lib/constants';
+import { httpGet } from '@/lib/fetch';
+import { getDateLocale, getTextDirection } from '@/lib/lang';
+import { setItem } from '@/lib/storage';
+import { setLocale, useApp } from '@/store/app';
+import enUS from '../../../public/intl/country/en-US.json';
+import { useForceUpdate } from './useForceUpdate';
+
+const messages = {
+ 'en-US': enUS,
+};
+
+const selector = (state: { locale: string }) => state.locale;
+
+export function useLocale() {
+ const locale = useApp(selector);
+ const forceUpdate = useForceUpdate();
+ const dir = getTextDirection(locale);
+ const dateLocale = getDateLocale(locale);
+
+ async function loadMessages(locale: string) {
+ const { data } = await httpGet(`${process.env.basePath || ''}/intl/messages/${locale}.json`);
+
+ messages[locale] = data;
+ }
+
+ async function saveLocale(value: string) {
+ if (!messages[value]) {
+ await loadMessages(value);
+ }
+
+ setItem(LOCALE_CONFIG, value);
+
+ document.getElementById('__next')?.setAttribute('dir', getTextDirection(value));
+
+ if (locale !== value) {
+ setLocale(value);
+ } else {
+ forceUpdate();
+ }
+ }
+
+ useEffect(() => {
+ if (!messages[locale]) {
+ saveLocale(locale);
+ }
+ }, [locale]);
+
+ useEffect(() => {
+ const url = new URL(window?.location?.href);
+ const locale = url.searchParams.get('locale');
+
+ if (locale) {
+ saveLocale(locale);
+ }
+ }, []);
+
+ return { locale, saveLocale, messages, dir, dateLocale };
+}
diff --git a/src/components/hooks/useMessages.ts b/src/components/hooks/useMessages.ts
new file mode 100644
index 0000000..d5bc242
--- /dev/null
+++ b/src/components/hooks/useMessages.ts
@@ -0,0 +1,48 @@
+import { FormattedMessage, type MessageDescriptor, useIntl } from 'react-intl';
+import { labels, messages } from '@/components/messages';
+import type { ApiError } from '@/lib/types';
+
+type FormatMessage = (
+ descriptor: MessageDescriptor,
+ values?: Record<string, string | number | boolean | null | undefined>,
+ opts?: any,
+) => string | null;
+
+interface UseMessages {
+ formatMessage: FormatMessage;
+ messages: typeof messages;
+ labels: typeof labels;
+ getMessage: (id: string) => string;
+ getErrorMessage: (error: ApiError) => string | undefined;
+ FormattedMessage: typeof FormattedMessage;
+}
+
+export function useMessages(): UseMessages {
+ const intl = useIntl();
+
+ const getMessage = (id: string) => {
+ const message = Object.values(messages).find(value => value.id === `message.${id}`);
+
+ return message ? formatMessage(message) : id;
+ };
+
+ const getErrorMessage = (error: ApiError) => {
+ if (!error) {
+ return undefined;
+ }
+
+ const code = error?.code;
+
+ return code ? getMessage(code) : error?.message || 'Unknown error';
+ };
+
+ const formatMessage = (
+ descriptor: MessageDescriptor,
+ values?: Record<string, string | number | boolean | null | undefined>,
+ opts?: any,
+ ) => {
+ return descriptor ? intl.formatMessage(descriptor, values, opts) : null;
+ };
+
+ return { formatMessage, messages, labels, getMessage, getErrorMessage, FormattedMessage };
+}
diff --git a/src/components/hooks/useMobile.ts b/src/components/hooks/useMobile.ts
new file mode 100644
index 0000000..6b40f3d
--- /dev/null
+++ b/src/components/hooks/useMobile.ts
@@ -0,0 +1,9 @@
+import { useBreakpoint } from '@umami/react-zen';
+
+export function useMobile() {
+ const breakpoint = useBreakpoint();
+ const isMobile = ['xs', 'sm', 'md'].includes(breakpoint);
+ const isPhone = ['xs', 'sm'].includes(breakpoint);
+
+ return { breakpoint, isMobile, isPhone };
+}
diff --git a/src/components/hooks/useModified.ts b/src/components/hooks/useModified.ts
new file mode 100644
index 0000000..ea88888
--- /dev/null
+++ b/src/components/hooks/useModified.ts
@@ -0,0 +1,13 @@
+import { create } from 'zustand';
+
+const store = create(() => ({}));
+
+export function touch(key: string) {
+ store.setState({ [key]: Date.now() });
+}
+
+export function useModified(key?: string) {
+ const modified = store(state => state?.[key]);
+
+ return { modified, touch };
+}
diff --git a/src/components/hooks/useNavigation.ts b/src/components/hooks/useNavigation.ts
new file mode 100644
index 0000000..0a18ac7
--- /dev/null
+++ b/src/components/hooks/useNavigation.ts
@@ -0,0 +1,43 @@
+import { usePathname, useRouter, useSearchParams } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { buildPath } from '@/lib/url';
+
+export function useNavigation() {
+ const router = useRouter();
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+ const [, teamId] = pathname.match(/\/teams\/([a-f0-9-]+)/) || [];
+ const [, websiteId] = pathname.match(/\/websites\/([a-f0-9-]+)/) || [];
+ const [queryParams, setQueryParams] = useState(Object.fromEntries(searchParams));
+
+ const updateParams = (params?: Record<string, string | number>) => {
+ return buildPath(pathname, { ...queryParams, ...params });
+ };
+
+ const replaceParams = (params?: Record<string, string | number>) => {
+ return buildPath(pathname, params);
+ };
+
+ const renderUrl = (path: string, params?: Record<string, string | number> | false) => {
+ return buildPath(
+ teamId ? `/teams/${teamId}${path}` : path,
+ params === false ? {} : { ...queryParams, ...params },
+ );
+ };
+
+ useEffect(() => {
+ setQueryParams(Object.fromEntries(searchParams));
+ }, [searchParams.toString()]);
+
+ return {
+ router,
+ pathname,
+ searchParams,
+ query: queryParams,
+ teamId,
+ websiteId,
+ updateParams,
+ replaceParams,
+ renderUrl,
+ };
+}
diff --git a/src/components/hooks/usePageParameters.ts b/src/components/hooks/usePageParameters.ts
new file mode 100644
index 0000000..42cf391
--- /dev/null
+++ b/src/components/hooks/usePageParameters.ts
@@ -0,0 +1,16 @@
+import { useMemo } from 'react';
+import { useNavigation } from './useNavigation';
+
+export function usePageParameters() {
+ const {
+ query: { page, pageSize, search },
+ } = useNavigation();
+
+ return useMemo(() => {
+ return {
+ page,
+ pageSize,
+ search,
+ };
+ }, [page, pageSize, search]);
+}
diff --git a/src/components/hooks/usePagedQuery.ts b/src/components/hooks/usePagedQuery.ts
new file mode 100644
index 0000000..c818de6
--- /dev/null
+++ b/src/components/hooks/usePagedQuery.ts
@@ -0,0 +1,27 @@
+import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
+import type { PageResult } from '@/lib/types';
+import { useApi } from './useApi';
+import { useNavigation } from './useNavigation';
+
+export function usePagedQuery<TData = any, TError = Error>({
+ queryKey,
+ queryFn,
+ ...options
+}: Omit<
+ UseQueryOptions<PageResult<TData>, TError, PageResult<TData>, readonly unknown[]>,
+ 'queryFn' | 'queryKey'
+> & {
+ queryKey: readonly unknown[];
+ queryFn: (params?: object) => Promise<PageResult<TData>> | PageResult<TData>;
+}): UseQueryResult<PageResult<TData>, TError> {
+ const {
+ query: { page, search },
+ } = useNavigation();
+ const { useQuery } = useApi();
+
+ return useQuery<PageResult<TData>, TError>({
+ queryKey: [...queryKey, page, search] as const,
+ queryFn: () => queryFn({ page, search }),
+ ...options,
+ });
+}
diff --git a/src/components/hooks/useRegionNames.ts b/src/components/hooks/useRegionNames.ts
new file mode 100644
index 0000000..57dcc41
--- /dev/null
+++ b/src/components/hooks/useRegionNames.ts
@@ -0,0 +1,22 @@
+import regions from '../../../public/iso-3166-2.json';
+import { useCountryNames } from './useCountryNames';
+
+export function useRegionNames(locale: string) {
+ const { countryNames } = useCountryNames(locale);
+
+ const getRegionName = (regionCode: string, countryCode?: string) => {
+ if (!countryCode) {
+ return regions[regionCode];
+ }
+
+ if (!regionCode) {
+ return null;
+ }
+
+ const region = regionCode?.includes('-') ? regionCode : `${countryCode}-${regionCode}`;
+
+ return regions[region] ? `${regions[region]}, ${countryNames[countryCode]}` : region;
+ };
+
+ return { regionNames: regions, getRegionName };
+}
diff --git a/src/components/hooks/useSlug.ts b/src/components/hooks/useSlug.ts
new file mode 100644
index 0000000..f795dfe
--- /dev/null
+++ b/src/components/hooks/useSlug.ts
@@ -0,0 +1,14 @@
+import { useConfig } from '@/components/hooks/useConfig';
+import { LINKS_URL, PIXELS_URL } from '@/lib/constants';
+
+export function useSlug(type: 'link' | 'pixel') {
+ const { linksUrl, pixelsUrl } = useConfig();
+
+ const hostUrl = type === 'link' ? linksUrl || LINKS_URL : pixelsUrl || PIXELS_URL;
+
+ const getSlugUrl = (slug: string) => {
+ return `${hostUrl}/${slug}`;
+ };
+
+ return { getSlugUrl, hostUrl };
+}
diff --git a/src/components/hooks/useSticky.ts b/src/components/hooks/useSticky.ts
new file mode 100644
index 0000000..ef9fb36
--- /dev/null
+++ b/src/components/hooks/useSticky.ts
@@ -0,0 +1,25 @@
+import { useEffect, useRef, useState } from 'react';
+
+export function useSticky({ enabled = true, threshold = 1 }) {
+ const [isSticky, setIsSticky] = useState(false);
+ const ref = useRef(null);
+
+ useEffect(() => {
+ let observer: IntersectionObserver | undefined;
+ // eslint-disable-next-line no-undef
+ const handler: IntersectionObserverCallback = ([entry]) =>
+ setIsSticky(entry.intersectionRatio < threshold);
+
+ if (enabled && ref.current) {
+ observer = new IntersectionObserver(handler, { threshold: [threshold] });
+ observer.observe(ref.current);
+ }
+ return () => {
+ if (observer) {
+ observer.disconnect();
+ }
+ };
+ }, [ref, enabled, threshold]);
+
+ return { ref, isSticky };
+}
diff --git a/src/components/hooks/useTimezone.ts b/src/components/hooks/useTimezone.ts
new file mode 100644
index 0000000..ef25539
--- /dev/null
+++ b/src/components/hooks/useTimezone.ts
@@ -0,0 +1,95 @@
+import { formatInTimeZone, utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz';
+import { TIMEZONE_CONFIG, TIMEZONE_LEGACY } from '@/lib/constants';
+import { getTimezone } from '@/lib/date';
+import { setItem } from '@/lib/storage';
+import { setTimezone, useApp } from '@/store/app';
+import { useLocale } from './useLocale';
+
+const selector = (state: { timezone: string }) => state.timezone;
+
+export function useTimezone() {
+ const timezone = useApp(selector);
+ const localTimeZone = getTimezone();
+ const { dateLocale } = useLocale();
+
+ const saveTimezone = (value: string) => {
+ setItem(TIMEZONE_CONFIG, value);
+ setTimezone(value);
+ };
+
+ const formatTimezoneDate = (date: string, pattern: string) => {
+ return formatInTimeZone(
+ /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3})?Z$/.test(date)
+ ? date
+ : `${date.split(' ').join('T')}Z`,
+ timezone,
+ pattern,
+ { locale: dateLocale },
+ );
+ };
+
+ const formatSeriesTimezone = (data: any, column: string, timezone: string) => {
+ return data.map(item => {
+ const date = new Date(item[column]);
+
+ const format = new Intl.DateTimeFormat('en-US', {
+ timeZone: timezone,
+ hour12: false,
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ });
+
+ const parts = format.formatToParts(date);
+ const get = type => parts.find(p => p.type === type)?.value;
+
+ const year = get('year');
+ const month = get('month');
+ const day = get('day');
+ const hour = get('hour');
+ const minute = get('minute');
+ const second = get('second');
+
+ return {
+ ...item,
+ [column]: `${year}-${month}-${day} ${hour}:${minute}:${second}`,
+ };
+ });
+ };
+
+ const toUtc = (date: Date | string | number) => {
+ return zonedTimeToUtc(date, timezone);
+ };
+
+ const fromUtc = (date: Date | string | number) => {
+ return utcToZonedTime(date, timezone);
+ };
+
+ const localToUtc = (date: Date | string | number) => {
+ return zonedTimeToUtc(date, localTimeZone);
+ };
+
+ const localFromUtc = (date: Date | string | number) => {
+ return utcToZonedTime(date, localTimeZone);
+ };
+
+ const canonicalizeTimezone = (timezone: string): string => {
+ return TIMEZONE_LEGACY[timezone] ?? timezone;
+ };
+
+ return {
+ timezone,
+ localTimeZone,
+ toUtc,
+ fromUtc,
+ localToUtc,
+ localFromUtc,
+ saveTimezone,
+ formatTimezoneDate,
+ formatSeriesTimezone,
+ canonicalizeTimezone,
+ };
+}