Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e470c5b
feat: add Bookish favicon + icon file cleanup
ilsea Oct 8, 2026
120304f
fix: changed Url to URL in variable names for consistency
ilsea Oct 8, 2026
204b736
fix: change all exports to named instead of default
ilsea Oct 8, 2026
6da920c
fix: change prop naming for consistency
ilsea Oct 8, 2026
01493f7
fix: Change file name ~icons to ~icon
ilsea Oct 8, 2026
15068d9
fix: add "plank" to names that reference the decorative element
ilsea Oct 8, 2026
dc320c6
feat: move ShelfRibbon into a separate component
ilsea Oct 8, 2026
0da3d45
fix: swap out css color tag that was used incorrectly
ilsea Oct 8, 2026
26d1c7a
feat: add hover color behavior to star rating
ilsea Oct 8, 2026
a0c4d81
fix: replace hardcoded values in ribbon with var() from tokens.css
ilsea Oct 8, 2026
e387ef2
feat: Add new variant to Button component
ilsea Oct 8, 2026
fb5874b
feat: new component "Eyebrow" (sparkle icon + text above headers)
ilsea Oct 8, 2026
9eaf282
feat: new component ConfirmDialog popup for destructive actions
ilsea Oct 8, 2026
16e2607
feat: new component user notes panel
ilsea Oct 8, 2026
e3c3152
feat: add BookDetailPage
ilsea Oct 8, 2026
fc47aee
Merge branch 'main' into 29-create-book-details-page
ilsea Oct 8, 2026
a999a01
style: run Prettier formatting on /client
ilsea Oct 8, 2026
f52fa02
Merge branch 'main' into 29-create-book-details-page
ilsea Oct 8, 2026
6cc0ebc
fix: add browse page to its own folder inside pages/
ilsea Oct 8, 2026
827241c
style: run Prettier formatting on recently updated files
ilsea Oct 8, 2026
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
2 changes: 1 addition & 1 deletion client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/bookishFavicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>t14-project-2</title>
</head>
Expand Down
6 changes: 6 additions & 0 deletions client/public/bookishFavicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 6 additions & 8 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import { BrowserRouter, Routes, Route } from "react-router";
import AppShell from "./components/AppShell/AppShell";
import { BrowsePage } from "./pages/BrowsePage";
import { BrowserRouter, Routes, Route } from 'react-router';
import { AppShell } from './components/AppShell/AppShell';
import { BookDetailPage } from './pages/BookDetailPage/BookDetailPage';
import { BrowsePage } from './pages/BrowsePage/BrowsePage';

/* ---------- Temporary placeholder pages (delete when the real ones exist) ---------- */
function ShelfPlaceholder() {
return <h1>My shelf page</h1>;
}
function DetailPlaceholder() {
return <h1>Book detail page</h1>;
}

