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: 7 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ We deliberately left out reviews from other users. There is no user login and no

*Preview:*

<img src="docs\images\browse-desktop.png" alt="browse-desktop" width="600"/> <img src="docs\images\browse-mobile.png" alt="browse-mobile" width="350"/>
<img src="docs/images/browse-desktop.png" alt="browse-desktop" width="500"/> <img src="docs/images/browse-mobile.png" alt="browse-mobile" width="300"/>

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.

Expand All @@ -45,24 +45,8 @@ Links between the pages work in the mockup. Buttons do not. The ratings, notes a

**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.

**What a book looks like** (our contract between backend and frontend):

```ts
type Book = {
id: string; // Google Books volume id
title: string;
authors: string[];
publishedYear: number | null;
genres: string[]; // from a fixed list we define
description: string | null; // plain text
coverUrl: string | null; // a placeholder cover is shown if missing
pageCount: number | null;
language: string | null;
myEntry: ShelfEntry | null; // this user's rating and note, if any
};

type ShelfEntry = { rating: number | null; note: string | null; updatedAt: string };
```
**What a book looks like**
See `docs/decisions.md`

**Two kinds of stored data:**
1. **Books:** read-only, filled in by a seed script.
Expand Down Expand Up @@ -107,18 +91,19 @@ type ShelfEntry = { rating: number | null; note: string | null; updatedAt: strin
| 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 |

We will reuse existing libraries wherever they make sense and explain the important choices in this README as the project grows.

**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.
**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.

**Sustainability:** results load in small batches, search waits before it sends requests, covers are small and loaded lazily, and we use few fonts and few font weights.
**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.

## 6. 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 ment to help us keep git clean and informational.
- **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.
- Time logs and each member's contribution are delivered separately in the course's submission system, not in this repository.
Expand Down
16 changes: 8 additions & 8 deletions client/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
import prettier from 'eslint-config-prettier'
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
import { defineConfig, globalIgnores } from 'eslint/config';
import prettier from 'eslint-config-prettier';

export default defineConfig([
globalIgnores(['dist']),
Expand All @@ -21,4 +21,4 @@ export default defineConfig([
},
},
prettier,
])
]);
58 changes: 58 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"graphql": "^16.14.2",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-router-dom": "^7.18.4",
"rxjs": "^7.8.2",
"vitest": "^5.0.1"
},
Expand Down
6 changes: 2 additions & 4 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
function App() {
return (
<h1>🥂Project 2🥂</h1>
)
return <h1>🥂Project 2🥂</h1>;
}

export default App
export default App;
132 changes: 132 additions & 0 deletions client/src/components/BookCard/BookCard.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/* ===== List item ===== */
.item {
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 */
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
by a neighbouring card's plank */
.coverWrap {
position: relative;
z-index: 1;
}

/* ===== Shelf plank (only rendered when showShelf is true) ===== */
/* The plank itself is just a 10px spacer. The visible shelf is its ::before,
stretched far to both sides. Every card in a row draws the same shelf at the
same height, so they overlap into one continuous line.
BookGrid's .withShelf clips it at the edge of the grid. */
.plank {
position: relative;
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 */
}

/* ===== Text ===== */
.text {
display: flex;
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 */
line-height: 1.2;
}
.meta {
font-size: var(--text-sm);
color: var(--color-text-soft);
}

/* ===== Clickable card: the "stretched link" trick ===== */
.link {
color: inherit;
text-decoration: none;
}
.link::after {
/* invisible layer over the whole card */
content: '';
position: absolute;
inset: 0;
}
.card:hover .link {
text-decoration: underline;
}

/* Focus ring around the whole card instead of only the title text */
.link:focus-visible {
outline: none;
}
.card:has(.link:focus-visible) {
outline: 2px solid var(--color-focus);
outline-offset: 4px;
border-radius: var(--radius-sm);
}

/* ===== Stars ===== */
.stars {
display: flex;
gap: 2px;
align-items: center;
}
.starOn {
fill: var(--color-star);
}
.starOff {
fill: var(--color-line);
}

/* ===== Ribbon ===== */
.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;
}
Loading