From a1f3ad89f67f8069c33d31b034f091349b554841 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 12 Feb 2026 17:01:37 -0800 Subject: [PATCH] Add link and pixel selectors to top nav --- src/app/(main)/TopNav.tsx | 56 ++++++++--- src/components/hooks/queries/useLinksQuery.ts | 13 ++- .../hooks/queries/usePixelsQuery.ts | 13 ++- src/components/hooks/useNavigation.ts | 4 + src/components/input/LinkSelect.tsx | 93 +++++++++++++++++++ src/components/input/PixelSelect.tsx | 93 +++++++++++++++++++ 6 files changed, 255 insertions(+), 17 deletions(-) create mode 100644 src/components/input/LinkSelect.tsx create mode 100644 src/components/input/PixelSelect.tsx diff --git a/src/app/(main)/TopNav.tsx b/src/app/(main)/TopNav.tsx index 759a576da..d93f590e9 100644 --- a/src/app/(main)/TopNav.tsx +++ b/src/app/(main)/TopNav.tsx @@ -2,16 +2,26 @@ import { Icon, Row } from '@umami/react-zen'; import { useNavigation } from '@/components/hooks'; import { Slash } from '@/components/icons'; +import { LinkSelect } from '@/components/input/LinkSelect'; +import { PixelSelect } from '@/components/input/PixelSelect'; import { TeamsButton } from '@/components/input/TeamsButton'; import { WebsiteSelect } from '@/components/input/WebsiteSelect'; export function TopNav() { - const { websiteId, teamId, router, renderUrl } = useNavigation(); + const { websiteId, linkId, pixelId, teamId, router, renderUrl } = useNavigation(); const handleWebsiteChange = (value: string) => { router.push(renderUrl(`/websites/${value}`)); }; + const handleLinkChange = (value: string) => { + router.push(renderUrl(`/links/${value}`)); + }; + + const handlePixelChange = (value: string) => { + router.push(renderUrl(`/pixels/${value}`)); + }; + return ( - {websiteId && ( + {(websiteId || linkId || pixelId) && ( <> - + {websiteId && ( + + )} + {linkId && ( + + )} + {pixelId && ( + + )} )} diff --git a/src/components/hooks/queries/useLinksQuery.ts b/src/components/hooks/queries/useLinksQuery.ts index ebf945fbe..8345fcfeb 100644 --- a/src/components/hooks/queries/useLinksQuery.ts +++ b/src/components/hooks/queries/useLinksQuery.ts @@ -3,14 +3,21 @@ import { useApi } from '../useApi'; import { useModified } from '../useModified'; import { usePagedQuery } from '../usePagedQuery'; -export function useLinksQuery({ teamId }: { teamId?: string }, options?: ReactQueryOptions) { +export function useLinksQuery( + { teamId }: { teamId?: string }, + params?: Record, + options?: ReactQueryOptions, +) { const { modified } = useModified('links'); const { get } = useApi(); return usePagedQuery({ - queryKey: ['links', { teamId, modified }], + queryKey: ['links', { teamId, modified, ...params }], queryFn: pageParams => { - return get(teamId ? `/teams/${teamId}/links` : '/links', pageParams); + return get(teamId ? `/teams/${teamId}/links` : '/links', { + ...pageParams, + ...params, + }); }, ...options, }); diff --git a/src/components/hooks/queries/usePixelsQuery.ts b/src/components/hooks/queries/usePixelsQuery.ts index c431179bb..2eadcba98 100644 --- a/src/components/hooks/queries/usePixelsQuery.ts +++ b/src/components/hooks/queries/usePixelsQuery.ts @@ -3,14 +3,21 @@ import { useApi } from '../useApi'; import { useModified } from '../useModified'; import { usePagedQuery } from '../usePagedQuery'; -export function usePixelsQuery({ teamId }: { teamId?: string }, options?: ReactQueryOptions) { +export function usePixelsQuery( + { teamId }: { teamId?: string }, + params?: Record, + options?: ReactQueryOptions, +) { const { modified } = useModified('pixels'); const { get } = useApi(); return usePagedQuery({ - queryKey: ['pixels', { teamId, modified }], + queryKey: ['pixels', { teamId, modified, ...params }], queryFn: pageParams => { - return get(teamId ? `/teams/${teamId}/pixels` : '/pixels', pageParams); + return get(teamId ? `/teams/${teamId}/pixels` : '/pixels', { + ...pageParams, + ...params, + }); }, ...options, }); diff --git a/src/components/hooks/useNavigation.ts b/src/components/hooks/useNavigation.ts index 0a18ac7b2..6480c8ecf 100644 --- a/src/components/hooks/useNavigation.ts +++ b/src/components/hooks/useNavigation.ts @@ -8,6 +8,8 @@ export function useNavigation() { const searchParams = useSearchParams(); const [, teamId] = pathname.match(/\/teams\/([a-f0-9-]+)/) || []; const [, websiteId] = pathname.match(/\/websites\/([a-f0-9-]+)/) || []; + const [, linkId] = pathname.match(/\/links\/([a-f0-9-]+)/) || []; + const [, pixelId] = pathname.match(/\/pixels\/([a-f0-9-]+)/) || []; const [queryParams, setQueryParams] = useState(Object.fromEntries(searchParams)); const updateParams = (params?: Record) => { @@ -36,6 +38,8 @@ export function useNavigation() { query: queryParams, teamId, websiteId, + linkId, + pixelId, updateParams, replaceParams, renderUrl, diff --git a/src/components/input/LinkSelect.tsx b/src/components/input/LinkSelect.tsx new file mode 100644 index 000000000..665a1a94f --- /dev/null +++ b/src/components/input/LinkSelect.tsx @@ -0,0 +1,93 @@ +import { Icon, ListItem, Row, Select, type SelectProps, Text } from '@umami/react-zen'; +import { useEffect, useState } from 'react'; +import { Empty } from '@/components/common/Empty'; +import { useLinkQuery, useLinksQuery, useMessages } from '@/components/hooks'; +import { Link } from '@/components/icons'; + +export function LinkSelect({ + linkId, + teamId, + onChange, + isCollapsed, + buttonProps, + listProps, + ...props +}: { + linkId?: string; + teamId?: string; + isCollapsed?: boolean; +} & SelectProps) { + const { t, messages } = useMessages(); + const { data: link } = useLinkQuery(linkId); + const [name, setName] = useState(link?.name); + const [search, setSearch] = useState(''); + const { data, isLoading } = useLinksQuery({ teamId }, { search, pageSize: 20 }); + const listItems: { id: string; name: string }[] = data?.data || []; + + useEffect(() => { + setName(link?.name); + }, [link?.name]); + + const handleOpenChange = () => { + setSearch(''); + }; + + const handleChange = (id: string) => { + setName(listItems.find(item => item.id === id)?.name); + onChange?.(id); + }; + + const renderValue = () => { + if (isCollapsed) { + return ''; + } + + return ( + + + + + {name} + + ); + }; + + return ( + + ); +} diff --git a/src/components/input/PixelSelect.tsx b/src/components/input/PixelSelect.tsx new file mode 100644 index 000000000..b12c5aa46 --- /dev/null +++ b/src/components/input/PixelSelect.tsx @@ -0,0 +1,93 @@ +import { Icon, ListItem, Row, Select, type SelectProps, Text } from '@umami/react-zen'; +import { useEffect, useState } from 'react'; +import { Empty } from '@/components/common/Empty'; +import { useMessages, usePixelQuery, usePixelsQuery } from '@/components/hooks'; +import { Grid2x2 } from '@/components/icons'; + +export function PixelSelect({ + pixelId, + teamId, + onChange, + isCollapsed, + buttonProps, + listProps, + ...props +}: { + pixelId?: string; + teamId?: string; + isCollapsed?: boolean; +} & SelectProps) { + const { t, messages } = useMessages(); + const { data: pixel } = usePixelQuery(pixelId); + const [name, setName] = useState(pixel?.name); + const [search, setSearch] = useState(''); + const { data, isLoading } = usePixelsQuery({ teamId }, { search, pageSize: 20 }); + const listItems: { id: string; name: string }[] = data?.data || []; + + useEffect(() => { + setName(pixel?.name); + }, [pixel?.name]); + + const handleOpenChange = () => { + setSearch(''); + }; + + const handleChange = (id: string) => { + setName(listItems.find(item => item.id === id)?.name); + onChange?.(id); + }; + + const renderValue = () => { + if (isCollapsed) { + return ''; + } + + return ( + + + + + {name} + + ); + }; + + return ( + + ); +}