Skip to content
Merged
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
7 changes: 7 additions & 0 deletions backend/src/schema/books/resolvers/Query/searchBooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
sql,
type SQL,
} from "drizzle-orm";
import { GraphQLError } from "graphql/error";
import { db } from "~/db";
import { bookGenres, books, bookToGenres } from "~/db/schema";
import type { QueryResolvers } from "~/schema/types.generated";
Expand All @@ -19,6 +20,12 @@ export const searchBooks: NonNullable<QueryResolvers["searchBooks"]> = async (
_parent,
args,
) => {
if (args.limit < 0 || args.offset < 0) {
throw new GraphQLError("Limit and offset cannot be negative.", {
extensions: { code: "BAD_USER_INPUT" },
});
}

const conditions: SQL[] = [];

if (args.title && args.fuzzy) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export const favoriteBooks: NonNullable<
);
}

if (arg.limit < 0 || arg.offset < 0) {
throw new GraphQLError("Limit and offset cannot be negative.", {
extensions: { code: "BAD_USER_INPUT" },
});
}

const [foundBooks, totalCount] = await Promise.all([
db.query.favoriteBooks.findMany({
where: {
Expand Down
12 changes: 12 additions & 0 deletions frontend/app/components/BookPagination/BookPagination.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,16 @@ describe("BookPagination", () => {
screen.getByRole("link", { name: "Go to next page" }),
).toHaveAttribute("href", "/?q=blade&genres=Fantasy&page=3");
});

it.each(["abc", "0", "-2", "1.5"])(
"treats page=%s as the first page",
(page) => {
render(<Stub initialEntries={[`/?page=${page}`]} />);

expect(currentPage()).toBe("1");
expect(
screen.getByRole("link", { name: "Go to previous page" }),
).toHaveAttribute("aria-disabled", "true");
},
);
});
3 changes: 2 additions & 1 deletion frontend/app/components/BookPagination/BookPagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import {
PaginationNext,
PaginationPrevious,
} from "~/components/ui/Pagination";
import { parsePageNum } from "~/hooks/useBookSearchParams";

const DISABLED_STYLE = "pointer-events-none opacity-50";

function BookPagination({ totalPages }: { totalPages: number }) {
const [searchParams] = useSearchParams();
const currentPage = parseInt(searchParams.get("page") ?? "1");
const currentPage = parsePageNum(searchParams.get("page"));

const isFirstPage = currentPage === 1;
const isLastPage = currentPage === totalPages;
Expand Down
9 changes: 7 additions & 2 deletions frontend/app/hooks/useBookSearchParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ function parseCount(value: string | null) {
return value && Number.isInteger(count) && count >= 0 ? count : undefined;
}

function parsePageNum(value: string | null) {
const page = Number(value);
return Number.isInteger(page) && page >= 1 ? page : 1;
}

const FILTER_KEYS = [
"genres",
"language",
Expand All @@ -21,7 +26,7 @@ function useBookSearchParams() {
const [searchParams] = useSearchParams();

const query = searchParams.get("q") ?? "";
const page = parseInt(searchParams.get("page") ?? "1", 10);
const page = parsePageNum(searchParams.get("page"));
const sort = parseBookSort(searchParams.get("sort"));

// Only update filters when searchParams change to avoid unnecessary re-renders
Expand All @@ -44,4 +49,4 @@ function useBookSearchParams() {
return { query, page, filters, sort };
}

export { FILTER_KEYS, useBookSearchParams };
export { FILTER_KEYS, parsePageNum, useBookSearchParams };
3 changes: 2 additions & 1 deletion frontend/app/routes/profile.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Link, Navigate, useSearchParams } from "react-router";
import { authClient } from "~/api/auth";
import { useFavoriteBooks } from "~/hooks/useFavoriteBooks";
import { parsePageNum } from "~/hooks/useBookSearchParams";
import { BookCard } from "~/components/BookCard/BookCard";
import { BookPagination } from "~/components/BookPagination/BookPagination";
import { PageBreadcrumb } from "~/components/PageBreadcrumb/PageBreadcrumb";
Expand All @@ -25,7 +26,7 @@ export function meta() {
export default function ProfilePage() {
const { data: session, isPending } = authClient.useSession();
const [searchParams] = useSearchParams();
const page = parseInt(searchParams.get("page") ?? "1", 10);
const page = parsePageNum(searchParams.get("page"));

const { books, totalCount, topGenres, loading, error } = useFavoriteBooks({
enabled: !!session,
Expand Down