From 1b9301892992fb747fae66fab2f576c8def731b6 Mon Sep 17 00:00:00 2001 From: Simo Date: Thu, 15 Jan 2026 13:35:43 +0100 Subject: [PATCH] wip Signed-off-by: Simo --- .../header/HeaderSearchModal.test.tsx | 2 +- .../header-search/HeaderSearchButton.test.tsx | 59 +-- .../HeaderSearchModalFocus.test.tsx | 4 +- components/header/AppHeader.tsx | 50 ++- .../header-search/HeaderSearchButton.tsx | 25 +- .../header-search/HeaderSearchModal.tsx | 401 ++++++++++++++---- components/layout/SmallScreenHeader.tsx | 13 +- components/layout/sidebar/WebSidebarNav.tsx | 33 +- hooks/useWaveDropsSearch.ts | 32 +- 9 files changed, 436 insertions(+), 183 deletions(-) diff --git a/__tests__/components/header/HeaderSearchModal.test.tsx b/__tests__/components/header/HeaderSearchModal.test.tsx index 8b2e337fde..7e64672f89 100644 --- a/__tests__/components/header/HeaderSearchModal.test.tsx +++ b/__tests__/components/header/HeaderSearchModal.test.tsx @@ -207,7 +207,7 @@ function setup(options: SetupOptions = {}) { }; }); } - render(); + render(); return { onClose, push, profilesRefetch, nftsRefetch, wavesRefetch }; } diff --git a/__tests__/components/header/header-search/HeaderSearchButton.test.tsx b/__tests__/components/header/header-search/HeaderSearchButton.test.tsx index 15ce5e48e0..f3295907f4 100644 --- a/__tests__/components/header/header-search/HeaderSearchButton.test.tsx +++ b/__tests__/components/header/header-search/HeaderSearchButton.test.tsx @@ -1,82 +1,83 @@ -import React from 'react'; -import { render, screen, fireEvent, act } from '@testing-library/react'; -import HeaderSearchButton from '@/components/header/header-search/HeaderSearchButton'; -import useDeviceInfo from '@/hooks/useDeviceInfo'; +import React from "react"; +import { render, screen, fireEvent, act } from "@testing-library/react"; +import HeaderSearchButton from "@/components/header/header-search/HeaderSearchButton"; +import useDeviceInfo from "@/hooks/useDeviceInfo"; let keyFilter: (e: KeyboardEvent) => boolean; let keyCb: () => void; -jest.mock('react-use', () => ({ +jest.mock("react-use", () => ({ useKey: (filter: (e: KeyboardEvent) => boolean, cb: () => void) => { keyFilter = filter; keyCb = cb; }, })); -jest.mock('@/components/utils/animation/CommonAnimationWrapper', () => ({ +jest.mock("@/components/utils/animation/CommonAnimationWrapper", () => ({ __esModule: true, default: ({ children }: any) =>
{children}
, })); -jest.mock('@/components/utils/animation/CommonAnimationOpacity', () => ({ +jest.mock("@/components/utils/animation/CommonAnimationOpacity", () => ({ __esModule: true, default: ({ children, ...props }: any) =>
{children}
, })); -jest.mock('@/components/header/header-search/HeaderSearchModal', () => ({ +jest.mock("@/components/header/header-search/HeaderSearchModal", () => ({ __esModule: true, default: (props: any) => (
props.onClose()}>
), })); -jest.mock('@heroicons/react/24/outline', () => ({ +jest.mock("@heroicons/react/24/outline", () => ({ MagnifyingGlassIcon: (props: any) => , })); -jest.mock('@/hooks/useDeviceInfo'); +jest.mock("@/hooks/useDeviceInfo"); -const useDeviceInfoMock = useDeviceInfo as jest.MockedFunction; +const useDeviceInfoMock = useDeviceInfo as jest.MockedFunction< + typeof useDeviceInfo +>; -describe('HeaderSearchButton', () => { +describe("HeaderSearchButton", () => { beforeEach(() => { jest.clearAllMocks(); keyFilter = () => false; keyCb = () => {}; }); - it('opens modal when button is clicked and closes via onClose', () => { + it("opens modal when button is clicked and closes via onClose", () => { useDeviceInfoMock.mockReturnValue({ isApp: false } as any); - render(); - expect(screen.queryByTestId('modal')).toBeNull(); + render(); + expect(screen.queryByTestId("modal")).toBeNull(); - fireEvent.click(screen.getByRole('button', { name: /search/i })); - expect(screen.getByTestId('modal')).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: /search/i })); + expect(screen.getByTestId("modal")).toBeInTheDocument(); - fireEvent.click(screen.getByTestId('modal')); - expect(screen.queryByTestId('modal')).toBeNull(); + fireEvent.click(screen.getByTestId("modal")); + expect(screen.queryByTestId("modal")).toBeNull(); }); - it('opens modal when meta+k is pressed', () => { + it("opens modal when meta+k is pressed", () => { useDeviceInfoMock.mockReturnValue({ isApp: false } as any); - render(); - expect(screen.queryByTestId('modal')).toBeNull(); + render(); + expect(screen.queryByTestId("modal")).toBeNull(); - const event = new KeyboardEvent('keydown', { key: 'k', metaKey: true }); + const event = new KeyboardEvent("keydown", { key: "k", metaKey: true }); if (keyFilter(event)) { act(() => { keyCb(); }); } - expect(screen.getByTestId('modal')).toBeInTheDocument(); + expect(screen.getByTestId("modal")).toBeInTheDocument(); }); - it('uses larger icon when app mode is true', () => { + it("uses larger icon when app mode is true", () => { useDeviceInfoMock.mockReturnValue({ isApp: true } as any); - render(); - const icon = screen.getByTestId('icon'); - expect(icon).toHaveClass('tw-h-6 tw-w-6'); + render(); + const icon = screen.getByTestId("icon"); + expect(icon).toHaveClass("tw-h-6 tw-w-6"); }); }); - diff --git a/__tests__/components/header/header-search/HeaderSearchModalFocus.test.tsx b/__tests__/components/header/header-search/HeaderSearchModalFocus.test.tsx index bb7b010fbd..608161d1c3 100644 --- a/__tests__/components/header/header-search/HeaderSearchModalFocus.test.tsx +++ b/__tests__/components/header/header-search/HeaderSearchModalFocus.test.tsx @@ -152,7 +152,7 @@ const PLACEHOLDER_TEXT = "Search 6529.io"; describe("HeaderSearchModal focus management", () => { it("keeps focus trapped within the modal while it is open", async () => { const user = userEvent.setup(); - render(); + render(); const trigger = screen.getByRole("button", { name: /search/i }); await user.click(trigger); @@ -183,7 +183,7 @@ describe("HeaderSearchModal focus management", () => { it("returns focus to the trigger button when the modal closes", async () => { const user = userEvent.setup(); - render(); + render(); const trigger = screen.getByRole("button", { name: /search/i }); await user.click(trigger); diff --git a/components/header/AppHeader.tsx b/components/header/AppHeader.tsx index 877bc14e2d..0a1e58a6c4 100644 --- a/components/header/AppHeader.tsx +++ b/components/header/AppHeader.tsx @@ -1,11 +1,13 @@ "use client"; -import { capitalizeEveryWord, formatAddress } from "@/helpers/Helpers"; import { resolveIpfsUrlSync } from "@/components/ipfs/IPFSContext"; -import Image from "next/image"; +import { useNavigationHistoryContext } from "@/contexts/NavigationHistoryContext"; +import { useMyStreamOptional } from "@/contexts/wave/MyStreamContext"; +import { capitalizeEveryWord, formatAddress } from "@/helpers/Helpers"; import { useIdentity } from "@/hooks/useIdentity"; import { useWaveById } from "@/hooks/useWaveById"; import { Bars3Icon } from "@heroicons/react/24/outline"; +import Image from "next/image"; import { useParams, usePathname } from "next/navigation"; import { useState } from "react"; import { useAuth } from "../auth/Auth"; @@ -15,16 +17,12 @@ import Spinner from "../utils/Spinner"; import AppSidebar from "./AppSidebar"; import HeaderSearchButton from "./header-search/HeaderSearchButton"; import HeaderActionButtons from "./HeaderActionButtons"; -import { useMyStreamOptional } from "@/contexts/wave/MyStreamContext"; -import { useNavigationHistoryContext } from "@/contexts/NavigationHistoryContext"; - - const COLLECTION_TITLES: Record = { "the-memes": "The Memes", "6529-gradient": "6529 Gradient", "meme-lab": "Meme Lab", - "nextgen": "NextGen", + nextgen: "NextGen", }; const sliceString = (str: string, length: number): string => { @@ -33,7 +31,10 @@ const sliceString = (str: string, length: number): string => { return `${str.slice(0, half)}...${str.slice(-half)}`; }; -const getCollectionTitle = (basePath: string, pageTitle: string): string | null => { +const getCollectionTitle = ( + basePath: string, + pageTitle: string +): string | null => { const prefix = COLLECTION_TITLES[basePath]; if (prefix && !Number.isNaN(Number(pageTitle))) { return `${prefix} #${pageTitle}`; @@ -69,7 +70,7 @@ export default function AppHeader() { return profile?.pfp ?? null; })(); - const pathSegments = (pathname ?? "").split("/").filter(Boolean); + const pathSegments = pathname.split("/").filter(Boolean); const basePath = pathSegments.length ? pathSegments[0] : ""; const pageTitle = pathSegments.length ? pathSegments[pathSegments.length - 1] @@ -87,7 +88,7 @@ export default function AppHeader() { pathname === "/waves/create" || pathname === "/messages/create"; const isInsideWave = !!waveId; - const isProfilePage = typeof params?.["user"] === "string"; + const isProfilePage = typeof params["user"] === "string"; const showBackButton = isInsideWave || isCreateRoute || (isProfilePage && canGoBack); @@ -99,7 +100,7 @@ export default function AppHeader() { if (isMessagesRoute && !waveId) return "Messages"; if (waveId) { if (isLoading || isFetching || wave?.id !== waveId) return ; - return wave?.name ?? "Wave"; + return wave.name; } const collectionTitle = getCollectionTitle(basePath!, pageTitle!); @@ -112,19 +113,20 @@ export default function AppHeader() { })(); return ( -
-
+
+
{showBackButton && } {!showBackButton && ( )} -
+
{finalTitle}
- +
setMenuOpen(false)} /> diff --git a/components/header/header-search/HeaderSearchButton.tsx b/components/header/header-search/HeaderSearchButton.tsx index 7036c63bd1..b7246dd24e 100644 --- a/components/header/header-search/HeaderSearchButton.tsx +++ b/components/header/header-search/HeaderSearchButton.tsx @@ -8,8 +8,13 @@ import CommonAnimationOpacity from "@/components/utils/animation/CommonAnimation import HeaderSearchModal from "./HeaderSearchModal"; import { useKey } from "react-use"; import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import type { ApiWave } from "@/generated/models/ApiWave"; -export default function HeaderSearchButton() { +interface HeaderSearchButtonProps { + readonly wave: ApiWave | null; +} + +export default function HeaderSearchButton({ wave }: HeaderSearchButtonProps) { const [isOpen, setIsOpen] = useState(false); const buttonRef = useRef(null); const wasOpenRef = useRef(false); @@ -36,11 +41,9 @@ export default function HeaderSearchButton() { const handleOpen = () => setIsOpen(true); const handleClose = () => setIsOpen(false); - useKey( - (event) => event.metaKey && event.key === "k", - handleOpen, - { event: "keydown" } - ); + useKey((event) => event.metaKey && event.key === "k", handleOpen, { + event: "keydown", + }); const iconSizeClasses = isApp ? "tw-h-6 tw-w-6" : "tw-h-5 tw-w-5"; @@ -53,11 +56,12 @@ export default function HeaderSearchButton() { title="Search" onClick={handleOpen} className={clsx( - "tw-flex tw-items-center tw-justify-center tw-rounded-lg tw-h-10 tw-w-10 tw-border-0 tw-text-iron-300 hover:tw-text-iron-50 tw-shadow-sm focus-visible:tw-outline focus-visible:tw-outline-2 focus-visible:tw-outline-primary-400 tw-transition tw-duration-300 tw-ease-out", + "tw-flex tw-h-10 tw-w-10 tw-items-center tw-justify-center tw-rounded-lg tw-border-0 tw-text-iron-300 tw-shadow-sm tw-transition tw-duration-300 tw-ease-out hover:tw-text-iron-50 focus-visible:tw-outline focus-visible:tw-outline-2 focus-visible:tw-outline-primary-400", isApp ? "tw-bg-black" : "tw-bg-iron-800 tw-ring-1 tw-ring-inset tw-ring-iron-700 hover:tw-bg-iron-700" - )}> + )} + > @@ -68,8 +72,9 @@ export default function HeaderSearchButton() { key="modal" elementClasses="tw-absolute tw-z-10" elementRole="dialog" - onClicked={(e) => e.stopPropagation()}> - + onClicked={(e) => e.stopPropagation()} + > + )} diff --git a/components/header/header-search/HeaderSearchModal.tsx b/components/header/header-search/HeaderSearchModal.tsx index e2cefa51aa..62f2117cc7 100644 --- a/components/header/header-search/HeaderSearchModal.tsx +++ b/components/header/header-search/HeaderSearchModal.tsx @@ -22,6 +22,8 @@ import { type SidebarPageEntry, } from "@/hooks/useSidebarSections"; import { useWaves } from "@/hooks/useWaves"; +import { useWaveDropsSearch } from "@/hooks/useWaveDropsSearch"; +import { useWaveChatScrollOptional } from "@/contexts/wave/WaveChatScrollContext"; import { commonApiFetch } from "@/services/api/common-api"; import { ChevronLeftIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { useQuery } from "@tanstack/react-query"; @@ -33,11 +35,13 @@ import { useClickAway, useDebounce, useKeyPressEvent } from "react-use"; import type { HeaderSearchModalItemType, NFTSearchResult, - PageSearchResult} from "./HeaderSearchModalItem"; + PageSearchResult, +} from "./HeaderSearchModalItem"; import HeaderSearchModalItem, { - getNftCollectionMap + getNftCollectionMap, } from "./HeaderSearchModalItem"; import { HeaderSearchTabToggle } from "./HeaderSearchTabToggle"; +import Drop, { DropLocation } from "@/components/waves/drops/Drop"; enum STATE { INITIAL = "INITIAL", @@ -47,6 +51,11 @@ enum STATE { SUCCESS = "SUCCESS", } +enum SEARCH_MODE { + WAVE = "WAVE", + SITE = "SITE", +} + enum CATEGORY { ALL = "ALL", PROFILES = "PROFILES", @@ -148,8 +157,10 @@ const pageMatchesQuery = ( export default function HeaderSearchModal({ onClose, + wave, }: { readonly onClose: () => void; + readonly wave: ApiWave | null; }) { const router = useRouter(); const pathname = usePathname(); @@ -159,6 +170,11 @@ export default function HeaderSearchModal({ useClickAway(modalRef, onClose); useKeyPressEvent("Escape", onClose); + const waveChatScroll = useWaveChatScrollOptional(); + const [searchMode, setSearchMode] = useState( + wave ? SEARCH_MODE.WAVE : SEARCH_MODE.SITE + ); + const [searchValue, setSearchValue] = useState(""); const [selectedCategory, setSelectedCategory] = useLocalPreference( "headerSearchCategoryFilter", @@ -189,6 +205,37 @@ export default function HeaderSearchModal({ !Number.isNaN(Number(trimmedDebouncedValue))); const hasActiveDebouncedSearch = shouldSearchNfts; + // Wave search (shorter debounce, lower min length) + const WAVE_SEARCH_MIN_LENGTH = 2; + const [waveSearchDebouncedValue, setWaveSearchDebouncedValue] = + useState(""); + useDebounce( + () => { + setWaveSearchDebouncedValue(searchValue); + }, + 250, + [searchValue] + ); + const trimmedWaveSearchValue = waveSearchDebouncedValue.trim(); + const shouldSearchWave = + wave !== null && + searchMode === SEARCH_MODE.WAVE && + trimmedWaveSearchValue.length >= WAVE_SEARCH_MIN_LENGTH; + + const { + drops: waveDropResults, + isLoading: isLoadingWaveDrops, + isError: isWaveDropsError, + hasNextPage: waveDropsHasNextPage, + fetchNextPage: fetchNextWaveDropsPage, + isFetchingNextPage: isFetchingNextWaveDropsPage, + } = useWaveDropsSearch({ + wave, + term: trimmedWaveSearchValue, + enabled: shouldSearchWave, + size: 50, + }); + const { appWalletsSupported } = useAppWallets(); const { country } = useCookieConsent(); const capacitor = useCapacitor(); @@ -402,13 +449,13 @@ export default function HeaderSearchModal({ () => shouldSearchDefault && (selectedCategory === CATEGORY.PROFILES || allowProfileFetch) - ? profiles ?? [] + ? (profiles ?? []) : [], [shouldSearchDefault, selectedCategory, allowProfileFetch, profiles] ); const nftResults: NFTSearchResult[] = useMemo( - () => (shouldSearchNfts ? nfts ?? [] : []), + () => (shouldSearchNfts ? (nfts ?? []) : []), [shouldSearchNfts, nfts] ); @@ -416,7 +463,7 @@ export default function HeaderSearchModal({ () => shouldSearchDefault && (selectedCategory === CATEGORY.WAVES || allowWaveFetch) - ? waves ?? [] + ? (waves ?? []) : [], [shouldSearchDefault, selectedCategory, allowWaveFetch, waves] ); @@ -582,6 +629,7 @@ export default function HeaderSearchModal({ const handleClearSearch = () => { setSearchValue(""); setDebouncedValue(""); + setWaveSearchDebouncedValue(""); setSelectedCategory(CATEGORY.ALL); setSelectedItemIndex(0); setAllowProfileFetch(false); @@ -592,6 +640,18 @@ export default function HeaderSearchModal({ }, 0); }; + const handleWaveDropSelect = (serialNo: number) => { + if (!wave) return; + if (waveChatScroll) { + waveChatScroll.requestScrollToSerialNo({ waveId: wave.id, serialNo }); + } else { + const params = new URLSearchParams(searchParams?.toString() || ""); + params.set("serialNo", String(serialNo)); + router.replace(`${pathname}?${params.toString()}`, { scroll: false }); + } + onClose(); + }; + const onHover = (index: number, state: boolean) => { if (!state) return; setSelectedItemIndex(index); @@ -797,7 +857,7 @@ export default function HeaderSearchModal({ return `nft:${item.contract}:${item.id}`; } if (isProfileResult(item)) { - const base = (item.profile_id ?? item.wallet ?? "profile").toLowerCase(); + const base = (item.profile_id ?? item.wallet).toLowerCase(); return `profile:${base}`; } if (isWaveResult(item)) { @@ -809,7 +869,8 @@ export default function HeaderSearchModal({ const renderItem = (item: HeaderSearchModalItemType, index: number) => (
+ key={getItemKey(item)} + > (
-
+

{CATEGORY_LABELS[group.category]}

@@ -840,7 +901,8 @@ export default function HeaderSearchModal({ )} @@ -871,22 +933,25 @@ export default function HeaderSearchModal({ (inputRef.current as HTMLElement | null) ?? (modalRef.current as HTMLElement | null) ?? document.body, - }}> -
+ }} + > +
-
+
-
+ className="inset-safe-area tw-relative tw-flex tw-h-[520px] tw-max-h-[70vh] tw-min-h-0 tw-w-full tw-max-w-[min(100vw-3rem,900px)] tw-transform tw-flex-col tw-overflow-hidden tw-rounded-xl tw-bg-iron-950 tw-text-left tw-shadow-xl tw-transition-all tw-duration-500 sm:tw-max-w-3xl" + > +
@@ -895,7 +960,8 @@ export default function HeaderSearchModal({ className="tw-pointer-events-none tw-absolute tw-left-4 tw-top-3.5 tw-h-5 tw-w-5 tw-text-iron-300" viewBox="0 0 20 20" fill="currentColor" - aria-hidden="true"> + aria-hidden="true" + > {searchValue.length > 0 && ( )} @@ -931,82 +1002,224 @@ export default function HeaderSearchModal({ type="button" onClick={onClose} aria-label="Close search" - className="tw-hidden sm:tw-inline-flex tw-h-9 tw-w-9 tw-items-center tw-justify-center tw-rounded-full tw-border tw-border-iron-700 tw-bg-iron-900 tw-text-iron-300 tw-border-solid hover:tw-border-iron-500 hover:tw-bg-iron-800 hover:tw-text-white tw-transition tw-duration-150"> + className="tw-hidden tw-h-9 tw-w-9 tw-items-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-iron-700 tw-bg-iron-900 tw-text-iron-300 tw-transition tw-duration-150 hover:tw-border-iron-500 hover:tw-bg-iron-800 hover:tw-text-white sm:tw-inline-flex" + >
- {shouldRenderCategoryToggle && ( -
- setSelectedCategory(k as CATEGORY)} - fullWidth - /> + {wave && ( +
+
+ + +
)} + {searchMode === SEARCH_MODE.SITE && + shouldRenderCategoryToggle && ( +
+ setSelectedCategory(k as CATEGORY)} + fullWidth + /> +
+ )}
- {shouldRenderCategoryToggle && ( - - )} -
- {state === STATE.SUCCESS && ( -
- {renderSuccessContent()} -
+ }`} + > + {searchMode === SEARCH_MODE.SITE && + shouldRenderCategoryToggle && ( + )} - {(state === STATE.LOADING || - (state === STATE.INITIAL && isSearching)) && ( +
+ {/* Wave search results */} + {searchMode === SEARCH_MODE.WAVE && wave && (
-

- Loading... -

+ className="tw-h-0 tw-min-h-0 tw-flex-1 tw-overflow-y-auto tw-px-4 tw-pb-6 tw-scrollbar-thin tw-scrollbar-track-iron-800 tw-scrollbar-thumb-iron-500 desktop-hover:hover:tw-scrollbar-thumb-iron-300" + > + {isLoadingWaveDrops && ( +
+ Loading… +
+ )} + + {!isLoadingWaveDrops && isWaveDropsError && ( +
+ Couldn't load search results. +
+ )} + + {!isLoadingWaveDrops && + !isWaveDropsError && + !shouldSearchWave && ( +
+ Type at least 2 characters to search in {wave.name}. +
+ )} + + {!isLoadingWaveDrops && + !isWaveDropsError && + shouldSearchWave && + waveDropResults.length === 0 && ( +
+ No matches found. +
+ )} + + {!isLoadingWaveDrops && + !isWaveDropsError && + shouldSearchWave && + waveDropResults.length > 0 && ( +
+
+ {waveDropResults.length} result + {waveDropResults.length === 1 ? "" : "s"} +
+
+ {waveDropResults.map((drop, index) => { + const previousDrop = + waveDropResults[index - 1] ?? null; + const nextDrop = + waveDropResults[index + 1] ?? null; + const serialNo = drop.serial_no; + const canSelect = typeof serialNo === "number"; + return ( + + ); + })} +
+ {waveDropsHasNextPage && ( +
+ +
+ )} +
+ )}
)} - {state === STATE.NO_RESULTS && ( + {/* Site-wide search results */} + {searchMode === SEARCH_MODE.SITE && + state === STATE.SUCCESS && ( +
+ {renderSuccessContent()} +
+ )} + {searchMode === SEARCH_MODE.SITE && + (state === STATE.LOADING || + (state === STATE.INITIAL && isSearching)) && ( +
+

+ Loading... +

+
+ )} + {searchMode === SEARCH_MODE.SITE && + state === STATE.NO_RESULTS && ( +
+

+ No results found +

+
+ )} + {searchMode === SEARCH_MODE.SITE && state === STATE.ERROR && (
-

- No results found -

-
- )} - {state === STATE.ERROR && ( -
+ className="tw-flex tw-h-0 tw-min-h-0 tw-flex-1 tw-flex-col tw-items-center tw-justify-center tw-gap-3 tw-px-4 tw-text-center md:tw-px-0" + >

+ className="tw-text-sm tw-font-normal tw-text-iron-300" + aria-live="polite" + > Something went wrong while searching. Please try again.

)} - {state === STATE.INITIAL && !isSearching && ( -
-

- Start typing to search 6529.io - {shouldShowCountdown && - ` (${charactersRemaining} more character${ - charactersRemaining === 1 ? "" : "s" - })`} -

-
- )} + {searchMode === SEARCH_MODE.SITE && + state === STATE.INITIAL && + !isSearching && ( +
+

+ Start typing to search 6529.io + {shouldShowCountdown && + ` (${charactersRemaining} more character${ + charactersRemaining === 1 ? "" : "s" + })`} +

+
+ )}
diff --git a/components/layout/SmallScreenHeader.tsx b/components/layout/SmallScreenHeader.tsx index 03eea4d445..d26ebdabc1 100644 --- a/components/layout/SmallScreenHeader.tsx +++ b/components/layout/SmallScreenHeader.tsx @@ -15,8 +15,8 @@ export default function SmallScreenHeader({ isMenuOpen, }: SmallScreenHeaderProps) { return ( -
-
+
+
- +
diff --git a/components/layout/sidebar/WebSidebarNav.tsx b/components/layout/sidebar/WebSidebarNav.tsx index 62a7ea5115..2036acdce3 100644 --- a/components/layout/sidebar/WebSidebarNav.tsx +++ b/components/layout/sidebar/WebSidebarNav.tsx @@ -58,7 +58,9 @@ const WebSidebarNav = React.forwardRef< top: number; height: number; } | null>(null); - const [submenuTrigger, setSubmenuTrigger] = useState(null); + const [submenuTrigger, setSubmenuTrigger] = useState( + null + ); useKey( (event) => event.metaKey && event.key === "k", @@ -208,16 +210,24 @@ const WebSidebarNav = React.forwardRef< return null; }, - [isCollapsed, openSubmenuKey, sections, pathname, closeSubmenu, submenuAnchor] + [ + isCollapsed, + openSubmenuKey, + sections, + pathname, + closeSubmenu, + submenuAnchor, + submenuTrigger, + ] ); return ( <> @@ -348,11 +353,13 @@ const WebSidebarNav = React.forwardRef< elementRole="dialog" onClicked={(event) => event.stopPropagation()} > - setIsSearchOpen(false)} /> + setIsSearchOpen(false)} + wave={null} + /> )} - ); }); diff --git a/hooks/useWaveDropsSearch.ts b/hooks/useWaveDropsSearch.ts index e0a2052252..6679842895 100644 --- a/hooks/useWaveDropsSearch.ts +++ b/hooks/useWaveDropsSearch.ts @@ -5,7 +5,10 @@ import type { ApiDropWithoutWavesPageWithoutCount } from "@/generated/models/Api import type { ApiWave } from "@/generated/models/ApiWave"; import type { ApiWaveMin } from "@/generated/models/ApiWaveMin"; import type { ExtendedDrop } from "@/helpers/waves/drop.helpers"; -import { generateUniqueKeys, mapToExtendedDrops } from "@/helpers/waves/wave-drops.helpers"; +import { + generateUniqueKeys, + mapToExtendedDrops, +} from "@/helpers/waves/wave-drops.helpers"; import { commonApiFetch } from "@/services/api/common-api"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useMemo } from "react"; @@ -15,8 +18,9 @@ const toWaveMin = (wave: ApiWave): ApiWaveMin => { id: wave.id, name: wave.name, picture: wave.picture, - description_drop_id: wave.description_drop?.id ?? "", - authenticated_user_eligible_to_vote: wave.voting.authenticated_user_eligible, + description_drop_id: wave.description_drop.id, + authenticated_user_eligible_to_vote: + wave.voting.authenticated_user_eligible, authenticated_user_eligible_to_participate: wave.participation.authenticated_user_eligible, authenticated_user_eligible_to_chat: wave.chat.authenticated_user_eligible, @@ -41,24 +45,29 @@ export function useWaveDropsSearch({ enabled, size = 50, }: { - readonly wave: ApiWave; + readonly wave: ApiWave | null; readonly term: string; readonly enabled: boolean; readonly size?: number | undefined; }) { const trimmedTerm = term.trim(); - const waveMin = useMemo(() => toWaveMin(wave), [wave]); + const waveMin = useMemo(() => (wave ? toWaveMin(wave) : null), [wave]); const query = useInfiniteQuery({ queryKey: [ QueryKey.DROPS, - { waveId: wave.id, term: trimmedTerm, size, context: "wave-search" }, + { + waveId: wave?.id ?? null, + term: trimmedTerm, + size, + context: "wave-search", + }, ], - enabled: enabled && trimmedTerm.length > 0, + enabled: enabled && wave !== null && trimmedTerm.length > 0, initialPageParam: 1, queryFn: async ({ pageParam }) => { return await commonApiFetch({ - endpoint: `waves/${wave.id}/search`, + endpoint: `waves/${wave!.id}/search`, params: { term: trimmedTerm, page: String(pageParam), @@ -72,9 +81,14 @@ export function useWaveDropsSearch({ }); const drops = useMemo(() => { + if (!waveMin) return []; const all = query.data?.pages.flatMap((page) => page.data) ?? []; if (all.length === 0) return []; - const mapped = mapToExtendedDrops([{ wave: waveMin, drops: all }], [], false); + const mapped = mapToExtendedDrops( + [{ wave: waveMin, drops: all }], + [], + false + ); return generateUniqueKeys(mapped, []); }, [query.data?.pages, waveMin]);