/* ---------- Routes ---------- */
export default function App() {
export function App() {
return (
<BrowserRouter>
<Routes>
<Route element={<AppShell />}>
<Route index element={<BrowsePage />} />
<Route path="shelf" element={<ShelfPlaceholder />} />
<Route path="book/:id" element={<DetailPlaceholder />} />
<Route path="book/:id" element={<BookDetailPage />} />
</Route>
</Routes>
</BrowserRouter>
Expand Down
14 changes: 8 additions & 6 deletions client/src/components/AppShell/AppShell.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
flex-direction: column;
min-height: 100vh;
min-height: 100dvh; /* mobile browsers: ignores the collapsing address bar */
padding-bottom: calc(var(--nav-height) + var(--space-3)); /* room so the fixed nav doesn't cover the footer */
padding-bottom: calc(
var(--nav-height) + var(--space-3)
); /* room so the fixed nav doesn't cover the footer */
background: var(--color-page);
color: var(--color-text);
}
Expand Down Expand Up @@ -107,8 +109,8 @@
.footer {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
align-items: center;
text-align: center;
gap: var(--space-2);
}

Expand All @@ -130,7 +132,7 @@
/* ================= Desktop: from 768px and up ================= */
@media (min-width: 48rem) {
.shell {
padding-bottom: var(--space-1);
padding-bottom: var(--space-1);
}

.inner {
Expand All @@ -149,7 +151,7 @@
min-height: 4.5rem;
}

.logo {
.logo {
align-self: center;
}

Expand Down Expand Up @@ -196,4 +198,4 @@
align-items: center;
justify-content: space-between;
}
}
}
145 changes: 72 additions & 73 deletions client/src/components/AppShell/AppShell.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,85 @@
// ===== Imports =====
import { Outlet, Link, NavLink, useNavigate, useSearchParams } from "react-router-dom";
import { Outlet, Link, NavLink, useNavigate, useSearchParams } from 'react-router-dom';
import styles from './AppShell.module.css';
import { SearchField } from "../SearchField/SearchField";
import { LogoIcon } from "../../icons/LogoIcon";
import { BrowseIcon } from "../../icons/BrowseIcon";
import { ShelfIcon } from "../../icons/ShelfIcon";
import { SearchField } from '../SearchField/SearchField';
import { LogoIcon } from '../../icons/LogoIcon';
import { BrowseIcon } from '../../icons/BrowseIcon';
import { ShelfIcon } from '../../icons/ShelfIcon';

export default function AppShell() {
export function AppShell() {
// ===== Read the current search =====
const [searchParam] = useSearchParams();
const q = searchParam.get('q') ?? '';

// ===== Read the current search =====
const [searchParam] = useSearchParams();
const q = searchParam.get("q") ?? "";
const navigate = useNavigate();

const navigate = useNavigate();

function handleSearch(query: string) {
if (query === "") {
navigate("/");
} else {
navigate("/?q=" + encodeURIComponent(query));
}
function handleSearch(query: string) {
if (query === '') {
navigate('/');
} else {
navigate('/?q=' + encodeURIComponent(query));
}
}

return (
// ===== Shell wrapper =====
<div className={styles.shell}>
{/* ===== Header ===== */}
<header className={styles.header}>
<div className={`${styles.inner} ${styles.headerInner}`}>
{/* ===== Logo ===== */}
<Link to="/" className={styles.logo}>
<LogoIcon />
Bookish
</Link>
return (
// ===== Shell wrapper =====
<div className={styles.shell}>
{/* ===== Header ===== */}
<header className={styles.header}>
<div className={`${styles.inner} ${styles.headerInner}`}>
{/* ===== Logo ===== */}
<Link to="/" className={styles.logo}>
<LogoIcon />
Bookish
</Link>

{/* ===== Search field ===== */}
<div className={styles.search}>
<SearchField value={q} onSearch={handleSearch} />
</div>
{/* ===== Search field ===== */}
<div className={styles.search}>
<SearchField value={q} onSearch={handleSearch} />
</div>

{/* ===== Main navigation ===== */}
<nav aria-label="Main" className={styles.nav}>
<NavLink
to="/"
end
className={({ isActive }) =>
isActive ? `${styles.navLink} ${styles.active}` : styles.navLink
}
>
<BrowseIcon />
Browse
</NavLink>
<NavLink
to="/shelf"
className={({ isActive }) =>
isActive ? `${styles.navLink} ${styles.active}` : styles.navLink
}
>
<ShelfIcon />
My shelf
</NavLink>
</nav>
</div>
</header>
{/* ===== Main navigation ===== */}
<nav aria-label="Main" className={styles.nav}>
<NavLink
to="/"
end
className={({ isActive }) =>
isActive ? `${styles.navLink} ${styles.active}` : styles.navLink
}
>
<BrowseIcon />
Browse
</NavLink>
<NavLink
to="/shelf"
className={({ isActive }) =>
isActive ? `${styles.navLink} ${styles.active}` : styles.navLink
}
>
<ShelfIcon />
My shelf
</NavLink>
</nav>
</div>
</header>

{/* ===== Header divider ===== */}
<div className={styles.divider} aria-hidden="true" />
{/* ===== Header divider ===== */}
<div className={styles.divider} aria-hidden="true" />

{/* ===== Page content slot ===== */}
<main className={styles.main}>
<div className={styles.inner}>
<Outlet />
</div>
</main>
{/* ===== Page content slot ===== */}
<main className={styles.main}>
<div className={styles.inner}>
<Outlet />
</div>
</main>

{/* ===== Footer ===== */}
<footer className={styles.footer}>
<div className={`${styles.inner} ${styles.footerInner}`}>
<span className={styles.wordmark}>Bookish</span>
<span>Book data from Google Books · IT2810 Prosjekt 2</span>
</div>
</footer>
{/* ===== Footer ===== */}
<footer className={styles.footer}>
<div className={`${styles.inner} ${styles.footerInner}`}>
<span className={styles.wordmark}>Bookish</span>
<span>Book data from Google Books · IT2810 Prosjekt 2</span>
</div>
);
}
</footer>
</div>
);
}
60 changes: 24 additions & 36 deletions client/src/components/BookCard/BookCard.module.css
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
/* ===== List item ===== */
.item {
min-width: 0; /* lets long titles shrink instead of stretching the grid */
min-width: 0;
/* lets long titles shrink instead of stretching the grid */
}

/* ===== Card container ===== */
.card {
position: relative; /* anchor for the stretched link and the ribbon */
position: relative;
/* anchor for the stretched link and the ribbon */
display: flex;
flex-direction: column;
gap: var(--space-3);
}

/* ===== Cover sits above the shelf ===== */
/* So the cover's shadow falls onto the shelf instead of being painted over
/* ===== Cover sits above the plank ===== */
/* So the cover's shadow falls onto the shelf plank instead of being painted over
by a neighbouring card's plank */
.coverWrap {
position: relative;
Expand All @@ -29,16 +31,19 @@
z-index: 0;
height: 10px;
}

.plank::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -100vw;
right: -100vw;
background: linear-gradient(var(--color-shelf-edge) 0 2px, var(--color-shelf) 2px);
border-bottom: 2px solid rgb(0 0 0 / 0.25); /* opaque "shadow": a real box-shadow would stack across cards */
pointer-events: none; /* otherwise it blocks clicks on neighbouring cards */
background: linear-gradient(var(--color-shelf-plank-edge) 0 2px, var(--color-shelf-plank) 2px);
border-bottom: 2px solid rgb(0 0 0 / 0.25);
/* opaque "shadow": a real box-shadow would stack across cards */
pointer-events: none;
/* otherwise it blocks clicks on neighbouring cards */
}

/* ===== Text ===== */
Expand All @@ -47,13 +52,16 @@
flex-direction: column;
gap: 3px;
}

