Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions frontend/app/components/FavoriteButton/FavoriteButton.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -71,7 +71,6 @@ describe("FavoriteButton", () => {
data: { addFavoriteBook: { __typename: "Book", id: 1 } },
},
},
favoriteIds([1]),
],
});

Expand All @@ -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 () => {
Expand Down
276 changes: 121 additions & 155 deletions frontend/app/components/books/BookFilters/BookFilters.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 (
<section className="flex flex-col gap-4">
<header className="flex items-center justify-between">
<h2 id="filter-books-heading" className="text-lg font-bold">
Filter Books
</h2>
<Button
type="button"
variant="link"
onClick={clearFilters}
disabled={!hasActiveFilters}
className="text-foreground hover:text-foreground/85"
<ul className="flex flex-col gap-4">
<li className="flex flex-col gap-2">
<label htmlFor="genres" className="flex items-center gap-2">
Genres
</label>
<Combobox
multiple
items={genres}
value={filters.genres ?? []}
onValueChange={(selected) => {
setSearchParams((searchParams) => {
searchParams.delete("genres");
selected.forEach((genre) => searchParams.append("genres", genre));
searchParams.delete("page");
return searchParams;
});
}}
>
Reset
</Button>
</header>

<ul className="flex flex-col gap-4">
<li className="flex flex-col gap-2">
<label htmlFor="genres" className="flex items-center gap-2">
Genres
</label>
<Combobox
multiple
items={genres}
value={filters.genres ?? []}
onValueChange={(selected) => {
setSearchParams((searchParams) => {
searchParams.delete("genres");
selected.forEach((genre) =>
searchParams.append("genres", genre),
);
searchParams.delete("page");
return searchParams;
});
}}
>
<ComboboxChips ref={genresAnchor}>
<ComboboxValue>
{(filters.genres ?? []).map((genre) => (
<ComboboxChip key={genre}>{genre}</ComboboxChip>
))}
</ComboboxValue>
<ComboboxChipsInput
id="genres"
placeholder={
filters.genres?.length ? undefined : "Search genres..."
}
/>
</ComboboxChips>
<ComboboxContent anchor={genresAnchor}>
<ComboboxEmpty>No genres found.</ComboboxEmpty>
<ComboboxList>
{(genre: string) => (
<ComboboxItem key={genre} value={genre}>
{genre}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</li>
<li className="flex flex-col gap-2">
<label htmlFor="language" className="flex items-center gap-2">
Language
</label>
<Combobox
items={languages}
value={filters.language ?? null}
onValueChange={(language: string | null) => {
setSearchParams((searchParams) => {
if (language) {
searchParams.set("language", language);
} else {
searchParams.delete("language");
}
searchParams.delete("page");
return searchParams;
});
}}
>
<ComboboxInput
id="language"
placeholder="Search languages..."
showClear
/>
<ComboboxContent>
<ComboboxEmpty>No languages found.</ComboboxEmpty>
<ComboboxList>
{(language: string) => (
<ComboboxItem key={language} value={language}>
{language}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</li>
<li className="flex flex-col gap-2">
<label htmlFor="bookFormat" className="flex items-center gap-2">
Book format
</label>
<Combobox
items={bookFormats}
value={filters.bookFormat ?? null}
onValueChange={(bookFormat: string | null) => {
setSearchParams((searchParams) => {
if (bookFormat) {
searchParams.set("bookFormat", bookFormat);
} else {
searchParams.delete("bookFormat");
}
searchParams.delete("page");
return searchParams;
});
}}
>
<ComboboxInput
id="bookFormat"
placeholder="Search book formats..."
showClear
<ComboboxChips ref={genresAnchor}>
<ComboboxValue>
{(filters.genres ?? []).map((genre) => (
<ComboboxChip key={genre}>{genre}</ComboboxChip>
))}
</ComboboxValue>
<ComboboxChipsInput
id="genres"
placeholder={
filters.genres?.length ? undefined : "Search genres..."
}
/>
<ComboboxContent>
<ComboboxEmpty>No book formats found.</ComboboxEmpty>
<ComboboxList>
{(bookFormat: string) => (
<ComboboxItem key={bookFormat} value={bookFormat}>
{bookFormat}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</li>
<li className="flex flex-col gap-2">
<MinRatingsFilter />
</li>
<li>
<PagesFilter />
</li>
</ul>
</section>
</ComboboxChips>
<ComboboxContent anchor={genresAnchor}>
<ComboboxEmpty>No genres found.</ComboboxEmpty>
<ComboboxList>
{(genre: string) => (
<ComboboxItem key={genre} value={genre}>
{genre}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</li>
<li className="flex flex-col gap-2">
<label htmlFor="language" className="flex items-center gap-2">
Language
</label>
<Combobox
items={languages}
value={filters.language ?? null}
onValueChange={(language: string | null) => {
setSearchParams((searchParams) => {
if (language) {
searchParams.set("language", language);
} else {
searchParams.delete("language");
}
searchParams.delete("page");
return searchParams;
});
}}
>
<ComboboxInput
id="language"
placeholder="Search languages..."
showClear
/>
<ComboboxContent>
<ComboboxEmpty>No languages found.</ComboboxEmpty>
<ComboboxList>
{(language: string) => (
<ComboboxItem key={language} value={language}>
{language}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</li>
<li className="flex flex-col gap-2">
<label htmlFor="bookFormat" className="flex items-center gap-2">
Book format
</label>
<Combobox
items={bookFormats}
value={filters.bookFormat ?? null}
onValueChange={(bookFormat: string | null) => {
setSearchParams((searchParams) => {
if (bookFormat) {
searchParams.set("bookFormat", bookFormat);
} else {
searchParams.delete("bookFormat");
}
searchParams.delete("page");
return searchParams;
});
}}
>
<ComboboxInput
id="bookFormat"
placeholder="Search book formats..."
showClear
/>
<ComboboxContent>
<ComboboxEmpty>No book formats found.</ComboboxEmpty>
<ComboboxList>
{(bookFormat: string) => (
<ComboboxItem key={bookFormat} value={bookFormat}>
{bookFormat}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</li>
<li className="flex flex-col gap-2">
<MinRatingsFilter />
</li>
<li>
<PagesFilter />
</li>
</ul>
);
}

Expand Down
Loading