Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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: 6 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Book } from "./api/types.ts";

function App() {
const [selectedBook, setSelectedBook] = useState<Book | null>(null);
const [page, setPage] = useState(1);

return (
<>
Expand All @@ -18,7 +19,11 @@ function App() {
onGoBack={() => setSelectedBook(null)}
/>
) : (
<BookGrid onBookSelect={setSelectedBook} />
<BookGrid
page={page}
onBookSelect={setSelectedBook}
onPageChange={setPage}
/>
)}
</main>
<Footer />
Expand Down
13 changes: 11 additions & 2 deletions src/api/openLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,17 @@ async function searchBooks(
): Promise<OpenLibrarySearchResponse> {
const url = new URL(SEARCH_URL);
url.searchParams.set("q", params.query);
url.searchParams.set("limit", String(SEARCH_LIMIT));
url.searchParams.set(
"limit",
params.limit ? String(params.limit) : String(SEARCH_LIMIT),
);
url.searchParams.set("fields", SEARCH_FIELDS.join(","));
url.searchParams.set("lang", params.lang ?? "en");
url.searchParams.set("page", params.page ? String(params.page) : "1");

if (params.sort) {
url.searchParams.set("sort", params.sort);
}

const response = await fetch(url);

Expand Down Expand Up @@ -52,4 +61,4 @@ function toBook(doc: OpenLibraryDoc): Book {
};
}

export { searchBooks, toBook };
export { SEARCH_URL, SEARCH_LIMIT, searchBooks, toBook };
6 changes: 5 additions & 1 deletion src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ export interface OpenLibraryDoc {

export interface BookSearchParams {
query: string;
// Add more search parameters here as needed.
fields?: string;
sort?: string;
lang?: string;
limit?: number;
page?: number;
}

export interface Book {
Expand Down
15 changes: 7 additions & 8 deletions src/api/useBooks.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { useQuery } from "@tanstack/react-query";
import { searchBooks, toBook } from "./openLibrary";
import type {
Book,
BookSearchParams,
OpenLibrarySearchResponse,
} from "./types";
import type { BookSearchParams, OpenLibrarySearchResponse } from "./types";

function toBooks(data: OpenLibrarySearchResponse): Book[] {
return data.docs.map(toBook);
function parseBooks(data: OpenLibrarySearchResponse) {
return {
numFound: data.numFound,
books: data.docs.map(toBook),
};
}

function useBooks(params: BookSearchParams) {
return useQuery({
queryKey: ["books", params],
queryFn: () => searchBooks(params),
select: toBooks,
select: parseBooks,
});
}

Expand Down
44 changes: 27 additions & 17 deletions src/components/BookGrid/BookGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@ import { BookCard } from "../BookCard/BookCard";
import styles from "./BookGrid.module.css";
import { useBooks } from "../../api/useBooks";
import type { Book } from "../../api/types";
import { Pagination } from "../Pagination/Pagination";
import { SEARCH_LIMIT } from "../../api/openLibrary";

interface BookGridProps {
page: number;
onBookSelect: (book: Book) => void;
onPageChange: (page: number) => void;
}

function BookGrid({ onBookSelect }: BookGridProps) {
function BookGrid({ page, onBookSelect, onPageChange }: BookGridProps) {
const { data, isPending, isError, error } = useBooks({
query: "subject:fantasy",
page,
});

if (isPending) {
return (
<p role="status" className={styles.message}>
Loading books...
</p>
);
}

if (isError) {
return (
<p role="status" className={styles.message}>
Expand All @@ -28,7 +25,7 @@ function BookGrid({ onBookSelect }: BookGridProps) {
);
}

if (data.length === 0) {
if (!data || data.books.length === 0) {
return (
<p role="status" className={styles.message}>
No books found for this search
Expand All @@ -37,13 +34,26 @@ function BookGrid({ onBookSelect }: BookGridProps) {
}

return (
<ul className={styles.bookGrid}>
{data.map((book) => (
<li key={book.id}>
<BookCard book={book} onBookSelect={onBookSelect} />
</li>
))}
</ul>
<>
{!isPending ? (
<ul className={styles.bookGrid}>
{data.books.map((book) => (
<li key={book.id}>
<BookCard book={book} onBookSelect={onBookSelect} />
</li>
))}
</ul>
) : (
<p role="status" className={styles.message}>
Loading books...
</p>
)}
<Pagination
currentPage={page}
totalPages={Math.ceil(data.numFound / SEARCH_LIMIT)}
onPageChange={onPageChange}
/>
</>
);
}

Expand Down
39 changes: 39 additions & 0 deletions src/components/Pagination/Pagination.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 0.75rem 0;
}

.paginationButton {
display: flex;
align-items: center;
justify-content: center;
width: 6rem;
padding: 0.5rem 1.25rem;
color: var(--theme-color);
background-color: white;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
cursor: pointer;
transition: 0.15s ease-in-out;
}

.paginationButton:hover:not(:disabled) {
background-color: #f3f4f6;
border-color: #9ca3af;
}

.paginationButton:disabled {
color: var(--muted);
background-color: var(--muted-background);
border-color: var(--border-color);
cursor: not-allowed;
}

.paginationInfo {
font-weight: 500;
width: 5rem;
text-align: center;
}
40 changes: 40 additions & 0 deletions src/components/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import styles from "./Pagination.module.css";

function Pagination({
totalPages,
currentPage,
onPageChange,
}: {
totalPages: number;
currentPage: number;
onPageChange: (page: number) => void;
}) {
const handlePageChange = (newPage: number) => {
window.scrollTo({ top: 0, behavior: "smooth" });
onPageChange(newPage);
};

return (
<section className={styles.pagination}>
<button
className={styles.paginationButton}
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
Previous
</button>
<span className={styles.paginationInfo}>
{currentPage} of {totalPages}
</span>
<button
className={styles.paginationButton}
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
Next
</button>
</section>
);
}

export { Pagination };
2 changes: 2 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
--footer-background-color: #2b2a2a;
--border-color: #ebe9e6;
--border-radius: 8px;
--muted: #6b7280;
--muted-background: rgba(255, 255, 255, 0.25);
}

* {
Expand Down