.title {
margin: 0;
font-family: var(--font-heading);
font-weight: 600;
font-size: var(--text-lg); /* mockup uses 18px; adjust if you like */
font-size: var(--text-lg);
/* mockup uses 18px; adjust if you like */
line-height: 1.2;
}

.meta {
font-size: var(--text-sm);
color: var(--color-text-soft);
Expand All @@ -64,12 +72,14 @@
color: inherit;
text-decoration: none;
}

.link::after {
/* invisible layer over the whole card */
content: '';
position: absolute;
inset: 0;
}

.card:hover .link {
text-decoration: underline;
}
Expand All @@ -78,6 +88,7 @@
.link:focus-visible {
outline: none;
}

.card:has(.link:focus-visible) {
outline: 2px solid var(--color-focus);
outline-offset: 4px;
Expand All @@ -90,43 +101,20 @@
gap: 2px;
align-items: center;
}

.starOn {
fill: var(--color-star);
}

.starOff {
fill: var(--color-line);
}

/* ===== Ribbon ===== */
/* ===== Ribbon position (the look lives in ShelfRibbon.module.css) ===== */
.ribbon {
position: absolute;
top: 0;
right: 4px;
z-index: 2; /* above the cover and the stretched link, so it stays clickable */
width: 44px; /* 44px-wide tap area; the drawn ribbon is only 24px */
height: 48px;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 0;
border: 0;
background: none;
cursor: pointer;
}
.ribbon:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: -2px;
}

/* Not saved: pale outline ribbon */
.ribbon path {
fill: rgb(255 253 248 / 0.9);
stroke: var(--color-ribbon);
stroke-width: 1.8;
}
/* Saved: solid ribbon. Styled from aria-pressed, so no extra class is needed */
.ribbon[aria-pressed='true'] path {
fill: var(--color-ribbon);
stroke: rgb(255 255 255 / 0.8);
stroke-width: 1;
z-index: 2;
/* above the cover and the stretched link, so it stays clickable */
}
Loading