diff options
| author | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
| commit | 396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b (patch) | |
| tree | b9df4ca6a70db45cfffbae6fdd7252e20fb8e93c /src/components/hooks | |
| download | umami-main.tar.xz umami-main.zip | |
Created from https://vercel.com/new
Diffstat (limited to 'src/components/hooks')
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, + }; +} |