Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
37a64e2
feat!: restructure to use multiple pages and search params
martakam Sep 30, 2026
d6f4a63
fix: adjust look of BookCard
martakam Sep 30, 2026
efe3be2
test: remove "works" prefix from openLibrary tests
martakam Sep 30, 2026
1c62ee2
chore: remove unused files
martakam Sep 30, 2026
1b352e8
test: check for new linking in BookCard
martakam Sep 30, 2026
a30092b
test: new pagination in search params
martakam Sep 30, 2026
2b6a41c
test: search bar query from URL
martakam Sep 30, 2026
2ad7e6c
test: adjust to new SelectedBookPage with search params
martakam Sep 30, 2026
4f42bff
test: adjust HomePage test to use URL search params
martakam Sep 30, 2026
0aa02d4
Merge branch 'main' into feat/multi-page-structure
martakam Sep 30, 2026
b120f4a
fix: do not remove search params when clicking back button
martakam Oct 1, 2026
143aeb2
fix: remove button in BookCard and style link directly
martakam Oct 1, 2026
8218894
fix: do not set query to empty string
martakam Oct 1, 2026
1198d67
fix: style back and next book buttons correctly
martakam Oct 1, 2026
7579660
fix: use filters correctly when selecting a book
martakam Oct 1, 2026
2716088
feat: show loading book details info text with proper styling
martakam Oct 1, 2026
73a4caa
fix: use debounce for book filters
martakam Oct 1, 2026
bc18083
fix: remove duplicate language codes
martakam Oct 1, 2026
3619b09
fix: always include searchParams even when link is disabled
martakam Oct 2, 2026
998f638
fix: always use "search" for searchParams in Link components
martakam Oct 2, 2026
5cbc4f1
fix: use new searchParams from URL immediately on URL change
martakam Oct 2, 2026
ce53c81
test: adjust to new button name and update snapshot
martakam Oct 2, 2026
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
8 changes: 4 additions & 4 deletions app/api/openLibrary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe("toBook", () => {
};

expect(toBook(doc)).toEqual({
id: "/works/OL8400950W",
id: "OL8400950W",
title: "The Blade Itself",
author: ["Joe Abercrombie"],
publishedYear: 2001,
Expand Down Expand Up @@ -140,7 +140,7 @@ describe("fetchWork", () => {
.spyOn(globalThis, "fetch")
.mockResolvedValue(Response.json(workResponse));

const data = await fetchWork("/works/OL8400950W");
const data = await fetchWork("OL8400950W");
const url = fetchMock.mock.calls[0][0] as URL;
expect(url.toString()).toBe(
"https://openlibrary.org/works/OL8400950W.json",
Expand All @@ -154,8 +154,8 @@ describe("fetchWork", () => {
new Response(null, { status: 429 }),
);

await expect(fetchWork("/works/OL8400950W")).rejects.toThrow(
"Failed to fetch work /works/OL8400950W with status 429",
await expect(fetchWork("OL8400950W")).rejects.toThrow(
"Failed to fetch work OL8400950W with status 429",
);
});
});
4 changes: 2 additions & 2 deletions app/api/openLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async function searchBooks(

function toBook(doc: OpenLibraryDoc): Book {
return {
id: doc.key,
id: doc.key.replace("/works/", ""),
title: doc.title,
author: doc.author_name ?? ["Unknown author"],
publishedYear: doc.first_publish_year,
Expand All @@ -78,7 +78,7 @@ function toBook(doc: OpenLibraryDoc): Book {
}

async function fetchWork(key: string): Promise<OpenLibraryWork> {
const response = await fetch(`${BASE_URL}${key}.json`);
const response = await fetch(`${BASE_URL}/works/${key}.json`);

if (!response.ok) {
throw new Error(
Expand Down
2 changes: 1 addition & 1 deletion app/api/useBookDescription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { fetchWork, toDescription } from "~/api/openLibrary";

function useBookDescription(key: string) {
return useQuery({
queryKey: ["work", key],
queryKey: ["bookDescription", key],
queryFn: () => fetchWork(key),
select: toDescription,
});
Expand Down
25 changes: 25 additions & 0 deletions app/api/useBookSearchParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useSearchParams } from "react-router";

function useBookSearchParams() {
const [searchParams] = useSearchParams();

const rawQuery = searchParams.get("q") ?? "";
const page = parseInt(searchParams.get("page") ?? "1", 10);
const filters = {
author: searchParams.get("author") ?? undefined,
subject: searchParams.get("subject") ?? undefined,
place: searchParams.get("place") ?? undefined,
person: searchParams.get("person") ?? undefined,
language: searchParams.get("language") ?? undefined,
publisher: searchParams.get("publisher") ?? undefined,
};

const hasFilters =
Object.values(filters).filter((value) => value !== undefined).length > 0;

const query = rawQuery || hasFilters ? rawQuery : "subject:fantasy";

return { rawQuery, query, page, filters };
}

export { useBookSearchParams };
File renamed without changes.
11 changes: 11 additions & 0 deletions app/api/useWork.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useQuery } from "@tanstack/react-query";
import { fetchWork } from "~/api/openLibrary";

function useWork(key: string) {
return useQuery({
queryKey: ["work", key],
queryFn: () => fetchWork(key),
});
}

export { useWork };
8 changes: 8 additions & 0 deletions app/components/BookCard/BookCard.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
border: 1px solid var(--border-color);
}

.book-card-link {
all: unset;
width: 100%;
height: 100%;

}

.book-card-button {
all: unset;
cursor: pointer;
Expand All @@ -17,6 +24,7 @@
flex: 1;
flex-direction: column;
width: 100%;
height: 100%;

box-sizing: border-box;
padding: 24px;
Expand Down
31 changes: 16 additions & 15 deletions app/components/BookCard/BookCard.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { BookCard } from "~/components/BookCard/BookCard";
import { bookWithCover as book } from "~/test/fixtures/books";
import { createRoutesStub } from "react-router";

const Stub = createRoutesStub([
{
path: "/",
Component: () => <BookCard book={book} />,
},
]);

describe("BookCard", () => {
it("shows title, authors and year from the book prop", () => {
render(<BookCard book={book} onBookSelect={vi.fn()} />);
render(<Stub />);

expect(
screen.getByRole("heading", { name: "The Blade Itself" }),
Expand All @@ -15,26 +22,20 @@ describe("BookCard", () => {
expect(screen.getByText("2001")).toBeInTheDocument();
});

it("calls onBookSelect with the book when clicked", async () => {
const user = userEvent.setup();
const onBookSelect = vi.fn();
render(<BookCard book={book} onBookSelect={onBookSelect} />);
it("links to the book's details page, keeping the current search", () => {
render(<Stub initialEntries={["/?q=fantasy"]} />);

await user.click(
screen.getByRole("button", { name: "View details for The Blade Itself" }),
expect(screen.getByRole("link")).toHaveAttribute(
"href",
`/book/${book.id}?q=fantasy`,
);

expect(onBookSelect).toHaveBeenCalledTimes(1);
expect(onBookSelect).toHaveBeenCalledWith(book);
});

// Snapshot test: the rendered HTML is saved in __snapshots__/ the first time
// the test runs. Later runs fail if the markup changes unexpectedly.
// Run `pnpm test -u` to update the snapshot when a change is intended.
it("matches the snapshot", () => {
const { container } = render(
<BookCard book={book} onBookSelect={vi.fn()} />,
);
const { container } = render(<Stub />);

expect(container).toMatchSnapshot();
});
Expand Down
36 changes: 19 additions & 17 deletions app/components/BookCard/BookCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,37 @@ import styles from "./BookCard.module.css";
import { BookCover } from "~/components/BookCover/BookCover";
import { FavoriteButton } from "~/components/FavoriteButton/FavoriteButton";
import type { Book } from "~/api/types";
import { Link, useSearchParams } from "react-router";

interface BookCardProps {
book: Book;
onBookSelect: (book: Book) => void;
}

function BookCard({ book, onBookSelect }: BookCardProps) {
const selectBook = () => {
onBookSelect(book);
};
function BookCard({ book }: BookCardProps) {
const [searchParams] = useSearchParams();

return (
<article className={styles.bookCard}>
<FavoriteButton book={book} />

<button
type="button"
onClick={selectBook}
className={styles.bookCardButton}
aria-label={`View details for ${book.title}`}
<Link
to={`/book/${book.id}?${searchParams}`}
className={styles.bookCardLink}
>
<BookCover book={book} />
<button
type="button"
className={styles.bookCardButton}
aria-label={`View details for ${book.title}`}
>
<BookCover book={book} />

<section className={styles.info}>
<h3 className={styles.title}>{book.title}</h3>
<p>{book.author.join(", ")}</p>
<p>{book.publishedYear ?? "Unknown"}</p>
</section>
</button>
<section className={styles.info}>
<h3 className={styles.title}>{book.title}</h3>
<p>{book.author.join(", ")}</p>
<p>{book.publishedYear ?? "Unknown"}</p>
</section>
</button>
</Link>
</article>
);
}
Expand Down
52 changes: 29 additions & 23 deletions app/components/BookCard/__snapshots__/BookCard.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,38 @@ exports[`BookCard > matches the snapshot 1`] = `
>
+
</button>
<button
aria-label="View details for The Blade Itself"
class="bookCardButton"
type="button"
<a
class="bookCardLink"
data-discover="true"
href="/book/OL8400950W"
>
<img
alt="Book cover of The Blade Itself"
class="coverImage"
src="https://covers.openlibrary.org/b/id/14543422-M.jpg?default=false"
/>
<section
class="info"
<button
aria-label="View details for The Blade Itself"
class="bookCardButton"
type="button"
>
<h3
class="title"
<img
alt="Book cover of The Blade Itself"
class="coverImage"
src="https://covers.openlibrary.org/b/id/14543422-M.jpg?default=false"
/>
<section
class="info"
>
The Blade Itself
</h3>
<p>
Joe Abercrombie
</p>
<p>
2001
</p>
</section>
</button>
<h3
class="title"
>
The Blade Itself
</h3>
<p>
Joe Abercrombie
</p>
<p>
2001
</p>
</section>
</button>
</a>
</article>
</div>
`;
52 changes: 0 additions & 52 deletions app/components/BookDetails/BookDetails.test.tsx

This file was deleted.

Loading