Skip to content
Merged
59 changes: 42 additions & 17 deletions src/components/BookGrid/BookGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,74 @@
import { useState } from "react";
import { BookCard } from "../BookCard/BookCard";
import { SearchBar } from "../SearchBar/SearchBar";
import styles from "./BookGrid.module.css";
import { useBooks } from "../../api/useBooks";
import { useDebounce } from "./useDebounce";
import type { Book } from "../../api/types";

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

function BookGrid({ onBookSelect }: BookGridProps) {
const [searchQuery, setSearchQuery] = useState("");

const debouncedSearch = useDebounce(searchQuery, 500);
const query = debouncedSearch.trim() || "subject:fantasy";

const { data, isPending, isError, error } = useBooks({
query: "subject:fantasy",
query,
});

const searchBar = (
<SearchBar query={searchQuery} onQueryChange={setSearchQuery} />
);

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

if (isError) {
return (
<p role="status" className={styles.message}>
Error: {error.message}
</p>
<>
{searchBar}
<p role="status" className={styles.message}>
Error: {error.message}
</p>
</>
);
}

if (data.length === 0) {
return (
<p role="status" className={styles.message}>
No books found for this search
</p>
<>
{searchBar}
<p role="status" className={styles.message}>
No books found for this search
</p>
</>
);
}

return (
<ul className={styles.bookGrid}>
{data.map((book) => (
<li key={book.id}>
<BookCard book={book} onBookSelect={onBookSelect} />
</li>
))}
</ul>
<>
{searchBar}

<ul className={styles.bookGrid}>
{data.map((book) => (
<li key={book.id}>
<BookCard book={book} onBookSelect={onBookSelect} />
</li>
))}
</ul>
</>
);
}

Expand Down
21 changes: 21 additions & 0 deletions src/components/BookGrid/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useEffect, useState } from "react";

// Debounce pattern largely adapted from this Stack Overflow post:
// https://stackoverflow.com/questions/75556418/how-to-use-debounce-hooks-in-react
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
const timeoutId = window.setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
window.clearTimeout(timeoutId);
};
}, [value, delay]);

return debouncedValue;
}

export { useDebounce };
22 changes: 22 additions & 0 deletions src/components/SearchBar/SearchBar.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.search-bar {
display: block;
width: 100%;
max-width: 500px;
padding: 10px 16px;
margin: 80px auto 0 auto;

border: 1px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 1rem;
font-family: inherit;

&:hover {
border-color: var(--theme-color);
}

@media only screen and (max-width: 600px) {
width: calc(100% - 80px); /* Adjust width to account for the margins*/
margin: 40px 40px 0 40px;
max-width: none;
}
}
22 changes: 22 additions & 0 deletions src/components/SearchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import styles from "./SearchBar.module.css";

interface SearchBarProps {
query: string;
onQueryChange: (newQuery: string) => void;
}

function SearchBar({ query, onQueryChange }: SearchBarProps) {
return (
<input
type="search"
name="search"
aria-label="Search for books"
className={styles.searchBar}
placeholder="Search books..."
value={query}
onChange={(e) => onQueryChange(e.target.value)}
/>
);
}

export { SearchBar };