From 0f8dff91282c774f464c3656adbfc9a92133e55f Mon Sep 17 00:00:00 2001 From: Robert Andreas Kyllo Date: Sun, 6 Sep 2026 14:30:55 +0200 Subject: [PATCH 1/2] feat: add BookCard presentation (#11) --- src/components/BookCard.css | 78 +++++++++++++++++++++++++++++++++++++ src/components/BookCard.tsx | 36 +++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/components/BookCard.css create mode 100644 src/components/BookCard.tsx diff --git a/src/components/BookCard.css b/src/components/BookCard.css new file mode 100644 index 0000000..a5aad97 --- /dev/null +++ b/src/components/BookCard.css @@ -0,0 +1,78 @@ +.book-card { + display: grid; + grid-template-columns: minmax(8rem, 12rem) 1fr; + gap: 1.5rem; + overflow: hidden; + border: 1px solid #d5dde5; + border-radius: 0.75rem; + background-color: #ffffff; + box-shadow: 0 0.25rem 0.75rem rgb(44 62 80 / 12%); +} + +.book-card-cover { + display: grid; + min-height: 12rem; + place-items: center; + background-color: #edf2f7; + color: #52606d; + text-align: center; +} + +.book-card-cover img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.book-card-cover p { + margin: 1rem; +} + +.book-card-content { + padding: 1.25rem 1.25rem 1.25rem 0; +} + +.book-card-content h2 { + margin: 0; + color: #243b53; +} + +.book-card-content p { + margin: 0.5rem 0 0; + color: #52606d; +} + +.book-card-authors { + font-weight: 600; +} + +.book-card-subjects { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 1rem 0 0; + padding: 0; + list-style: none; +} + +.book-card-subjects li { + border-radius: 999px; + background-color: #d9e2ec; + color: #243b53; + padding: 0.25rem 0.625rem; + font-size: 0.875rem; +} + +@media (max-width: 32rem) { + .book-card { + grid-template-columns: 1fr; + } + + .book-card-cover { + min-height: 16rem; + } + + .book-card-content { + padding: 0 1.25rem 1.25rem; + } +} diff --git a/src/components/BookCard.tsx b/src/components/BookCard.tsx new file mode 100644 index 0000000..6c66ecc --- /dev/null +++ b/src/components/BookCard.tsx @@ -0,0 +1,36 @@ +import { coverUrl } from "../api/openLibrary"; +import type { Book } from "../types"; +import "./BookCard.css"; + +interface BookCardProps { + book: Book; +} + +export function BookCard({ book }: BookCardProps) { + const imageUrl = coverUrl(book.coverId); + const authors = book.authors.length > 0 ? book.authors.join(", ") : "Unknown author"; + + return ( +
+
+ {imageUrl ? ( + {`Cover + ) : ( +

No cover available

+ )} +
+
+

{book.title}

+

{authors}

+ {book.firstPublishYear !== null &&

First published: {book.firstPublishYear}

} + {book.subjects.length > 0 && ( +
    + {book.subjects.map((subject) => ( +
  • {subject}
  • + ))} +
+ )} +
+
+ ); +} From 9fa70cdea75a9eb8966f1d5d719da66656ed1aeb Mon Sep 17 00:00:00 2001 From: Robert Andreas Kyllo Date: Sun, 6 Sep 2026 14:30:55 +0200 Subject: [PATCH 2/2] test: add BookCard snapshots (#11) --- src/components/BookCard.test.tsx | 44 +++++++++++ .../__snapshots__/BookCard.test.tsx.snap | 73 +++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 src/components/BookCard.test.tsx create mode 100644 src/components/__snapshots__/BookCard.test.tsx.snap diff --git a/src/components/BookCard.test.tsx b/src/components/BookCard.test.tsx new file mode 100644 index 0000000..4e74de9 --- /dev/null +++ b/src/components/BookCard.test.tsx @@ -0,0 +1,44 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import type { Book } from "../types"; +import { BookCard } from "./BookCard"; + +const fullBook: Book = { + key: "/works/OL1", + title: "A Wizard of Earthsea", + authors: ["Ursula K. Le Guin", "Another Author"], + firstPublishYear: 1968, + coverId: 123, + subjects: ["Fantasy", "Magic"], +}; + +const minimalBook: Book = { + key: "/works/OL2", + title: "Untitled Book", + authors: [], + firstPublishYear: null, + coverId: null, + subjects: [], +}; + +describe("BookCard", () => { + it("renders a complete book", () => { + const { container } = render(); + + expect(screen.getByRole("heading", { name: fullBook.title })).toBeInTheDocument(); + expect(screen.getByText("Ursula K. Le Guin, Another Author")).toBeInTheDocument(); + expect(screen.getByRole("img", { name: "Cover of A Wizard of Earthsea" })).toBeInTheDocument(); + expect(screen.getByRole("list", { name: "Subjects" })).toHaveTextContent("Fantasy"); + expect(container).toMatchSnapshot(); + }); + + it("renders a placeholder for a minimal book", () => { + const { container } = render(); + + expect(screen.getByRole("heading", { name: minimalBook.title })).toBeInTheDocument(); + expect(screen.getByText("Unknown author")).toBeInTheDocument(); + expect(screen.getByText("No cover available")).toBeInTheDocument(); + expect(screen.queryByRole("img")).not.toBeInTheDocument(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/components/__snapshots__/BookCard.test.tsx.snap b/src/components/__snapshots__/BookCard.test.tsx.snap new file mode 100644 index 0000000..3d54a76 --- /dev/null +++ b/src/components/__snapshots__/BookCard.test.tsx.snap @@ -0,0 +1,73 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`BookCard > renders a complete book 1`] = ` +
+
+
+ Cover of A Wizard of Earthsea +
+
+

+ A Wizard of Earthsea +

+

+ Ursula K. Le Guin, Another Author +

+

+ First published: + 1968 +

+
    +
  • + Fantasy +
  • +
  • + Magic +
  • +
+
+
+
+`; + +exports[`BookCard > renders a placeholder for a minimal book 1`] = ` +
+
+
+

+ No cover available +

+
+
+

+ Untitled Book +

+

+ Unknown author +

+
+
+
+`;