diff --git a/frontend/app/components/FavoriteButton/FavoriteButton.test.tsx b/frontend/app/components/FavoriteButton/FavoriteButton.test.tsx index 039faa34..1a31ef90 100644 --- a/frontend/app/components/FavoriteButton/FavoriteButton.test.tsx +++ b/frontend/app/components/FavoriteButton/FavoriteButton.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { screen } from "@testing-library/react"; +import { screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { authClient } from "~/api/auth"; import { FavoriteButton } from "~/components/FavoriteButton/FavoriteButton"; @@ -71,7 +71,6 @@ describe("FavoriteButton", () => { data: { addFavoriteBook: { __typename: "Book", id: 1 } }, }, }, - favoriteIds([1]), ], }); @@ -82,12 +81,11 @@ describe("FavoriteButton", () => { }), ); - expect( - await screen.findByRole("button", { - name: "Favorite The Blade Itself", - pressed: true, - }), - ).toHaveAttribute("aria-pressed", "true"); + await waitFor(() => + expect( + screen.getByRole("button", { name: "Favorite The Blade Itself" }), + ).not.toBeDisabled(), + ); }); it("removes a favorite book from the user's favorites", async () => { diff --git a/frontend/app/components/books/BookFilters/BookFilters.tsx b/frontend/app/components/books/BookFilters/BookFilters.tsx index 790a7bbc..c1b48ce1 100644 --- a/frontend/app/components/books/BookFilters/BookFilters.tsx +++ b/frontend/app/components/books/BookFilters/BookFilters.tsx @@ -1,6 +1,5 @@ -import { Button } from "~/components/ui/Button"; import { useSearchParams } from "react-router"; -import { FILTER_KEYS, useBookSearchParams } from "~/hooks/useBookSearchParams"; +import { useBookSearchParams } from "~/hooks/useBookSearchParams"; import { useBookFilters } from "~/hooks/useBookFilters"; import { Combobox, @@ -24,162 +23,129 @@ function BookFilters() { const { genres, languages, bookFormats } = useBookFilters(); const genresAnchor = useComboboxAnchor(); - const hasActiveFilters = Object.values(filters).some(Boolean); - - const clearFilters = () => { - setSearchParams((searchParams) => { - FILTER_KEYS.forEach((key) => searchParams.delete(key)); - - // Reset the page to 1 when clearing filters. - // A missing "page" search param means page 1 - searchParams.delete("page"); - - return searchParams; - }); - }; - return ( -
-
-

- Filter Books -

- -
- - -
+ + + No genres found. + + {(genre: string) => ( + + {genre} + + )} + + + + +
  • + + { + setSearchParams((searchParams) => { + if (language) { + searchParams.set("language", language); + } else { + searchParams.delete("language"); + } + searchParams.delete("page"); + return searchParams; + }); + }} + > + + + No languages found. + + {(language: string) => ( + + {language} + + )} + + + +
  • +
  • + + { + setSearchParams((searchParams) => { + if (bookFormat) { + searchParams.set("bookFormat", bookFormat); + } else { + searchParams.delete("bookFormat"); + } + searchParams.delete("page"); + return searchParams; + }); + }} + > + + + No book formats found. + + {(bookFormat: string) => ( + + {bookFormat} + + )} + + + +
  • +
  • + +
  • +
  • + +
  • + ); } diff --git a/frontend/app/components/books/SearchBar/SearchBar.tsx b/frontend/app/components/books/SearchBar/SearchBar.tsx index 6225530e..dfd84fdd 100644 --- a/frontend/app/components/books/SearchBar/SearchBar.tsx +++ b/frontend/app/components/books/SearchBar/SearchBar.tsx @@ -11,13 +11,19 @@ import { import { Button } from "~/components/ui/Button"; import { Badge } from "~/components/ui/Badge"; import { - Popover, - PopoverContent, - PopoverTrigger, -} from "~/components/ui/Popover"; + Drawer, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerTrigger, + DrawerDescription, + DrawerFooter, +} from "~/components/ui/Drawer"; import { useSearchParams } from "react-router"; import { useDebounce } from "~/hooks/useDebounce"; -import { useBookSearchParams } from "~/hooks/useBookSearchParams"; +import { FILTER_KEYS, useBookSearchParams } from "~/hooks/useBookSearchParams"; +import { useIsMobile } from "~/hooks/useIsMobile"; const SEARCH_DEBOUNCE_MS = 500; @@ -33,6 +39,8 @@ function SearchBar({ resultCount }: SearchBarProps) { const debouncedQuery = useDebounce(inputField, SEARCH_DEBOUNCE_MS); const newQueryFromInputRef = useRef(false); + const isMobile = useIsMobile(); + useEffect(() => { if (debouncedQuery !== inputField || debouncedQuery === query) { return; @@ -63,6 +71,18 @@ function SearchBar({ resultCount }: SearchBarProps) { (value) => value !== undefined, ).length; + const clearFilters = () => { + setSearchParams((searchParams) => { + FILTER_KEYS.forEach((key) => searchParams.delete(key)); + + // Reset the page to 1 when clearing filters. + // A missing "page" search param means page 1 + searchParams.delete("page"); + + return searchParams; + }); + }; + const filtersDescription = `${filtersCount} ${filtersCount === 1 ? "filter" : "filters"} active`; return ( @@ -94,8 +114,11 @@ function SearchBar({ resultCount }: SearchBarProps) { )} - - + )} - - - - - + + + + + Filter books + + + +
    + +
    + + + + {filtersDescription} + + + + {resultCount !== undefined + ? `Show ${resultCount} ${resultCount === 1 ? "result" : "results"}` + : "Show results"} + + } + /> + +
    + ); } diff --git a/frontend/app/components/ui/Drawer.tsx b/frontend/app/components/ui/Drawer.tsx new file mode 100644 index 00000000..c777cdb7 --- /dev/null +++ b/frontend/app/components/ui/Drawer.tsx @@ -0,0 +1,225 @@ +import * as React from "react"; +import { Drawer as DrawerPrimitive } from "@base-ui/react/drawer"; +import { cn } from "cn"; + +interface DrawerContextProps { + hasSnapPoints: boolean; + modal: DrawerPrimitive.Root.Props["modal"]; + showSwipeHandle: boolean; + swipeDirection: NonNullable; +} + +const DrawerContext = React.createContext(null); + +function useDrawer() { + const context = React.use(DrawerContext); + + if (!context) { + throw new Error("useDrawer must be used within a Drawer."); + } + + return context; +} + +function Drawer({ + modal = true, + showSwipeHandle = false, + snapPoints, + swipeDirection = "down", + ...props +}: DrawerPrimitive.Root.Props & { + showSwipeHandle?: boolean; +}) { + const hasSnapPoints = snapPoints != null && snapPoints.length > 0; + const contextValue = React.useMemo( + () => ({ hasSnapPoints, modal, showSwipeHandle, swipeDirection }), + [hasSnapPoints, modal, showSwipeHandle, swipeDirection], + ); + + return ( + + + + ); +} + +function DrawerTrigger({ ...props }: DrawerPrimitive.Trigger.Props) { + return ; +} + +function DrawerPortal({ ...props }: DrawerPrimitive.Portal.Props) { + return ; +} + +function DrawerClose({ ...props }: DrawerPrimitive.Close.Props) { + return ; +} + +function DrawerOverlay({ + className, + ...props +}: DrawerPrimitive.Backdrop.Props) { + return ( + + ); +} + +function DrawerSwipeHandle({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +