From 048edeb728ba28eb58d2fdfba913f4192909fa2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Str=C3=B8m-Andersen?= Date: Tue, 15 Sep 2026 23:59:05 +0200 Subject: [PATCH 01/11] refactor: extract FavoriteButton component from BookCard --- src/components/BookCard/BookCard.tsx | 38 +---------------- .../FavoriteButton/FavoriteButton.module.css | 26 ++++++++++++ .../FavoriteButton/FavoriteButton.tsx | 41 +++++++++++++++++++ 3 files changed, 69 insertions(+), 36 deletions(-) create mode 100644 src/components/FavoriteButton/FavoriteButton.module.css create mode 100644 src/components/FavoriteButton/FavoriteButton.tsx diff --git a/src/components/BookCard/BookCard.tsx b/src/components/BookCard/BookCard.tsx index 5d7b299..c2bcd82 100644 --- a/src/components/BookCard/BookCard.tsx +++ b/src/components/BookCard/BookCard.tsx @@ -1,6 +1,6 @@ -import { useState } from "react"; import styles from "./BookCard.module.css"; import { CheckIcon } from "../icons/CheckIcon"; +import { FavoriteButton } from "../FavoriteButton/FavoriteButton"; import type { Book } from "../../api/types"; interface BookCardProps { @@ -8,43 +8,9 @@ interface BookCardProps { } function BookCard({ book }: BookCardProps) { - const [favorites, setFavorites] = useState(() => { - const rawFavorites = localStorage.getItem("favorites"); - return JSON.parse(rawFavorites ?? "[]"); - }); - - const saveFavorite = () => { - const rawFavorites = localStorage.getItem("favorites"); - let savedFavorites: string[] = JSON.parse(rawFavorites ?? "[]"); - - if (savedFavorites.includes(book.id)) { - // Remove from favorites if already present - savedFavorites = savedFavorites.filter((id) => id !== book.id); - } else { - savedFavorites.push(book.id); - } - - localStorage.setItem("favorites", JSON.stringify(savedFavorites)); - setFavorites(savedFavorites); - }; - return (
- - {book.coverImage ? ( - {`Book - ) : ( -
No cover available
- )} +

{book.title}

{book.author.join(", ")}

diff --git a/src/components/FavoriteButton/FavoriteButton.module.css b/src/components/FavoriteButton/FavoriteButton.module.css new file mode 100644 index 0000000..4f08bfe --- /dev/null +++ b/src/components/FavoriteButton/FavoriteButton.module.css @@ -0,0 +1,26 @@ +.favoriteButton { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + top: 8px; + right: 8px; + background-color: white; + color: var(--theme-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + width: 32px; + height: 32px; + font-size: 1.5rem; + cursor: pointer; + + &:hover { + border: 1px solid var(--theme-color); + } +} + +.favoriteButtonActive { + border-color: var(--theme-color); + color: green; + border: 1px solid green; +} diff --git a/src/components/FavoriteButton/FavoriteButton.tsx b/src/components/FavoriteButton/FavoriteButton.tsx new file mode 100644 index 0000000..5bd7f54 --- /dev/null +++ b/src/components/FavoriteButton/FavoriteButton.tsx @@ -0,0 +1,41 @@ +import { useState } from "react"; +import styles from "./FavoriteButton.module.css"; +import { CheckIcon } from "../icons/CheckIcon"; +import type { Book } from "../../api/types"; + +interface FavoriteButtonProps { + book: Book; +} + +function FavoriteButton({ book }: FavoriteButtonProps) { + const [favorites, setFavorites] = useState(() => { + const rawFavorites = localStorage.getItem("favorites"); + return JSON.parse(rawFavorites ?? "[]"); + }); + + const saveFavorite = () => { + const rawFavorites = localStorage.getItem("favorites"); + let savedFavorites: string[] = JSON.parse(rawFavorites ?? "[]"); + + if (savedFavorites.includes(book.id)) { + // Remove from favorites if already present + savedFavorites = savedFavorites.filter((id) => id !== book.id); + } else { + savedFavorites.push(book.id); + } + + localStorage.setItem("favorites", JSON.stringify(savedFavorites)); + setFavorites(savedFavorites); + }; + + return ( + + ); +} + +export { FavoriteButton }; From dba8bbe72dc56382a87b4f513406fa70bb0f4df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Str=C3=B8m-Andersen?= Date: Wed, 16 Sep 2026 00:00:15 +0200 Subject: [PATCH 02/11] refactor: extract BookCover component from BookCard --- src/components/BookCard/BookCard.module.css | 46 ------------------- src/components/BookCard/BookCard.tsx | 4 +- src/components/BookCover/BookCover.module.css | 18 ++++++++ src/components/BookCover/BookCover.tsx | 20 ++++++++ 4 files changed, 41 insertions(+), 47 deletions(-) create mode 100644 src/components/BookCover/BookCover.module.css create mode 100644 src/components/BookCover/BookCover.tsx diff --git a/src/components/BookCard/BookCard.module.css b/src/components/BookCard/BookCard.module.css index b6519bf..c9841e0 100644 --- a/src/components/BookCard/BookCard.module.css +++ b/src/components/BookCard/BookCard.module.css @@ -9,52 +9,6 @@ padding: 24px; } -.favoriteButton { - position: absolute; - display: flex; - align-items: center; - justify-content: center; - top: 8px; - right: 8px; - background-color: white; - color: var(--theme-color); - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - width: 32px; - height: 32px; - font-size: 1.5rem; - cursor: pointer; - - &:hover { - border: 1px solid var(--theme-color); - } -} - -.favoriteButtonActive { - border-color: var(--theme-color); - color: green; - border: 1px solid green; -} - -.coverImage { - width: 100%; - padding: 12px 24px; - object-fit: contain; -} - -.coverPlaceholder { - display: flex; - align-items: center; - justify-content: center; - aspect-ratio: 2 / 3; - margin: 12px 24px; - background-color: var(--background-color); - border-radius: var(--border-radius); - color: var(--theme-color); - font-size: 0.875rem; - text-align: center; -} - .info { margin-top: 16px; font-size: 0.9rem; diff --git a/src/components/BookCard/BookCard.tsx b/src/components/BookCard/BookCard.tsx index c2bcd82..aebd02b 100644 --- a/src/components/BookCard/BookCard.tsx +++ b/src/components/BookCard/BookCard.tsx @@ -1,5 +1,5 @@ import styles from "./BookCard.module.css"; -import { CheckIcon } from "../icons/CheckIcon"; +import { BookCover } from "../BookCover/BookCover"; import { FavoriteButton } from "../FavoriteButton/FavoriteButton"; import type { Book } from "../../api/types"; @@ -11,6 +11,8 @@ function BookCard({ book }: BookCardProps) { return (
+ +

{book.title}

{book.author.join(", ")}

diff --git a/src/components/BookCover/BookCover.module.css b/src/components/BookCover/BookCover.module.css new file mode 100644 index 0000000..272f503 --- /dev/null +++ b/src/components/BookCover/BookCover.module.css @@ -0,0 +1,18 @@ +.coverImage { + width: 100%; + padding: 12px 24px; + object-fit: contain; +} + +.coverPlaceholder { + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 2 / 3; + margin: 12px 24px; + background-color: var(--background-color); + border-radius: var(--border-radius); + color: var(--theme-color); + font-size: 0.875rem; + text-align: center; +} diff --git a/src/components/BookCover/BookCover.tsx b/src/components/BookCover/BookCover.tsx new file mode 100644 index 0000000..1d6de6c --- /dev/null +++ b/src/components/BookCover/BookCover.tsx @@ -0,0 +1,20 @@ +import styles from "./BookCover.module.css"; +import type { Book } from "../../api/types"; + +interface BookCoverProps { + book: Book; +} + +function BookCover({ book }: BookCoverProps) { + return book.coverImage ? ( + {`Book + ) : ( +
No cover available
+ ); +} + +export { BookCover }; From f8b9f689f2f2737fbb4c4e2643f1d7dac0f156d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Str=C3=B8m-Andersen?= Date: Wed, 16 Sep 2026 00:20:44 +0200 Subject: [PATCH 03/11] feat: improve screen reader support for favorite button --- src/components/FavoriteButton/FavoriteButton.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/FavoriteButton/FavoriteButton.tsx b/src/components/FavoriteButton/FavoriteButton.tsx index 5bd7f54..f95d73f 100644 --- a/src/components/FavoriteButton/FavoriteButton.tsx +++ b/src/components/FavoriteButton/FavoriteButton.tsx @@ -32,6 +32,13 @@ function FavoriteButton({ book }: FavoriteButtonProps) { From 7e4c2b9728641b86aa12bd769a01037e55e2b1b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Str=C3=B8m-Andersen?= Date: Wed, 16 Sep 2026 00:22:53 +0200 Subject: [PATCH 04/11] refactor: remove redundant border-color --- src/components/FavoriteButton/FavoriteButton.module.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/FavoriteButton/FavoriteButton.module.css b/src/components/FavoriteButton/FavoriteButton.module.css index 4f08bfe..3367373 100644 --- a/src/components/FavoriteButton/FavoriteButton.module.css +++ b/src/components/FavoriteButton/FavoriteButton.module.css @@ -20,7 +20,6 @@ } .favoriteButtonActive { - border-color: var(--theme-color); color: green; border: 1px solid green; } From 30d6d008579482405cc57fd07d4f35631f83306b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Str=C3=B8m-Andersen?= Date: Wed, 16 Sep 2026 00:37:53 +0200 Subject: [PATCH 05/11] refactor: replace div with p element --- src/components/BookCover/BookCover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/BookCover/BookCover.tsx b/src/components/BookCover/BookCover.tsx index 1d6de6c..6468650 100644 --- a/src/components/BookCover/BookCover.tsx +++ b/src/components/BookCover/BookCover.tsx @@ -13,7 +13,7 @@ function BookCover({ book }: BookCoverProps) { className={styles.coverImage} /> ) : ( -
No cover available
+

No cover available

); } From f69c8f6376f12cad04ffc93ffcef8a5b89e532ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Str=C3=B8m-Andersen?= Date: Wed, 16 Sep 2026 01:54:57 +0200 Subject: [PATCH 06/11] refactor: remove unused class --- src/components/BookCard/BookCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/BookCard/BookCard.tsx b/src/components/BookCard/BookCard.tsx index aebd02b..445c736 100644 --- a/src/components/BookCard/BookCard.tsx +++ b/src/components/BookCard/BookCard.tsx @@ -15,7 +15,7 @@ function BookCard({ book }: BookCardProps) {

{book.title}

-

{book.author.join(", ")}

+

{book.author.join(", ")}

{book.publishedYear ?? "Unknown"}

From 449df75dad23941d579ac9c146c4ecf0a298d199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Str=C3=B8m-Andersen?= Date: Wed, 16 Sep 2026 01:56:12 +0200 Subject: [PATCH 07/11] feat: add BookDetails component --- src/components/BookDetails/BookDetails.tsx | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/components/BookDetails/BookDetails.tsx diff --git a/src/components/BookDetails/BookDetails.tsx b/src/components/BookDetails/BookDetails.tsx new file mode 100644 index 0000000..1d3e2e0 --- /dev/null +++ b/src/components/BookDetails/BookDetails.tsx @@ -0,0 +1,31 @@ +import styles from "./BookDetails.module.css"; +import { BookCover } from "../BookCover/BookCover"; +import { FavoriteButton } from "../FavoriteButton/FavoriteButton"; +import type { Book } from "../../api/types"; + +interface BookDetailsProps { + book: Book; +} + +// TODO: Temporary placeholder until we have a real description from the API +const PLACEHOLDER_DESCRIPTION = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas efficitur rhoncus massa fermentum tristique. Curabitur interdum, elit eget rutrum posuere, turpis nisl malesuada neque, at consectetur massa nisl at risus. Mauris in lacinia dui. Proin vitae interdum nulla. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce sem lacus, pulvinar nec libero id, dapibus mollis eros. Integer nisi augue, malesuada nec sem ac, laoreet elementum turpis. Maecenas rutrum purus a laoreet vehicula. Duis tristique odio eget dolor accumsan, sed laoreet ipsum mattis. Nulla molestie nisi quis urna mollis egestas. Aliquam bibendum tristique facilisis. Curabitur feugiat, lectus et vulputate vehicula, erat turpis aliquet nunc, vitae dapibus erat nulla in ipsum. Curabitur rhoncus ipsum ligula, ut ultricies ex rutrum cursus. Sed in nisl in nisi finibus lobortis."; + +function BookDetails({ book }: BookDetailsProps) { + return ( +
+
+ + +
+
+

{book.title}

+

{book.author.join(", ")}

+

{book.publishedYear ?? "Unknown"}

+

{PLACEHOLDER_DESCRIPTION}

+
+
+ ); +} + +export { BookDetails }; From 59363ea12b76b0a46adacc261b784e03a6b82e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Str=C3=B8m-Andersen?= Date: Wed, 16 Sep 2026 01:56:39 +0200 Subject: [PATCH 08/11] feat: add styling for BookDetails --- .../BookDetails/BookDetails.module.css | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/components/BookDetails/BookDetails.module.css diff --git a/src/components/BookDetails/BookDetails.module.css b/src/components/BookDetails/BookDetails.module.css new file mode 100644 index 0000000..abc681d --- /dev/null +++ b/src/components/BookDetails/BookDetails.module.css @@ -0,0 +1,56 @@ +.bookDetails { + display: flex; + flex-direction: row; + max-width: 1200px; + margin: 0 auto; + margin-top: 25px; + padding: 40px; + gap: 10px; + + @media only screen and (max-width: 900px) { + flex-direction: column; + } +} + +.coverSection { + position: relative; + flex: 0 0 40%; + padding: 30px; + background-color: white; + border-radius: var(--border-radius); + border: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: center; +} + +.info { + flex: 1; + padding: 30px; + background-color: white; + border-radius: var(--border-radius); + border: 1px solid var(--border-color); +} + +.title { + margin-bottom: 8px; + font-size: 1.5rem; +} + +.description { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--border-color); + line-height: 1.5; +} + +@media only screen and (max-width: 900px) and (min-width: 750px) { + .coverSection img { + margin: 50px; +} +} +@media only screen and (max-width: 1000px) and (min-width: 901px) { + .coverSection { + padding: 15px; +} +} From 7ee535184891907f74b916e1060e92be2bcba9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Str=C3=B8m-Andersen?= Date: Wed, 16 Sep 2026 03:26:40 +0200 Subject: [PATCH 09/11] feat: add book selection from grid to show BookDetails Clicking a BookCard now updates the selected book in App state. App uses the state to decide whether to render Bookgrid or BookDetails, switching between the two views as sthe state changes --- src/App.tsx | 14 ++++++++++- src/components/BookCard/BookCard.tsx | 28 +++++++++++++++------- src/components/BookDetails/BookDetails.tsx | 18 ++++++++------ src/components/BookGrid/BookGrid.tsx | 9 +++++-- 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 2e500c4..385ecc3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,13 +1,25 @@ +import { useState } from "react"; import { Header } from "./components/Header/Header.tsx"; import { Footer } from "./components/Footer/Footer.tsx"; import { BookGrid } from "./components/BookGrid/BookGrid.tsx"; +import { BookDetails } from "./components/BookDetails/BookDetails.tsx"; +import type { Book } from "./api/types.ts"; function App() { + const [selectedBook, setSelectedBook] = useState(null); + return ( <>
- + {selectedBook ? ( + setSelectedBook(null)} + /> + ) : ( + + )}