Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 16 additions & 4 deletions frontend/app/components/BookCard/BookCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ interface BookCardProps {
function BookCard({ book, searchListParams }: BookCardProps) {
const [searchParams] = useSearchParams();
const location = useLocation();
const isProfilePage = location.pathname === "/profile";

const nextSearchParams = new URLSearchParams(
searchListParams ?? searchParams,
);

if (location.pathname === "/profile") {
if (isProfilePage) {
nextSearchParams.set("origin", "favorites");
}

Expand All @@ -40,8 +41,18 @@ function BookCard({ book, searchListParams }: BookCardProps) {

<CardContent className="mt-2">
<p>{book.authors.join(", ")}</p>
<p>{book.publishYear ?? "Unknown"}</p>
<p className="mt-2 flex items-center gap-1 text-sm text-muted-foreground">
<p className="text-muted-foreground">
{book.publishYear ?? "Year unknown"}
</p>
<p
className="mt-2 flex items-center gap-1 text-sm text-muted-foreground"
role={book.rating != null ? "group" : undefined}
aria-label={
book.rating != null
? `Rating ${book.rating.toFixed(2)} out of 5 based on ${(book.numRatings ?? 0).toLocaleString("en")} ratings`
: undefined
}
>
{book.rating == null ? (
"No ratings yet"
) : (
Expand All @@ -53,7 +64,8 @@ function BookCard({ book, searchListParams }: BookCardProps) {
<span className="font-medium text-foreground">
{book.rating.toFixed(2)}
</span>
({(book.numRatings ?? 0).toLocaleString("en")} ratings)
({(book.numRatings ?? 0).toLocaleString("en")}
{!isProfilePage && " ratings"})
</>
)}
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ exports[`BookCard > matches the snapshot 1`] = `
<p>
Joe Abercrombie
</p>
<p>
<p
class="text-muted-foreground"
>
2001
</p>
<p
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function Header() {

<MobileHeaderMenu />

<div className="hidden md:flex items-center gap-4">
<div className="hidden md:flex items-center gap-2">
<DarkModeToggle variant="button" />
{!isPending && session ? (
<>
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/components/books/SearchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function SearchBar({ resultCount }: SearchBarProps) {
const filtersDescription = `${filtersCount} ${filtersCount === 1 ? "filter" : "filters"} active`;

return (
<section className="flex items-center justify-center gap-4 mx-auto mt-10 w-full px-10 sm:mt-20">
<section className="flex items-center justify-center gap-4 mx-auto mt-10 w-full px-10">
<h1 className="sr-only">Browse books</h1>
<InputGroup className="h-10 max-w-150 bg-card">
<InputGroupAddon align="inline-start">
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/components/ui/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ function ComboboxItem({
<ComboboxPrimitive.Item
data-slot="combobox-item"
className={cn(
"relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative flex w-full cursor-pointer items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
Expand Down
8 changes: 4 additions & 4 deletions frontend/app/components/ui/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
"group/dropdown-menu-item relative flex cursor-pointer items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
className,
)}
{...props}
Expand All @@ -112,7 +112,7 @@ function DropdownMenuSubTrigger({
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"flex cursor-pointer items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
Expand Down Expand Up @@ -161,7 +161,7 @@ function DropdownMenuCheckboxItem({
data-slot="dropdown-menu-checkbox-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative flex cursor-pointer items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
Expand Down Expand Up @@ -202,7 +202,7 @@ function DropdownMenuRadioItem({
data-slot="dropdown-menu-radio-item"
data-inset={inset}
className={cn(
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative flex cursor-pointer items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
Expand Down
8 changes: 4 additions & 4 deletions frontend/app/components/ui/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
Expand Down Expand Up @@ -117,7 +117,7 @@ function SelectItem({
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
"relative flex w-full cursor-pointer items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
Expand Down Expand Up @@ -157,7 +157,7 @@ function SelectScrollUpButton({
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button"
className={cn(
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
"top-0 z-10 flex w-full cursor-pointer items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
Expand All @@ -175,7 +175,7 @@ function SelectScrollDownButton({
<SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button"
className={cn(
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
"bottom-0 z-10 flex w-full cursor-pointer items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
Expand Down
2 changes: 2 additions & 0 deletions frontend/app/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
}

:root {
color-scheme: light;
--background: oklch(0.9659 0.0042 56.37);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
Expand Down Expand Up @@ -126,6 +127,7 @@
}

.dark {
color-scheme: dark;
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export function Layout({ children }: { children: React.ReactNode }) {
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Readr</title>
<link
rel="apple-touch-icon"
sizes="180x180"
Expand Down Expand Up @@ -129,6 +128,7 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
alignItems: "center",
}}
>
<title>{`${message === "404" ? "Page not found" : "Error"} | Readr`}</title>
<h1>{message}</h1>
<p>{details}</p>
{stack && (
Expand Down
16 changes: 12 additions & 4 deletions frontend/app/routes/book/selectedBook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export default function SelectedBookPage({ params }: Route.ComponentProps) {
const origin = searchParams.get("origin");
const { book, loading, error } = useBook({ id: params.bookId });

const pageTitle = book ? `${book.title} | Readr` : "Book details | Readr";

const { books: searchBooks } = useSearchBooks({
filters,
title: query,
Expand Down Expand Up @@ -65,6 +67,8 @@ export default function SelectedBookPage({ params }: Route.ComponentProps) {
if (loading) {
return (
<main className="flex flex-col items-center justify-center h-full">
{/* Uses <title> instead of meta() to allow the document title to be defined dynamically in the route. */}
<title>{pageTitle}</title>
<h1>Loading Book Details</h1>
<p>The requested book details are currently being loaded.</p>
</main>
Expand All @@ -74,6 +78,8 @@ export default function SelectedBookPage({ params }: Route.ComponentProps) {
if (error || !book || !books) {
return (
<main className="flex flex-col items-center justify-center h-full">
{/* Uses <title> instead of meta() to allow the document title to be defined dynamically in the route. */}
<title>{pageTitle}</title>
<h1>Book Not Found</h1>
<p>The requested book could not be found.</p>
</main>
Expand All @@ -91,6 +97,8 @@ export default function SelectedBookPage({ params }: Route.ComponentProps) {

return (
<main>
{/* Uses <title> instead of meta() to allow the document title to be defined dynamically in the route. */}
<title>{pageTitle}</title>
{origin === "favorites" ? (
<PageBreadcrumb
items={[
Expand Down Expand Up @@ -147,15 +155,15 @@ export default function SelectedBookPage({ params }: Route.ComponentProps) {
Go back
</Button>

<section className="grid grid-cols-1 gap-2 lg:grid-cols-[2fr_3fr]">
<Card className="relative min-h-96 h-full items-center justify-center p-7.5 lg:sticky lg:top-4 lg:self-start [&>figure]:w-76 [&>figure]:max-w-full [&>p]:w-80 [&>p]:max-w-full">
<section className="grid grid-cols-1 gap-2 lg:items-stretch lg:grid-cols-[2fr_3fr]">
<Card className="relative min-h-96 items-center justify-center p-7.5 lg:sticky lg:top-4 lg:h-160 lg:self-start [&>figure]:w-76 [&>figure]:max-w-full [&>p]:w-80 [&>p]:max-w-full">
<FavoriteButton book={book} />
<BookCover book={book} loading="eager" />
<BookRating rating={book.rating} numRatings={book.numRatings} />
</Card>

<section className="flex flex-col gap-2">
<Card className="p-7.5">
<section className="flex flex-col gap-2 lg:self-stretch">
<Card className="p-7.5 lg:flex-1">
<CardHeader className="px-0">
<CardTitle>
<h1 className="text-2xl font-bold">{book.title}</h1>
Expand Down
11 changes: 7 additions & 4 deletions frontend/app/routes/books.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import { screen } from "@testing-library/react";
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import BooksPage from "~/routes/books";
import SelectedBookPage from "~/routes/book/selectedBook";
Expand Down Expand Up @@ -71,9 +71,12 @@ describe("BooksPage", () => {
});

expect(await screen.findByText("15 results")).toBeInTheDocument();
expect(screen.getByRole("link", { current: "page" })).toHaveTextContent(
"2",
);
expect(
within(screen.getByRole("navigation", { name: "pagination" })).getByRole(
"link",
{ current: "page" },
),
).toHaveTextContent("2");
});

it("tells the user when no books match the search", async () => {
Expand Down
12 changes: 12 additions & 0 deletions frontend/app/routes/books.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useSearchBooks, type SearchBook } from "~/hooks/useSearchBooks";
import { SearchBar } from "~/components/books/SearchBar/SearchBar";
import { BookCard } from "~/components/BookCard/BookCard";
import { SortSelect } from "~/components/books/SortSelect/SortSelect";
import { PageBreadcrumb } from "~/components/PageBreadcrumb/PageBreadcrumb";
import { useBookSearchParams } from "~/hooks/useBookSearchParams";
import { useSuggestedBooks } from "~/hooks/useSuggestedBooks";
import { useSearchParams } from "react-router";
Expand Down Expand Up @@ -33,6 +34,12 @@ function BookList({
);
}

function BooksBreadcrumb() {
return (
<PageBreadcrumb items={[{ label: "Home", to: "/" }, { label: "Books" }]} />
);
}

export default function BooksPage() {
const [searchParams] = useSearchParams();
const { query, page, filters, sort } = useBookSearchParams();
Expand Down Expand Up @@ -69,6 +76,7 @@ export default function BooksPage() {
if (loading) {
return (
<main>
<BooksBreadcrumb />
<SearchBar />
<p
role="status"
Expand All @@ -83,6 +91,7 @@ export default function BooksPage() {
if (error) {
return (
<main>
<BooksBreadcrumb />
<SearchBar />
<p
role="alert"
Expand All @@ -98,6 +107,7 @@ export default function BooksPage() {
if (suggestions && suggestions.length > 0) {
return (
<main>
<BooksBreadcrumb />
<SearchBar resultCount={0} />
<section
aria-labelledby="suggestions-heading"
Expand Down Expand Up @@ -133,6 +143,7 @@ export default function BooksPage() {

return (
<main>
<BooksBreadcrumb />
<SearchBar resultCount={0} />
<p
role="status"
Expand All @@ -146,6 +157,7 @@ export default function BooksPage() {

return (
<main>
<BooksBreadcrumb />
<SearchBar resultCount={totalCount} />
<SortSelect />
<BookList books={books} />
Expand Down
16 changes: 14 additions & 2 deletions frontend/app/routes/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { PageBreadcrumb } from "~/components/PageBreadcrumb/PageBreadcrumb";
import { ReviewList } from "~/components/ReviewList/ReviewList";
import { Avatar, AvatarFallback } from "~/components/ui/Avatar";
import { Badge } from "~/components/ui/Badge";
import { buttonVariants } from "~/components/ui/Button";
import { Button, buttonVariants } from "~/components/ui/Button";
import {
Card,
CardContent,
Expand Down Expand Up @@ -131,7 +131,7 @@ export default function ProfilePage() {
</section>
)}
</CardContent>
<CardFooter>
<CardFooter className="flex flex-col gap-2">
<Link
to="/settings"
className={buttonVariants({
Expand All @@ -141,6 +141,18 @@ export default function ProfilePage() {
>
Edit profile
</Link>
<Button
variant="outline"
className="w-full border-destructive/50 text-destructive"
onClick={() => {
void (async () => {
await authClient.signOut();
location.reload();
})();
}}
>
Log out
</Button>
</CardFooter>
</Card>

Expand Down