diff --git a/README.md b/README.md
index e5cc0a2..dd1abaf 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,10 @@
A small book catalogue where you can search for books, save the ones you care about to your own shelf, and keep your own rating and notes on each.
-**Status:** first submission for Project 2 (IT2810). This is a design draft with mock data. The application itself is not built yet.
-**Group:** 14 · **VM address:** Not set up yet
+**Status:** second submission for Project 2 (IT2810). The database and backend run on our VM, and Browse and Book detail work with real data. Shelf saving, filters, sort and the My shelf page are not finished yet (see [Where we are](#2-where-we-are)).
+
+
+**Group:** 14 · **Live site:** (reachable from eduroam or through Cisco VPN)
---
@@ -17,19 +19,47 @@ Finding a book to read and remembering what you thought of it are two separate p
- **Save it to your shelf** with the bookmark ribbon.
- **Rate it and write private notes** about it. These are yours only.
-We deliberately left out reviews from other users. There is no user login and not enough users to fill them, and there would be no way to tell if they were genuine. Prefetched ratings from Google Books would go stale as we are storing static data. Bookish is a way to keep track of *your own* books.
+We deliberately left out reviews from other users. There is no user login and not enough users to fill them, and there would be no way to tell if they were genuine. Ratings fetched from Google Books would go stale because we store the data. Bookish is a way to keep track of *your own* books.
+
+## 2. Where we are
+
+This README describes both what exists and what we planned. Each part is marked.
+
+| Feature | Status |
+|---|---|
+| **Browse:** search on title and author, done by the server over the whole catalogue and sent only on Enter or the search button. Results load in batches of 12 with a **Load more** button. The search text is kept in the URL. | Built |
+| **Book detail page:** fetched by id, so it also works from a direct URL | Built |
+| **Loading, empty and error states,** including "book not found" | Built |
+| **GraphQL API:** books, a single book, the shelf, genres. Supports search, genre and year filters, sorting and cursor paging. | Built |
+| **Shelf, rating and notes:** the detail page has the full interface (ribbon, star rating, notes, confirm dialogs) and the API can store everything, but the two are not connected. Changes are lost when you reload. **Note:** Parts of our docs say Shelf will not have a search feature, but this is yet to be decided as the search field lives in the app shell at the moment. | Partly |
+| **Anonymous user ID:** the server reads and checks it. The client does not create or send it yet. | Partly |
+| **Filter and sort controls** in the interface (the API is ready), kept in the URL like the search | Planned |
+| **My shelf page** (a placeholder for now), and connecting shelf, ratings and notes to the API | Planned |
+| **Cleaning the data:** plain-text descriptions, a short fixed genre list, one edition per book | Planned |
+| **Accessibility check, tests, dark mode,** a back link and previous/next on the detail page, clickable genres | Planned |
+
+**Design:** a warm ivory page with a plum header, plum and rose covers and a gold bookmark ribbon. One set of shared components and design tokens (colors, spacing, fonts) keeps the pages consistent. It is written **mobile first**: the mobile and desktop layouts use the same components, arranged differently (a bottom tab bar on mobile, a header nav on desktop). A dark mode comes later. The rules are in [`docs/styles-and-components.md`](docs/styles-and-components.md). Every decision and its status is in [`docs/decisions-and-contracts.md`](docs/decisions-and-contracts.md).
+
+### Trying the live site
+
+On the live site you can:
+1. **Search** for a title or an author in the header field (press Enter or use the search button). The result count and the URL (`?q=...`) change. Reload or press Back to see that the search is kept.
+2. **Load more** at the bottom of the list to get the next 12 books.
+3. **Open a book** to see its details, and try the ribbon, the star rating and the notes box. These work in the interface, but they are not saved yet, and a reload resets them.
+
+Not yet available: filters, sorting and the My shelf page (it shows a placeholder).
-## 2. Visual mockup
+## 3. Visual mockup
-**[Open the mockup: `docs/bookish-mockup.html`](docs/bookish-mockup.html)**
+**Open the mockup:** [`docs/bookish-mockup.html`](docs/bookish-mockup.html)
-*Preview:*
+*Preview:*
-
+
-The mockup is a single HTML file. GitHub shows HTML files as source code, so **download the file (or clone the repository) and open it in a web browser**. It works offline, but the fonts are loaded from Google Fonts, so without internet it uses fallback fonts.
+**How to view it:** the mockup is a single HTML file, and GitHub shows HTML files as source code. **Download the file (or clone the repository) and open it in a web browser.** It works offline, but the fonts come from Google Fonts, so without internet it uses fallback fonts.
-The mockup shows three pages on desktop and on mobile, and the three states of the rating and notes panel:
+It was made before we started building, so it shows the full idea, including parts that are not built yet, and the real app may differ from it. It shows three pages on desktop and on mobile:
| Page | What it shows |
|---|---|
@@ -39,92 +69,159 @@ The mockup shows three pages on desktop and on mobile, and the three states of t
Links between the pages work in the mockup. Buttons do not. The ratings, notes and counts in it are made up.
-> **Disclaimer:** the visual mockup was created in collaboration with Claude (an AI assistant from Anthropic). See [AI use](#7-ai-use).
+> **Disclaimer:** the mockup was created in collaboration with Claude (an AI assistant from Anthropic). See [AI use](#10-ai-use).
-## 3. The data
+## 4. The data
-**Source:** the [Google Books API](https://developers.google.com/books). We fetch books once and store a cleaned copy in our own database, so the app does not depend on Google while running. The app will be built to be able to handle larger amounts of data than this, which allows us to fetch more books for the app in the future, if we want to.
+**Source:** the [Google Books API](https://developers.google.com/books). We store the books in our own database, so the app does not depend on Google while running. The app is built to handle more books than we have imported, so we can add more later.
-**What a book looks like**
-See `docs/decisions.md`
+**Two databases, two sets of books:**
+- **The VM (the live site)** has a larger set of books from Google Books, added with the import script (`server/src/scripts/importBooks.ts`). The script fetches in batches with a pause between requests (at most 20 requests per run), remembers where it stopped, skips books without an id, title or author, and updates existing books instead of creating duplicates.
+- **Your own computer (development and testing)** uses about 30 books, also from Google Books, saved in a JSON file in the server code. `npm run seed:sample` loads them into your local database.
**Two kinds of stored data:**
-1. **Books:** read-only, filled in by a seed script.
-2. **Shelf entries:** created by the user. One per browser and book, holding the rating (1-5) and the note. These are stored in the database on our server.
+1. **Books:** read-only for users.
+2. **Shelf entries:** created by the user. One per browser and book, holding the rating (1-5) and the note (up to 1000 characters).
-**Plans for cleaning up fetched books**, because Google's data is uneven:
-- Remove HTML from descriptions.
-- Reduce Google's free-form categories to a short fixed list of genres.
-- Turn `publishedDate` into a year.
-- Keep one edition per book (Google lists many editions of the same book).
-- Accept that some books have no cover or description, and design for that.
+The book shape is in [`docs/decisions-and-contracts.md`](docs/decisions-and-contracts.md), and the schema is in `server/src/graphql/typeDefs.ts`.
-**Users:** there is no login at this point. The browser makes a random ID once and keeps it in `localStorage`. It is sent with every request so the server knows whose shelf to show. It is not a password: anyone who knows the ID can see that shelf. Due to the scope of this project, we have decided that this is fine for now.
+**Cleaning the data.** Google's data is uneven.
+- Done: `publishedDate` becomes a year, and books without an id, title or author are skipped.
+- Planned: remove HTML from descriptions, reduce Google's free-form categories to a short fixed list of genres, and keep one edition per book. Until then, genres are Google's own categories. Some books have no cover or description, and we design for that.
-## 4. How the app is meant to work
+**Users:** there is no login. The browser will create a random ID once and keep it in `localStorage`. It is sent with every request in the `x-user-id` header, so the server knows whose shelf to use. It is not a password: anyone who knows the ID can see and change that shelf. We decided this is fine for the scope of this project. The server side is built, the client side is planned.
-**Search, filter, sort, paging**
-- Search matches title and author, ignoring upper and lower case.
-- Filters: genre and year range. Sort: Title A-Z or Newest first.
-- Search, filters and sort are done by the **server over the whole catalogue**, not on the books already loaded.
-- Results load in batches with a **Load more** button.
-- The filter/search choices are kept in the URL, so reloading or pressing Back gives the same results.
+## 5. Technology
-**Shelf, rating and notes**
-- The bookmark ribbon on a card or on the detail page adds or removes the book from your shelf.
-- Rating a book or writing a note also puts it on your shelf. Removing a book that has a rating or note asks for confirmation first.
-- Notes are edited in place on the detail page: empty, saved, or editing. They are shown as plain text.
+| Part | What we use | Why |
+|---|---|---|
+| Frontend | React 19 and TypeScript, set up with Vite | Course requirement |
+| Routing | React Router | Routes, and the URL as shared state for the search |
+| Data fetching | Apollo Client | Sends the GraphQL queries and caches the results. A cache rule joins the pages of the book list, so Load more adds to it. |
+| State | Apollo cache for server data, the URL for the search, React state for the interface | We have not added a separate state library |
+| API | Our own GraphQL server in Node and TypeScript (Apollo Server) | Course requirement. The schema is the contract between backend and frontend. |
+| Database | MongoDB with Mongoose, installed directly on our VM (no Docker) | Books are nested documents (authors and genres are lists) |
+| Styling | Plain CSS with shared design tokens, and one CSS Module per component | Few dependencies, and shared values live in one place |
+| Quality | ESLint and Prettier (client and server), TypeScript | Vitest is installed in the client, but there are no tests yet |
+| Deployment | Apache serves the built client under `/project2`, backend on port 3001 | Course requirement |
-**Design**
-- A warm ivory page with a plum header, plum and rose covers, and a gold bookmark ribbon. A dark mode comes later.
-- One set of shared components and design tokens (colors, spacing, fonts) keeps all pages consistent.
-- **Mobile first approach:** Designing and building for mobile screens first, before scaling up to larger screens like tablets and desktops.
-- The mobile and desktop layout will use the same components, arranged differently.
+We reuse existing libraries wherever they make sense.
-## 5. Planned technology
+## 6. Running the project
-| Part | Plan |
-|---|---|
-| Frontend | React and TypeScript, set up with Vite |
-| API | Our own GraphQL server in Node and TypeScript |
-| Database | Installed directly on our VM (no Docker). Engine still to be decided. |
-| State | A state management library (to be decided) |
-| Quality | ESLint and Prettier, component tests with Vitest, API tests, and an automated end-to-end test |
-| Deployment | Apache serving the built app under `/project2`, backend on port 3001 |
-| Styling | Plain CSS with shared design tokens, and CSS Modules per component |
+### Run it on your own computer
+
+**You need:** Node.js 22 or newer, and MongoDB installed and running locally (by default on port 27017).
+
+**1. Get the code and install the dependencies.** Run each `npm install` in its own folder:
+
+```bash
+git clone
+cd /server
+npm install
+cd ../client
+npm install
+```
+
+**2. Create the server settings.** In `server/`, copy `.env.example` to `.env` (`cp .env.example .env`, or `copy .env.example .env` on Windows). The defaults work for a local MongoDB:
+
+```dotenv
+MONGODB_URI=mongodb://127.0.0.1:27017/project2
+PORT=3001
+```
+
+**3. Load the sample books into your local database.** From `server/`:
+
+```bash
+npm run build
+npm run seed:sample
+```
+
+About 30 books from Google Books are stored in a JSON file in the server code, and this command loads them into your local MongoDB. It needs no API key. MongoDB must be running, and `MONGODB_URI` in `.env` must point to it. The live site on the VM has a larger set of books.
-We will reuse existing libraries wherever they make sense and explain the important choices in this README as the project grows.
+**4. Start the server.** From `server/`:
-**Accessibility:** semantic HTML, real buttons and labels, keyboard use, visible focus, enough text contrast, and touch targets of at least 44 px. The bookmark ribbon is a real button with a pressed state, and the star rating is built so it can be used with keyboard and screen readers. Each book card contains one link, separate from the bookmark button, so there are no nested buttons.
+```bash
+npm run dev
+```
-**Sustainability:** Search is sent only when the user presses Enter or the search button, never while typing. Results load in small batches, covers are small and loaded lazily, and we use few fonts and few font weights.
+The terminal should say `Connected to MongoDB`. The API runs on . Open that address in a browser to try queries in Apollo Sandbox.
-## 6. How we work
+**5. Start the client.** From `client/`, in a second terminal:
+
+```bash
+npm run dev
+```
+
+Open the address Vite prints (usually ). Browse should show the books. In development the client talks to `http://localhost:3001/graphql`. To use another server, set `VITE_GRAPHQL_URL` in `client/.env`.
+
+**If no books show up:** check that the server terminal is still running and says `Connected to MongoDB`, that you ran `npm run seed:sample` after `npm run build`, and that nothing else is using port 3001. In the browser's Network tab, the `graphql` request should go to `localhost:3001`.
+
+### Other commands
+
+| Where | Command | What it does |
+|---|---|---|
+| `client/`, `server/` | `npm run lint` | Runs ESLint |
+| `client/`, `server/` | `npm run format` / `npm run format:check` | Formats the code with Prettier / checks the formatting |
+| `client/` | `npm run build` | Type-checks and builds the app into `dist/` |
+| `server/` | `npm run build` | Compiles the TypeScript into `dist/` |
+| `server/` | `npm start` | Runs the compiled server |
+
+### On the VM
+
+The app is at . The client is built with the base path `/project2/` and served by Apache. The built app sends its requests to `/project2/graphql`, which reaches the backend on port 3001. MongoDB is installed directly on the VM (no Docker). The server gets its settings from `server/.env`.
+
+### Secrets
+
+Secrets are never committed. They live in `server/.env`, which is git-ignored. `server/.env.example` lists the variable names. Anything starting with `VITE_` in the client is public, so no secrets go there.
+
+## 7. Testing
+
+We do **not have automated tests yet.** ESLint, Prettier and TypeScript run as checks on the code, but they are not tests. Vitest is installed in the client. Planned:
+- component tests with Vitest
+- tests of the API
+- an end-to-end test of a longer sequence (search, open a book, add it to the shelf, add a note, see it on the shelf)
+
+So far all testing has been manual.
+
+## 8. Accessibility and sustainability
+
+**Accessibility**
+- Built: semantic HTML (headings, lists, a description list for the book facts), one `` per page (the loading, empty and error states can take over the page heading), and result counts and loading that are announced to screen readers.
+- Our design goals are keyboard use, visible focus, enough text contrast, touch targets of at least 44 px, and a bookmark ribbon and star rating that work for keyboard and screen reader users, with no nested buttons on the cards.
+- Not done yet: a proper check against those goals (keyboard only, screen reader, contrast, and an automated tool such as Lighthouse or axe).
+
+**Sustainability**
+- Search is sent only when the user presses Enter or the search button, never while typing.
+- Results load in small batches (12 at a time), and the Apollo cache keeps pages that were already loaded.
+- The server does the searching and sorting, so the browser never needs the whole catalogue.
+- We use few fonts (Fraunces for headings, Figtree for the body).
+- Planned: smaller queries that ask only for the fields a card needs, and lazy-loaded covers.
+
+## 9. How we work
- **Four roles, two pairs.** A (Data) and B (API) build the backend. C (Browse and Shelf) and D (Design and Detail) build the frontend.
-- **We agree on contracts, not code.** The data shapes and operations between the backend and the frontend are fixed in [`docs/decisions.md`](docs/decisions.md). Each side is free to build its part its own way, as long as the results match. Changing a contract needs its own pull request.
-- **Git.** Every task is an issue, work happens on branches, and every change goes through a pull request with a review from someone else. Our git wiki has a "Git conventions" page that is meant to help us keep git clean and informational.
-- **Checklist.** The project requirements and open tasks are in [`docs/project-checklist.md`](docs/project-checklist.md).
-- **Secrets** are never committed. They live in a `.env` file on the server.
+- **We agree on contracts, not code.** The data shapes and operations between backend and frontend are fixed in [`docs/decisions-and-contracts.md`](docs/decisions-and-contracts.md). Each side builds its part its own way, as long as the results match. Changing a contract needs its own pull request.
+- **Git.** Every task is an issue, work happens on branches, and every change goes through a pull request with a review from someone else. Our git wiki has a "Git conventions" page that helps us keep git clean and informational.
+- **Project documents** (in `docs/`): [decisions and contracts](docs/decisions-and-contracts.md), [roles and rules](docs/roles-and-rules.md), [styles and components](docs/styles-and-components.md), and the [project checklist](docs/project-checklist.md) with the course requirements and our open tasks.
- Time logs and each member's contribution are delivered separately in the course's submission system, not in this repository.
-## 7. AI use
+## 10. AI use
-We use AI as an assistant and are responsible for understanding and checking everything it produces.
+We use AI as a tool and are responsible for understanding and checking everything it produces.
-- **The visual mockup (`docs/bookish-mockup.html`) was created in collaboration with Claude** (Anthropic). The group supplied the idea, the requirements, the color palette references and the design decisions. Claude wrote the HTML and CSS for the mockup and helped refine the layout, colors and simplifications through several rounds of feedback. The mockup is a design sketch and not part of the application code.
-- **Early project planning** was done with the help of Claude to quickly validate and visualise ideas to reduce the time spent on plans that wouldn't work out for us.
-- Later AI use in code, tests and documentation will be described here as the project grows.
+- **Planning:** we used AI to plan the project, to quickly try out and visualise ideas, and to avoid spending time on plans that would not work for us.
+- **The visual mockup (`docs/bookish-mockup.html`) was created in collaboration with Claude** (Anthropic). We supplied the idea, the requirements, the color palette references and the design decisions. Claude wrote the HTML and CSS and helped refine the layout and colors through several rounds of feedback. The mockup is a design sketch and not part of the application code.
+- **Code:** we used AI to review code and to generate pieces of it, mostly components based on our own descriptions. The intentions and decisions come from us. We have read the code we submitted, and understood it at the time we wrote it.
+- **Testing:** all manual testing was done by us.
+- **The plans changed along the way.** When something worked differently than planned, or we hit a problem, we changed the plan and the docs (see the changelogs in `docs/`).
+- AI let us build much more than we could have in this time, but the ideas and the final choices are ours.
-## 8. Team
+## 11. Team
| Role | Name |
|---|---|
-| A: Data | Ronny Engan|
+| A: Data | Ronny Engan |
| B: API | Adrian Hamnvik |
| C: Browse and Shelf | Line Sofie Flaate |
| D: Design and Detail | Ilse Resell |
-
-## 9. Running the project
-
-Setup and run instructions will be added here once there is code to run. The goal is that anyone can install and start the project by following this README.
\ No newline at end of file
diff --git a/client/src/App.tsx b/client/src/App.tsx
index e2ab86d..0326c8d 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,4 +1,4 @@
-import { BrowserRouter, Routes, Route } from 'react-router';
+import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AppShell } from './components/AppShell/AppShell';
import { BookDetailPage } from './pages/BookDetailPage/BookDetailPage';
import { BrowsePage } from './pages/BrowsePage/BrowsePage';
diff --git a/client/src/components/BookGrid/BookGrid.module.css b/client/src/components/BookGrid/BookGrid.module.css
index 0917d1f..c45db55 100644
--- a/client/src/components/BookGrid/BookGrid.module.css
+++ b/client/src/components/BookGrid/BookGrid.module.css
@@ -16,4 +16,4 @@
.withShelfPlank {
overflow-x: clip;
padding-inline: var(--space-2); /* room so focus rings and shadows aren't clipped */
-}
\ No newline at end of file
+}
diff --git a/client/src/data/searchBooks.ts b/client/src/data/searchBooks.ts
deleted file mode 100644
index 3ba79a1..0000000
--- a/client/src/data/searchBooks.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { Book } from '../types/book';
-
-export function searchBooks(books: Book[], query: string): Book[] {
- const term = query.trim().toLowerCase();
- if (term === '') return books; //if the seach is empty, show everything
-
- return books.filter(
- (book) =>
- book.title.toLowerCase().includes(term) ||
- book.authors.some((author) => author.toLowerCase().includes(term)),
- );
-}
diff --git a/client/src/pages/BrowsePage/BrowsePage.module.css b/client/src/pages/BrowsePage/BrowsePage.module.css
index 34586fd..5504949 100644
--- a/client/src/pages/BrowsePage/BrowsePage.module.css
+++ b/client/src/pages/BrowsePage/BrowsePage.module.css
@@ -31,9 +31,8 @@
}
@media (min-width: 40rem) {
-
/*tablets and bigger devices*/
.page {
padding: var(--space-8) var(--space-6);
}
-}
\ No newline at end of file
+}
diff --git a/client/src/pages/BrowsePage/BrowsePage.tsx b/client/src/pages/BrowsePage/BrowsePage.tsx
index 10a74c1..6e2e89f 100644
--- a/client/src/pages/BrowsePage/BrowsePage.tsx
+++ b/client/src/pages/BrowsePage/BrowsePage.tsx
@@ -1,5 +1,5 @@
import { useState } from 'react';
-import { useSearchParams } from 'react-router';
+import { useSearchParams } from 'react-router-dom';
import { useQuery } from '@apollo/client/react';
import type { Book } from '../../types/book';
import { BookGrid } from '../../components/BookGrid/BookGrid';
diff --git a/docs/decisions-and-contracts.md b/docs/decisions-and-contracts.md
index 807f4a6..edba100 100644
--- a/docs/decisions-and-contracts.md
+++ b/docs/decisions-and-contracts.md
@@ -2,93 +2,114 @@
[Mockup](bookish-mockup.html) (light mode)
-Mockup is a pure HTML file - open in a browser to render visually.
-**Note:** The mockup was created before we started building the app, so the real design might change during the project, and the mockup might not get updated when this happens.
+Mockup is a pure HTML file - open in a browser to render visually.
+**Note:** The mockup was created before we started building the app, so the real design might change during the project, and the mockup might not get updated when this happens.
+
+**How to read this document:** the decisions and the contract are the plan we agreed on. The **Status** columns and the "Resolved" list show where the code is now (Delivery 2, 16.10.2026). Status values: **Done**, **Partial**, **Planned**.
## The contract between backend and frontend
+The source of truth for the API is the schema in `server/src/graphql/typeDefs.ts`. The blocks below are a readable summary of it. If they ever differ, the schema wins and this document should be fixed (decision 11).
+
```ts
type Book = {
- id: string; // UNCERTAIN: Google Books id (googleBooksId) or Mongo _id? Must be stable across re-seeds
+ id: string; // the Google Books id (googleBooksId), same in every database. Mongo's _id is never exposed.
title: string;
subtitle: string; // "" when none
authors: string[]; // [] when none
publishedYear: number | null; // null when unknown
- genres: string[]; // UNCERTAIN: from a fixed list (see `genres` query), exact list not decided
+ genres: string[]; // see `genres` query. The final fixed list is not applied yet (see decision 7).
description: string; // "" when none; plain text, never HTML
coverURL: string; // "" when none -> the client shows a generated cover
pageCount: number | null; // null when unknown
language: string; // "" when unknown
+ myEntry: ShelfEntry | null; // the current user's entry. null when the book is not on their shelf
};
-// UNCERTAIN: how the user's own data reaches the client is not decided.
-// Option 1: a `myEntry` field on each Book. Option 2: separate lookups (saved ids + ratings).
-type ShelfEntry = { rating: number | null; note: string | null; updatedAt: string };
+type ShelfEntry = { rating: number | null; note: string | null; updatedAt: string }; // updatedAt is ISO 8601
```
```graphql
type Query {
books(search: String, genres: [String!], yearFrom: Int, yearTo: Int,
sort: BookSort = TITLE_ASC, first: Int = 12, after: String): BookPage!
- book(id: ID!): Book
- myShelf(sort: ShelfSort = RECENT): [Book!]!
+ book(id: ID!): Book # null if no book has this id
+ myShelf(sort: ShelfSort = RECENT): [Book!]! # needs the x-user-id header
genres: [String!]!
}
type BookPage { items: [Book!]!, nextCursor: String, totalCount: Int! }
enum BookSort { TITLE_ASC YEAR_DESC }
enum ShelfSort { RECENT TITLE_ASC RATING_DESC }
-type Mutation {
+type Mutation { # all need the x-user-id header
setShelved(bookId: ID!, shelved: Boolean!): Book!
setRating(bookId: ID!, rating: Int): Book! # 1-5, null clears
- setNote(bookId: ID!, note: String): Book! # max 1000 characters, null deletes
+ setNote(bookId: ID!, note: String): Book! # max 1000 characters, null or empty deletes
}
```
-Genre filter: a book matches if it has **any** of the selected genres. Search: matches title and author, case-insensitive. Results follow the chosen sort.
-UserID: Client only knows its own ID. Server stores IDs along with shelf entries, but the client never needs to know about any other IDs than its own, so it's not part of the contract.
+**Rules**
+- Genre filter: a book matches if it has **any** of the selected genres.
+- Search: every word must appear in the title or in one of the authors, case-insensitive. Max 200 characters.
+- Results follow the chosen sort. `first` defaults to 12 and is capped at 50. `yearFrom` after `yearTo` is rejected.
+- Books with an unknown year are left out when `yearFrom` or `yearTo` is set, and come last in `YEAR_DESC`.
+
+**User ID (this is now part of the contract):** the client sends its own anonymous ID in the `x-user-id` header (8-64 characters: letters, digits, `_` or `-`; a `crypto.randomUUID()` fits). `books` and `book` work without it (then `myEntry` is always null). `myShelf` and all mutations fail with the error code `USER_ID_REQUIRED` without it. The client never needs to know about any other user's ID.
+
+**Error codes** (in `error.extensions.code`): `BAD_USER_INPUT` (invalid argument), `NOT_FOUND` (mutation on a book that does not exist), `USER_ID_REQUIRED`.
## Decisions that affect several parts
-| # | Decision | Affects | Consequence |
-| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------- |
-| 1 | **Scope:** three pages (Browse, Book detail, My shelf). No reviews from other users. Users only rate and write notes on their own books. | Everyone | No public review data anywhere. |
-| 2 | **No Google ratings.** No external rating on cards, no rating filter, no "highest rated" sort. Because rating gets stale when we don't fetch from Google Books regularly. | A, B, C, D | Cards show only the user's own rating, if set. |
-| 3 | **A shelf entry** (one per browser ID and book) holds rating and note. An entry existing means the book is on the shelf. Rating or writing a note creates it if it is missing. Removing from the shelf deletes it, including rating and note. | A, B, C, D | The client asks for confirmation before removing a book that has a rating or note. |
-| 4 | **Anonymous identity:** the browser creates a random ID once, keeps it in `localStorage`, and sends it with every request. Shelf data is stored in the database under that ID. | B, C | It is not a password. Anyone with the ID can see and change that shelf. We note this in the README. |
-| 5 | **Search, filter and sort run on the server** over the whole dataset. Results load with a Load more button and a cursor (`after` / `nextCursor`). Sort options: Title A-Z (default) and Newest first. | A, B, C | A: the database must handle this fast. C: reset the paging when search, filters or sort change. |
-| 6 | **Tie-break rule:** every sort ends with `id` as a last tie-breaker (for example: year, then id). The cursor is an opaque string that only the server understands. `totalCount` counts the filtered set. | A, B, C | Without a fixed order for equal values, "Load more" can repeat or skip books. C only passes the cursor back and never builds one. |
-| 7 | **The data is cleaned when seeded.** Descriptions are plain text, genres come from a fixed list, `publishedDate` becomes `publishedYear`, one edition per book. | A, B, C, D | The client never handles HTML from Google and never has to guess genre names. |
-| 8 | **Notes are plain text**, max 1000 characters, shown with line breaks kept, never as HTML. | B, D | The server enforces the limit. The client shows a counter. |
-| 9 | **Design tokens and shared components.** Colors, spacing, radius and fonts come from `tokens.css`. Styling is plain CSS: global `tokens.css`, `reset.css` and `base.css`, plus one CSS Module per component. `BookCover`, `BookCard`, `BookGrid`, `SearchField`, `Button`, `AppShell` (and later `ShelfButton` and `StarRating`) are reused, not copied. | C, D | Need a new value or variant? Add it to the shared file in a pull request. | |
-| 10 | **Secrets** live only in `server/.env` (git-ignored, never committed). `server/.env.example` lists the names. Nothing secret in the client. Anything starting with `VITE_` is public. | Everyone | If a secret is ever committed, tell the group and replace it. Deleting the file later does not help. |
-| 11 | **Contract changes** (the block above) go in their own pull request, with a heads-up to the people they touch. | Everyone | |
-| 12 | **Search is submitted with Enter or the search button, never while typing.** The applied query lives in the URL as `q`. | C, D | No debouncing needed. The search field in the shared header also works on the detail page by navigating to `/?q=…`. A new `q` resets the paging. |
-| 13 | **The URL holds the browse state:** `q`, genre(s), `yearFrom`, `yearTo`, `sort`. | C, D | Reload and Back give the same results. The filter panel, sort select and search all read and write the URL. |
+| # | Decision | Affects | Consequence | Status |
+| --- | --- | --- | --- | --- |
+| 1 | **Scope:** three pages (Browse, Book detail, My shelf). No reviews from other users. Users only rate and write notes on their own books. | Everyone | No public review data anywhere. | **Partial:** Browse and Book detail are built. My shelf is still a placeholder page. |
+| 2 | **No Google ratings.** No external rating on cards, no rating filter, no "highest rated" sort. Because rating gets stale when we don't fetch from Google Books regularly. | A, B, C, D | Cards show only the user's own rating, if set. | **Done:** the API has no external rating. |
+| 3 | **A shelf entry** (one per browser ID and book) holds rating and note. An entry existing means the book is on the shelf. Rating or writing a note creates it if it is missing. Removing from the shelf deletes it, including rating and note. | A, B, C, D | The client asks for confirmation before removing a book that has a rating or note. | **Partial:** server done. The Book detail page has the full UI, including both confirm dialogs, but it uses temporary local state and does not save yet. Browse ribbons are also local only. |
+| 4 | **Anonymous identity:** the browser creates a random ID once, keeps it in `localStorage`, and sends it with every request. Shelf data is stored in the database under that ID. | B, C | It is not a password. Anyone with the ID can see and change that shelf. We note this in the README. | **Partial:** the server reads and validates the `x-user-id` header. The client does not create or send an ID yet. |
+| 5 | **Search, filter and sort run on the server** over the whole dataset. Results load with a Load more button and a cursor (`after` / `nextCursor`). Sort options: Title A-Z (default) and Newest first. | A, B, C | A: the database must handle this fast. C: reset the paging when search, filters or sort change. | **Partial:** server supports all of it. The client has search and Load more. The filter and sort UI is planned. |
+| 6 | **Tie-break rule:** every sort ends with `id` as a last tie-breaker (for example: year, then id). The cursor is an opaque string that only the server understands. `totalCount` counts the filtered set. | A, B, C | Without a fixed order for equal values, "Load more" can repeat or skip books. C only passes the cursor back and never builds one. | **Done.** The cursor holds an offset. This is fine because the catalogue only changes when the import script runs. It can later be swapped for a position-based cursor without changing the client. |
+| 7 | **The data is cleaned when seeded.** Descriptions are plain text, genres come from a fixed list, `publishedDate` becomes `publishedYear`, one edition per book. | A, B, C, D | The client never handles HTML from Google and never has to guess genre names. | **Partial:** At the time of updating this document, the status of this is uncertain.|
+| 8 | **Notes are plain text**, max 1000 characters, shown with line breaks kept, never as HTML. | B, D | The server enforces the limit. The client shows a counter. | **Done.**|
+| 9 | **Design tokens and shared components.** Colors, spacing, radius and fonts come from `tokens.css`. Styling is plain CSS: global `tokens.css`, `reset.css` and `base.css`, plus one CSS Module per component. Shared components (`BookCover`, `BookCard`, `BookGrid`, `SearchField`, `Button`, `AppShell`, `ShelfRibbon`, `StarRatingInput`, ...) are reused, not copied. | C, D | Need a new value or variant? Add it to the shared file in a pull request. | **Done.** Component list: see `styles-and-components.md`. |
+| 10 | **Secrets** live only in `server/.env` (git-ignored, never committed). `server/.env.example` lists the names. Nothing secret in the client. Anything starting with `VITE_` is public. | Everyone | If a secret is ever committed, tell the group and replace it. Deleting the file later does not help. | **In use**. Continuously checked when committing/pushing |
+| 11 | **Contract changes** (the block above) go in their own pull request, with a heads-up to the people they touch. | Everyone | | **In use.** |
+| 12 | **Search is submitted with Enter or the search button, never while typing.** The applied query lives in the URL as `q`. | C, D | No debouncing needed. The search field in the shared header also works on the detail page by navigating to `/?q=…`. A new `q` resets the paging. | **Done.** |
+| 13 | **The URL holds the browse state:** `q`, genre(s), `yearFrom`, `yearTo`, `sort`. | C, D | Reload and Back give the same results. The filter panel, sort select and search all read and write the URL. | **Partial:** only `q` is in the URL. The rest comes with the filter and sort UI. |
## Pages and routes
-| Route | Page | Owner | Notes |
-|---|---|---|---|
-| `/` | Browse | C | Search, filters and sort live in the URL |
-| `/book/:id` | Book detail | D | `:id` is the book `id` (see open questions) |
-| `/shelf` | My shelf | C | Sort RECENT / TITLE_ASC / RATING_DESC, no search |
-
-## Open questions
-
-- Database engine (A + B decide).
-- Fonts: self-hosting approach for Fraunces and Figtree (C + D decide).
-- User ID handoff between client and server.
-- **Book `id`:** Google Books id or Mongo `_id`? It becomes the URL, the saved-books key, and the key for ratings and notes, so it must be stable across re-seeds.
-- **How the user's own data reaches the client:** a `myEntry` field on each `Book` (the mutations already return `Book!`, which points this way), or separate lookups (saved ids and ratings). Decides the props of `BookGrid`.
-- **Removal confirmation:** decision 3 says the client asks before removing a book with a rating or note. Does this apply on the cards too, or only on the detail page? Dialog or inline?
-- **Year filter edge case:** what happens if `yearFrom` is greater than `yearTo`? Validate on the client, the server, or both?
-- **Genre list:** who fixes the final list, and does it live in the seed script or in its own table?
-- **Empty results from filters:** can a filter combination give zero results, or does `books` return counts per genre so the client can hide empty ones?
-- **`updatedAt` on `ShelfEntry`:** needed for the RECENT sort on My shelf. Check that the schema exposes it.
+| Route | Page | Owner | Notes | Status |
+|---|---|---|---|---|
+| `/` | Browse | C | Search, filters and sort live in the URL | **Partial:** search and Load more. Filters and sort planned. |
+| `/book/:id` | Book detail | D | `:id` is the book `id` (the Google Books id). Works by direct URL. | **Partial:** shelf, rating and notes are not saved yet. |
+| `/shelf` | My shelf | C | Sort RECENT / TITLE_ASC / RATING_DESC, no search | **Planned:** placeholder page. |
+
+On the VM the app is served under `/project2`. The router uses Vite's `base` as its `basename`.
+
+## Resolved questions
+
+Open questions from previous versions of this doc, kept here so we don't lose track of the process.
+
+- **Database engine:** MongoDB (with Mongoose), installed on the VM.
+- **Book `id`:** the Google Books id (`googleBooksId`). It is the same in every database and stable across re-seeds. Mongo's `_id` is never exposed.
+- **How the user's own data reaches the client:** a `myEntry` field on each `Book` (option 1). Mutations return the updated `Book!`.
+- **User ID handoff:** the `x-user-id` header (see the contract above). Client side still to be built.
+- **`updatedAt` on `ShelfEntry`:** exposed by the API. It drives the RECENT sort on My shelf.
+- **Year filter edge case:** the server rejects `yearFrom` greater than `yearTo` with `BAD_USER_INPUT`. Client validation can be added with the filter UI.
+- **Removal confirmation on the detail page:** a dialog, for both removing a book with a rating or note and deleting a note.
+
+## Still open
+
+- **Removal confirmation on the cards** (Browse and My shelf): does it apply there too, or only on the detail page?
+- **Fonts:** self-hosting approach for Fraunces and Figtree.
+- **URL parameter names for filters:** `genre` or `genres`. Suggested: `genres`, to match the API.
+- **Genre list:** under construction, still unsure how to solve this practically.
+- **Empty results from filters:** can a filter combination give zero results, or should `books` return counts per genre so the client can hide empty ones? (Genre counts are in the "later" list.)
+- **My shelf search:** should My shelf be searchable? The current contract says no search.
## Changelog
- v1 (29.09.2026): created by Ilse.
- v2 (01.10.2026): `Book` type aligned with the backend model. CSS approach decided. Added decisions 12 and 13, the routes table, and more open questions.
-- v3 (02.10.2026): Added disclaimer about real design possibly diverging from initial design mockup.
\ No newline at end of file
+- v3 (02.10.2026): Added disclaimer about real design possibly diverging from initial design mockup.
+- v4 (16.10.2026): Delivery 2 update. Contract aligned with `typeDefs.ts` (`myEntry`, user ID header, error codes, limits). Added Status columns. Moved answered questions to "Resolved questions". This document update was made with the help of Claude (AI) (code analyse, compare to docs) to ensure that the document reflects actual changes to the app, and the current state.
\ No newline at end of file
diff --git a/docs/project-checklist.md b/docs/project-checklist.md
index bac14f8..2ca30a5 100644
--- a/docs/project-checklist.md
+++ b/docs/project-checklist.md
@@ -1,33 +1,33 @@
# Project checklist: things to keep in mind
-Drafted 2026-09-29 from the Project 2 description, the lectures and the Project 1 peer review. This document was created in the planning phase and might not get updated regularly.
+Made from the Project 2 description and the lectures. The checklist itself is the plan. Notes in *italics* show where we are.
-Roles: **A** Data, **B** API, **C** Browse + Shelf, **D** Design + Detail. See `roles.md`.
+Roles: **A** Data, **B** API, **C** Browse + Shelf, **D** Design + Detail. See `roles-and-rules.md`.
---
## 1. What the project requires
### Functionality
-- [ ] Search on single words, case-insensitive, and no unnecessary requests (debounce or a search button)
-- [ ] Result list that copes with large result sets (Load more, decided)
-- [ ] More details about each book
-- [ ] Sort and filter over the **whole** result set, not only what is loaded in the browser
-- [ ] User-generated data, saved **on the database server** and shown in the app (our shelf, ratings and notes)
+- [x] Search on single words, case-insensitive, and no unnecessary requests (debounce or a search button)
+- [x] Result list that copes with large result sets (Load more, decided)
+- [x] More details about each book
+- [ ] Sort and filter over the **whole** result set, not only what is loaded in the browser — *server done, client UI missing*
+- [ ] User-generated data, saved **on the database server** and shown in the app (our shelf, ratings and notes) — *server done, client not connected*
- [ ] Universal design / accessibility
- [ ] Sustainable web development, through design choices
-- [ ] Good design and sensible choices that fit the data
-- [ ] Database and backend hosted on **our group's VM**
+- [x] Database and backend hosted on **our group's VM**
+- [ ] Back link and prev/next on the detail page
### Technology
-- [ ] React + TypeScript, set up with Vite
+- [x] React + TypeScript, set up with Vite
- [ ] **State management** in use (for example Apollo local state, Redux, MobX). We must be able to point to where and why.
-- [ ] Our own GraphQL backend in Node/TypeScript
-- [ ] Database installed **directly on the VM**, not in Docker
+- [x] Our own GraphQL backend in Node/TypeScript
+- [x] Database installed **directly on the VM**, not in Docker
- [ ] Reuse of good third-party libraries is encouraged
### Testing and quality
-- [ ] Linting **and** Prettier (ESLint comes with Vite, Prettier must be added). Both in client and server.
+- [x] Linting **and** Prettier (ESLint comes with Vite, Prettier must be added). Both in client and server.
- [ ] Component tests with **Vitest**
- [ ] Some **automated end-to-end test** (a longer sequence of interactions)
- [ ] Tests of the **API**
@@ -42,11 +42,11 @@ Roles: **A** Data, **B** API, **C** Browse + Shelf, **D** Design + Detail. See `
- [ ] Take part in the peer review after each delivery (individual)
### VM and deployment
-- [ ] App served through Apache at `http://it2810-xx.idi.ntnu.no/project2`
-- [ ] Backend preferably on port **3001**
+- [x] App served through Apache at `http://it2810-xx.idi.ntnu.no/project2`
+- [x] Backend preferably on port **3001**
- [ ] Everything needed to start the backend is available to the teaching staff (documented in Git, or in a file readable with sudo on the VM). No real secrets in Git.
-**Storage note:** Project 2 does not list `localStorage` or `sessionStorage` as requirements (Project 1 did). We use `localStorage` for the anonymous ID. `sessionStorage` is optional (for example an unsaved note draft).
+**Storage note:** Project 2 does not list `localStorage` or `sessionStorage` as requirements. We plan to use `localStorage` for the anonymous ID (not built yet). `sessionStorage` is optional (for example an unsaved note draft).
---
@@ -65,27 +65,31 @@ Every delivery is used for peer review, so the earlier ones are for learning. Ai
## 3. Tasks that still need an owner
-| Task | Suggested owner | Needed by |
-|---|---|---|
-| Move the app into `client/`, add `server/`, check `.gitignore` | D | 1 |
-| Choose database engine | A + B | 2 |
-| Install the database on the VM (no Docker) | A | 2 |
-| Backend running on the VM on port 3001, restart after reboot | B | 2 |
-| Vite `base` path and router base for `/project2`, and Apache setting so page reloads on sub-pages work | B + C | 2 |
-| `.env.example` and a startup guide staff can follow | B | 2 |
-| ESLint and Prettier config for client and server, and a rule to run them before every pull request | D | 2 |
-| Choose state management and use it for something real | C | 3 |
-| Accessibility check (keyboard only, screen reader, contrast, an automated tool such as Lighthouse or axe) | D, then everyone fixes their own | 3 |
-| List of sustainability choices made (paging, lazy-loaded images, few fonts, small images, debounce, caching) and a README section on them | Everyone adds their own, D collects | 3 |
-| Component tests with Vitest for the shared components and pages | Each owner tests their own components | 4 |
-| API tests | B | 4 |
-| End-to-end test (for example search, open a book, add to shelf, add a note, see it on the shelf) and choosing a tool | C + D | 4 |
-| README (setup, choices and why, testing, AI use, sustainability, accessibility) | Each writes their own section, one person merges | 2 to 4 |
-| Time log, and the contribution summary for the submission system | Each keeps their own, one person compiles | Every delivery |
-| Peer reviews | Each person, individually | After each delivery |
+| Task | Suggested owner | Needed by | Status (16.10.26) |
+|---|---|---|---|
+| Move the app into `client/`, add `server/`, check `.gitignore` | D | 1 | Done |
+| Choose database engine | A + B | 2 | Done (MongoDB) |
+| Install the database on the VM (no Docker) | A | 2 | Done |
+| Backend running on the VM on port 3001, restart after reboot | B | 2 | Running, reboot untested |
+| Vite `base` path and router base for `/project2`, and Apache setting so page reloads on sub-pages work | B + C | 2 | Base paths done, sub-page reload untested |
+| `.env.example` and a startup guide staff can follow | B | 2 | To check |
+| ESLint and Prettier config for client and server, and a rule to run them before every pull request | D | 2 | Config done |
+| Choose state management and use it for something real | C | 3 | To decide |
+| Accessibility check (keyboard only, screen reader, contrast, an automated tool such as Lighthouse or axe) | D, then everyone fixes their own | 3 | Planned |
+| List of sustainability choices made (paging, lazy-loaded images, few fonts, small images, search on submit, caching) and a README section on them | Everyone adds their own, D collects | 3 | Planned |
+| Component tests with Vitest for the shared components and pages | Each owner tests their own components | 4 | Not started |
+| API tests | B | 4 | Unknown |
+| End-to-end test (for example search, open a book, add to shelf, add a note, see it on the shelf) and choosing a tool | C + D | 4 | Not started |
+| README (setup, choices and why, testing, AI use, sustainability, accessibility) | Each writes their own section, one person merges | 2 to 4 | In progress |
+| Time log, and the contribution summary for the submission system | Each keeps their own, one person compiles | Every delivery | Ongoing |
+| Peer reviews | Each person, individually | After each delivery | Ongoing |
---
+## 4. Quality practices we are keeping to
+
+These apply from the start, so they do not have to be fixed at the end.
+
## 4. Lessons from the Project 1 peer review
- **README must be accurate about tests.** Linting and building are not automated tests. Do not list them as such.
@@ -102,14 +106,15 @@ Every delivery is used for peer review, so the earlier ones are for learning. Ai
---
-## 5. Later, if there is time and energy
+## 5. Later, if there is time
- Dark mode (same tokens with a second set of values)
-- Genre counts in the filters
- Load more may be refined into infinite scroll only if it stays accessible
- Extra pages, such as an author page
+- Clickable genre chips on the detail page
+- Narrow the list queries to only the fields the cards need
+
+
+## Changelog
-## 6. Open questions
-- Database engine
-- CSS approach (CSS Modules or plain files), fonts, UI library
-- Which end-to-end tool
-- Who compiles the README and the contribution summary
\ No newline at end of file
+- v1 (29.09.2026): drafted from the Project 2 description and the lectures.
+- v2 (16.10.2026): Delivery 2 update. Ticked what is done, added a status column for the tasks. This document update was made with the help of Claude (AI) (code analyse, compare to docs) to ensure that the document reflects actual changes to the app, and the current state.
diff --git a/docs/roles-and-rules.md b/docs/roles-and-rules.md
index 3b8be2c..27aa2d6 100644
--- a/docs/roles-and-rules.md
+++ b/docs/roles-and-rules.md
@@ -1,10 +1,8 @@
# Roles
-v1: Created by Ilse, 29.09.2026
-
## How we work
-- **We agree on the contracts, not the code.** Each role owns its side of an interface (the data shapes and operations in `decisions.md`). How you build your side is up to you, as long as the result matches the "contract".
+- **We agree on the contracts, not the code.** Each role owns its side of an interface (the data shapes and operations in `decisions-and-contracts.md`). How you build your side is up to you, as long as the result matches the "contract".
- Need to change a contract? Propose it in its own pull request and tell the people it affects.
- Backend pair: **A + B**. Frontend pair: **C + D**. Pairs review each other, and anything that changes the contract gets one reviewer from each pair.
@@ -13,11 +11,24 @@ v1: Created by Ilse, 29.09.2026
| Role | Owns | Delivers to the others | Needs from the others |
| ---------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| **A: Data** | Google Books fetching and cleaning, the database on the VM | Books in the database in the agreed Book shape. First: `sample-books.json` (about 40 books). | The Book contract (agreed in the meeting) |
-| **B: API** | The GraphQL server, shelf mutations, deployment on the VM | A server on port 3001 that answers the schema in `decisions.md` | Books from A (sample file first, database later) |
+| **B: API** | The GraphQL server, shelf mutations, deployment on the VM | A server on port 3001 that answers the schema in `decisions-and-contracts.md` | Books from A (sample file first, database later) |
| **C: Browse + Shelf** | Browse page, search, filters, sort, Load more, the Shelf page, data fetching in the client | Working Browse and Shelf pages | Shared components from D, the API from B (sample file until then) |
-| **D: Design + Detail** | Design tokens, shared components, Detail page, notes panel | `tokens.css`, BookCover, BookCard, ShelfButton, StarRating, Detail page | The notes mutations from B |
+| **D: Design + Detail** | Design tokens, shared components, Detail page, notes panel | `tokens.css`, BookCover, BookCard, ShelfRibbon, StarRatingInput, Detail page | The notes mutations from B |
+
+## Status at Delivery 2 (16.10.2026)
+
+The table above is the plan. This is where the code is now, per area. Who built what is in the git history. Persons A+B are responsible for backend, C+D are responsible for frontend. Within the pairs we have collaborated and split work between us as needed to make sure everyone is contributing. Some of the tasks turned out to be bigger or smaller than we thought, and we have divided tasks differently when needed. The table below might not reflect who did what, but rather which area a task belongs to.
+
+| Area | Built | Still to do |
+|---|---|---|
+| **A: Data** | Resumable Google Books import script (limited number of requests per run), a sample-books seed script, books stored in MongoDB | Map original categories to fixed genres. Ensure imported data is cleaned as described in decision 7. |
+| **B: API** | GraphQL server with `books`, `book`, `myShelf`, `genres` and the shelf mutations (`setShelved`, `setRating`, `setNote`). Input validation, error codes, anonymous user ID header | API tests (see the checklist) |
+| **C: Browse + Shelf** | Apollo client, Browse page with server-side search, Load more, loading/empty/error states | Filters and sort (also in the URL), the Shelf page, sending the user ID, saving shelf data through the API |
+| **D: Design + Detail** | Design tokens, shared components, AppShell, Detail page with rating, notes and confirm dialogs | Hosting fonts, accessibility check, dark mode (later) |
-## Where to start
+## Initial plan: where to start (29.09.2026)
+
+This was the starting plan. Parts of it (sample files, three hard-coded books) are no longer needed because the real backend exists.
**A: Data**
@@ -34,7 +45,7 @@ v1: Created by Ilse, 29.09.2026
**C: Browse + Shelf**
- Set up the Apollo client and a Browse page that shows `sample-books.json` in a grid.
-- Add search with debounce, then filters, sort and Load more. Keep the choices in the URL.
+- Add search (submitted with Enter or the search button, decision 12), then filters, sort and Load more. Keep the choices in the URL.
- Switch from the sample file to the real API when B's server answers.
**D: Design + Detail**
@@ -47,19 +58,19 @@ v1: Created by Ilse, 29.09.2026
## Example: how one decision reaches all four roles
-**Decision:** search, filter and sort run on the server over the whole dataset. Results load in batches with a cursor. Every sort ends with `id` as a tie-breaker (decisions 5 and 6 in `decisions.md`).
+**Decision:** search, filter and sort run on the server over the whole dataset. Results load in batches with a cursor. Every sort ends with `id` as a tie-breaker (decisions 5 and 6 in `decisions-and-contracts.md`).
| Role | What the contract requires of this side | Left to you |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| **A: Data** | The database answers "filtered, sorted, one page" over the whole collection, fast. Every sort ends with `id`. | Database engine, indexes, how search matching works, how the cursor is built |
| **B: API** | Accept the arguments in the schema. Turn the GraphQL arguments into one database query. Return `items`, `nextCursor` and a `totalCount` for the _filtered_ set. Cap the page size. | Resolver structure, cursor format (it is opaque), validation details |
-| **C: Browse + Shelf** | Send search, genres, years and sort as query variables. Never sort or filter the loaded books in the browser. Pass `nextCursor` back as `after`. Start again from page one when search, filters or sort change. Show `totalCount`. | State handling, debounce, URL handling, loading/empty/error states |
+| **C: Browse + Shelf** | Send search, genres, years and sort as query variables. Never sort or filter the loaded books in the browser. Pass `nextCursor` back as `after`. Start again from page one when search, filters or sort change. Show `totalCount`. | State handling, URL handling, loading/empty/error states |
| **D: Design + Detail** | Nothing to build for this. Shared components (like BookCard) must not assume anything about list length or order. | Everything else |
**Where the roles meet**
-- **A and B** agree one function that takes the filters and returns items, cursor and count. They pick its name and shape between them.
-- **B and C** meet at the GraphQL schema in `decisions.md`. It is the only thing C needs to know about the backend.
+- **A and B** agree one function that takes the filters and returns items, cursor and count. They pick its name and shape between them. (It is now `findBooks` in `server/src/services/bookService.ts`.)
+- **B and C** meet at the GraphQL schema (`server/src/graphql/typeDefs.ts`, summarized in `decisions-and-contracts.md`). It is the only thing C needs to know about the backend.
**What goes wrong if a side misses its part**
@@ -67,6 +78,11 @@ v1: Created by Ilse, 29.09.2026
- **The client sorts what it has loaded:** it looks right with 40 sample books and breaks with thousands.
- **`totalCount` counts all books, not the filtered ones:** "Showing 12 of 4,318" stays wrong after filtering.
-**How to read this** Every decision in `decisions.md` works the same way. The contract is the fixed point, and each role checks its own side against it. Everything in "Left to you" is yours to decide.
+**How to read this** Every decision in `decisions-and-contracts.md` works the same way. The contract is the fixed point, and each role checks its own side against it. Everything in "Left to you" is yours to decide.
+
+**And:** remember to document your choices.
+
+## Changelog
-**And:** remember to document your choices.
\ No newline at end of file
+- v1: Created by Ilse, 29.09.2026
+- v2: Delivery 2 update, 16.10.2026. Fixed file names and component names, replaced the debounce plan with the submit-on-Enter decision (decision 12), and added a status table. This document update was made with the help of Claude (AI) (code analyse, compare to docs) to ensure that the document reflects actual changes to the app, and the current state.
\ No newline at end of file
diff --git a/docs/styles-and-components.md b/docs/styles-and-components.md
index f30ea2c..dbe4044 100644
--- a/docs/styles-and-components.md
+++ b/docs/styles-and-components.md
@@ -1,41 +1,57 @@
# Rules and information regarding styles, design and components
+**Status labels:** **Built** = exists in the code. **Planned** = not built yet. Updated for Delivery 2 (16.10.2026).
+
## Folder structure
-```
-src/
+```
+client/src/
+├── main.tsx, App.tsx entry point, and the routes (AppShell is the layout route)
+├── apollo/ client.ts (Apollo Client + cache rules), queries.ts (GraphQL queries)
+├── data/ mockBooks.ts, searchBooks.ts (temporary mock data and helpers, see "Data fetching")
├── styles/ tokens.css, reset.css, base.css, index.css
-├── icons/ Logo, Browse, Shelf, Search
-├── components/
-│ ├── Button/ 3 variants used throughout app
-│ ├── BookCover/ image, fallback, color pick
-│ ├── BookCard/ BookCard + ribbon + starsdisplay
-│ ├── BookGrid/ render auto grid of BookCard, with shelf option
-│ ├── SearchField/ draft state, submit on Enter or button
-│ ├── FilterPanel/ genre, year, clear, mobile toggle
-│ ├── RatingInput/ radio group styled as stars
-│ ├── MyNotesPanel/ empty / saved / editing
-│ ├── StateMessage/ empty, error, (loading)
-│ └── AppShell/ shell + header + nav + footer
+├── icons/ BrowseIcon, ChevronIcon, FilterIcon, LogoIcon, RibbonIcon,
+│ SearchIcon, ShelfIcon, SparkleIcon, StarIcon (one component per icon)
+├── types/ book.ts (the Book type, mirrors the backend)
+├── components/ (every folder holds Name.tsx and Name.module.css)
+│ ├── AppShell/ shell + header + nav + footer
+│ ├── BookCard/ card for one book (cover, title, authors, ribbon)
+│ ├── BookCardSkeleton/ loading placeholder shaped like a BookCard
+│ ├── BookCover/ image, generated fallback, color pick
+│ ├── BookGrid/ auto grid of BookCard, with shelf option
+│ ├── Button/ 3 variants used throughout app
+│ ├── ConfirmDialog/ confirm popup (remove from shelf, delete note)
+│ ├── EmptyState/ "nothing here" message with optional action
+│ ├── ErrorState/ error message with retry
+│ ├── Eyebrow/ small label above a heading
+│ ├── LoadingState/ loading message for screen readers or visible
+│ ├── MyNotesPanel/ rating and note: empty / saved / editing
+│ ├── SearchField/ draft state, submit on Enter or button
+│ ├── ShelfRibbon/ the bookmark ribbon (add to / remove from shelf)
+│ └── StarRatingInput/ rating input, styled as stars
└── pages/
- ├── BrowsePage/ includes "Load more" + results count
- ├── BookDetailPage/ includes hero, facts, description, ribbon, tags
- └── ShelfPage/ includes page heading + sort
-```
+ ├── BrowsePage/ search results, "Load more", results count (Built)
+ └── BookDetailPage/ hero, facts, description, ribbon, tags, notes panel (Built)
+```
+
+**Planned (not built yet):** `FilterPanel` (genre, year, clear, mobile toggle) and `ShelfPage` (page heading + sort). `/shelf` shows a placeholder until the real page exists.
+
+**Names changed since v1:** `RatingInput` is now `StarRatingInput`. The planned `ShelfButton` became `ShelfRibbon`. `StateMessage` was split into `EmptyState`, `ErrorState` and `LoadingState`.
- Global CSS holds only tokens and element defaults.
- Every component owns its own `.module.css` and reads only `var(--token)` values, never raw hex.
+- **CSS is written mobile-first:** base styles are for small screens, and `min-width` media queries add the larger layouts.
## Global CSS
|File|Purpose|Rules|
|----|-------|-----|
|index.css|Imports the three below in order (tokens, reset, base)|No rules of its own. Order matters because base uses tokens|
|tokens.css|Design decisions, as CSS variables|Only :root{--...}, with no selectors or layout. The only place for raw hex/px values. Name by role (`--color-accent`), not appearance (`--color-purple`). Add a token only when it's a real decision or reused. Keep it tidy and readable!|
-|reset.css|Removes browser inconsistencies (box-sixing, margins, image behavior)|Tiny and generic. No colors or fonts, since "brand" values belong to tokens|
+|reset.css|Removes browser inconsistencies (box-sizing, margins, image behavior)|Tiny and generic. No colors or fonts, since "brand" values belong to tokens|
|base.css|Default look of plain HTML elements: body, headings, links, focus ring, `.visually-hidden`|Element selectors plus a few utilities only. If you write a specific class here, it belongs in a component module instead!|
## AppShell
-**AppShell is in** (`components/AppShell/`). It's the frame around every page: header (logo, search, nav), the main area, and the footer.
+**AppShell is built** (`components/AppShell/`). It's the frame around every page: header (logo, search, nav), the main area, and the footer.
**For anyone building a page:**
- **Don't render a ``, a header, a footer, or a search field in your page.** AppShell renders all of them, and your page appears inside its ``.
@@ -44,32 +60,58 @@ src/
- **Start your page with an ``.**
-- **Routes:** AppShell is a layout route, so every page goes in as a child of it in `App.tsx` (`/`, `/shelf`, `/book/:id`). The current `App.tsx` has temporary placeholder pages you can swap out.
+- **Routes:** AppShell is a layout route, so every page goes in as a child of it in `App.tsx` (`/`, `/shelf`, `/book/:id`). Only the shelf route still uses a temporary placeholder page, which you can swap out. The router's `basename` comes from Vite's `base` (`/` in development, `/project2/` in the build).
+
+- **On mobile, the nav is a fixed tab bar at the bottom.** The shell already leaves room for it, so you don't need to. On desktop the nav sits in the header.
-- **On mobile, the nav is a fixed tab bar at the bottom.** The shell already leaves room for it, so you don't need to.
-
## Search
- The search field is in the header on every page. On submit it navigates to `/?q=`. An empty search goes to plain `/`.
- Browse reads `q` from the URL with `useSearchParams`. There's no prop and no context. **The URL is the shared state.**
-- A new search drops the other params (genres, years, sort), so it starts fresh.
+- A new search from the header drops the other params (genres, years, sort), so it starts fresh.
-- **Browse** is responsible for restarting the paging when `q` changes.
+- **Browse** restarts the paging when `q` changes. This works because the Apollo cache key for the book list includes the search arguments but not `after` (see "Data fetching").
- `SearchField` resets its text to match `q` when it changes (Back button, clearing), so the field always matches the URL.
-- **Param names must match exactly.** We have `genre` in one place and `genres` in another, so let's pick one.
+- **Param names must match exactly.** The API argument is `genres`, but the URL param name is **not decided yet** (`genre` vs `genres`). Suggested: `genres`. Decide before building the filter panel.
- When your filter panel updates the URL, keep `q` and the other params. Setting params naively replaces them all.
-
+## Data fetching
+**New in v3.** Documented here because pages and components depend on it.
+
+- **Apollo Client** (`apollo/client.ts`). The GraphQL address is `VITE_GRAPHQL_URL` if set. Otherwise it is `http://localhost:3001/graphql` in development and `/project2/graphql` in the production build (Apache forwards that path to the backend on the VM).
+- **Queries** live in `apollo/queries.ts` (`BOOKS_QUERY`, `BOOK_QUERY`). Both currently request all `Book` fields. Narrowing list queries to only the fields the cards need is planned.
+- **List cache rule** for `books`: results are cached per search/filter/sort combination (`keyArgs`: `search`, `genres`, `yearFrom`, `yearTo`, `sort`) but not per page. A request without `after` replaces the list. A request with `after` (Load more) appends the new items to the old ones.
+- **Load more:** `fetchMore` with the cursor from `nextCursor`. The button is hidden when `nextCursor` is null.
+- **Shelf data** (ribbon state, rating, notes) is still temporary local state from `data/mockBooks.ts`. It is replaced when the anonymous user ID header is added to the client. Search for `TODO` and `mockBooks` to find the places.
+
+## Page feedback states
+Pages show loading, error and empty situations with three shared components. Props below are the ones in use.
+
+| Component | Props in use | Used for |
+|---|---|---|
+| `LoadingState` | `label`, `announceOnly` | Announces loading to screen readers. `announceOnly` hides the visible text. |
+| `ErrorState` | `message`, `onRetry`, `headingLevel` | A failed request, with a retry button. |
+| `EmptyState` | `title`, `message`, `action`, `headingLevel` | No search results, or a book that was not found. |
+
+Rules:
+- **Errors are shown inline** where the content would have been. No toasts.
+- **Order of checks on a page:** loading, then error, then "not found". A failed or still-loading request has no data, and that is not the same as "not found".
+- **"Book not found" is not an error.** The `book` query returns `null`, and the detail page shows an `EmptyState` with a link back to browse.
+- **One `` per page:** when a state replaces the whole page, pass `headingLevel={1}`.
+- **Load more:** its loading state is on the button itself and the books already shown stay visible. If it fails, an `ErrorState` with retry replaces the button.
+- **List loading** shows `BookCardSkeleton` placeholders in the grid.
-## Icons
-Every SVG now lives in `src/icons/` as a small component (``, optional `size` prop). Please add new icons there instead of inline in components. They use `currentColor`, so you color them with CSS.
+## Icons
+Every SVG lives in `src/icons/` as a small component (``, optional `size` prop). Please add new icons there instead of inline in components. They use `currentColor`, so you color them with CSS.
+Icons are pure SVG and have been written with the help of Claude (AI).
## Changelog
- v1 (29.09.2026): created by Ilse
-- v2 (02.10.2026): Added information about AppShell, Search and Icons. Updated folder structure to reflect relevant changes.
\ No newline at end of file
+- v2 (02.10.2026): Added information about AppShell, Search and Icons. Updated folder structure to reflect relevant changes.
+- v3 (16.10.2026): Delivery 2 update. Folder structure matches the code (with Planned items marked). Added mobile-first note, Data fetching, and Page feedback states. Updated the AppShell routes note and the status of the param-name question. This document update was made with the help of Claude (AI) (code analyse, compare to docs) to ensure that the document reflects actual changes to the app, and the current state.