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
29 changes: 26 additions & 3 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
function App() {
return <h1>🥂Project 2🥂</h1>;
import { BrowserRouter, Routes, Route } from "react-router";
import AppShell from "./components/AppShell/AppShell";

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

export default App;
/* ---------- Routes ---------- */
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route element={<AppShell />}>
<Route index element={<BrowsePlaceholder />} />
<Route path="shelf" element={<ShelfPlaceholder />} />
<Route path="book/:id" element={<DetailPlaceholder />} />
</Route>
</Routes>
</BrowserRouter>
);
}
199 changes: 199 additions & 0 deletions client/src/components/AppShell/AppShell.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/* ---------- Shell: full-height column, so the footer sits at the bottom on short pages ---------- */
.shell {
--nav-height: 3.75rem; /* used by the fixed bottom nav and the space reserved for it */

display: flex;
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 */
background: var(--color-page);
color: var(--color-text);
}

/* ---------- Inner: the constant page width, shared by header, main and footer ---------- */
.inner {
width: 100%;
max-width: var(--content-width);
margin-inline: auto;
padding-inline: var(--space-4);
}

/* ---------- Header: full-width plum bar ---------- */
.header {
flex-shrink: 0;
padding-block: var(--space-4);
background: var(--color-header);
color: var(--color-header-text);
}

/* Mobile: logo and search stacked (the nav is fixed to the bottom, so it's out of this flow) */
.headerInner {
display: flex;
flex-direction: column;
gap: var(--space-3);
}

/* ---------- Logo ---------- */
.logo {
display: flex;
align-items: center;
align-self: flex-start; /* keeps the clickable area to the logo, not the full row */
gap: var(--space-2);
min-height: var(--tap-target);
font-family: var(--font-heading);
font-size: var(--text-xl);
font-style: italic;
font-weight: 600;
color: var(--color-header-text);
text-decoration: none;
}

/* ---------- Search wrapper: lets AppShell control the field's width ---------- */
.search {
min-width: 0; /* allows the field to shrink inside a flex row */
}

/* ---------- Nav: mobile = tab bar pinned to the bottom of the screen ---------- */
.nav {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 10; /* page content scrolls underneath */
display: flex;
background: var(--color-card);
border-top: 1px solid var(--color-line);
}

/* ---------- Nav link: mobile = icon above the label, equal-width tabs ---------- */
.navLink {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-1);
min-height: var(--nav-height);
font-size: var(--text-sm);
font-weight: 700;
color: var(--color-text-soft);
text-decoration: none;
border-top: 3px solid transparent; /* reserves space, so the active marker doesn't shift things */
}

/* ---------- Active link: must come after .navLink, because the later rule wins ---------- */
.active {
color: var(--color-header);
border-top-color: var(--color-accent);
}

/* ---------- Divider: decorative gold double line under the header ---------- */
.divider {
flex-shrink: 0;
height: 3px;
background: var(--color-header);
border-top: 1px solid var(--color-highlight);
border-bottom: 1px solid var(--color-highlight);
}

/* ---------- Main: grows to fill the free space, pushing the footer down ---------- */
.main {
flex: 1;
padding-block: var(--space-6);
}

/* ---------- Footer ---------- */
.footer {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: var(--space-2);
}

/* Mobile: wordmark above the text */
.footerInner {
display: flex;
flex-direction: column;
gap: var(--space-2);
}

.wordmark {
font-family: var(--font-heading);
font-size: var(--text-md);
font-style: italic;
font-weight: 600;
color: var(--color-header);
}

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

.inner {
padding-inline: var(--space-8);
}

/* Header: one row, fixed height */
.header {
padding-block: 0;
}

.headerInner {
flex-direction: row;
align-items: center;
gap: var(--space-8);
min-height: 4.5rem;
}

.logo {
align-self: center;
}

.search {
flex: 1;
max-width: 32.5rem;
}

/* Nav moves back into the header row, pushed to the right */
.nav {
position: static;
margin-left: auto;
gap: var(--space-2);
background: none;
border-top: 0;
}

.navLink {
flex: none;
flex-direction: row;
gap: var(--space-2);
min-height: var(--tap-target);
padding-inline: var(--space-3);
font-size: var(--text-md);
color: var(--color-header-text);
opacity: 0.8; /* inactive links are slightly dimmed */
border-top: 0;
border-bottom: 2px solid transparent;
}

.active {
color: var(--color-header-text);
opacity: 1;
border-bottom-color: var(--color-highlight);
}

.main {
padding-block: calc(var(--space-6) * 2);
}

/* Footer: wordmark left, text right */
.footerInner {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
}
86 changes: 86 additions & 0 deletions client/src/components/AppShell/AppShell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// ===== Imports =====
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";

export default function AppShell() {

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

const navigate = useNavigate();

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>

{/* ===== 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>

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

{/* ===== 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>
</div>
);
}
17 changes: 4 additions & 13 deletions client/src/components/BookCard/BookCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Link } from 'react-router-dom';
import type { Book } from '../../types/book';
import { BookCover } from '../BookCover/BookCover';
import styles from './BookCard.module.css';
import { StarIcon } from '../../icons/StarIcon';
import { RibbonIcon } from '../../icons/RibbonIcon';

/* ---------- Types: the contract with BookGrid and the pages ---------- */
type BookCardProps = {
Expand Down Expand Up @@ -66,16 +68,7 @@ function StarRow({ value }: { value: number }) {
// One image for screen readers, so the five SVGs are hidden
<span className={styles.stars} role="img" aria-label={`Your rating: ${value} out of 5`}>
{[1, 2, 3, 4, 5].map((n) => (
<svg
key={n}
className={n <= value ? styles.starOn : styles.starOff}
width="15"
height="15"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path d="M12 2l3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z" />
</svg>
<StarIcon key={n} className={n <= value ? styles.starOn : styles.starOff} />
))}
</span>
);
Expand All @@ -99,9 +92,7 @@ function FavoriteRibbon({
aria-label={`Save "${title}" to My shelf`} // ...so the label stays constant
onClick={onToggle}
>
<svg width="24" height="40" viewBox="0 0 24 40" aria-hidden="true">
<path d="M1 0H23V37L12 29L1 37Z" />
</svg>
<RibbonIcon />
</button>
);
}
8 changes: 2 additions & 6 deletions client/src/components/BookCover/BookCover.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react';
import styles from './BookCover.module.css';
import { SparkleIcon } from '../../icons/SparkleIcon';

const TONE_COUNT = 6;

Expand Down Expand Up @@ -39,12 +40,7 @@ export function BookCover({ id, title, author, coverUrl, eager = false }: Props)
aria-hidden="true"
>
<div className={styles.frame}>
<svg className={styles.sparkle} viewBox="0 0 24 24">
<path
d="M12 1L14.6 9.4L23 12L14.6 14.6L12 23L9.4 14.6L1 12L9.4 9.4Z"
fill="currentColor"
/>
</svg>
<SparkleIcon className={styles.sparkle} />
<span className={styles.title}>{title}</span>
<span className={styles.rule} />
{author && <span className={styles.author}>{author}</span>}
Expand Down
Loading