From e470c5b9fe6a804c39586ea7100e778167635b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilse=20=C3=85sbakk=20Resell?= Date: Thu, 8 Oct 2026 08:58:56 +0200 Subject: [PATCH 01/18] feat: add Bookish favicon + icon file cleanup Add bookishFavicon.svg in public folder. Change favicon path in index.html. Clean up unnecessary comments and formatting in a couple icon files. No change to funcitonality. --- client/index.html | 2 +- client/public/bookishFavicon.svg | 6 ++++++ client/src/icons/BrowseIcon.tsx | 1 - client/src/icons/SparkleIcon.tsx | 2 -- client/src/icons/StarIcon.tsx | 1 + 5 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 client/public/bookishFavicon.svg diff --git a/client/index.html b/client/index.html index f3dcf16..0738587 100644 --- a/client/index.html +++ b/client/index.html @@ -2,7 +2,7 @@ - + t14-project-2 diff --git a/client/public/bookishFavicon.svg b/client/public/bookishFavicon.svg new file mode 100644 index 0000000..8c84107 --- /dev/null +++ b/client/public/bookishFavicon.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/client/src/icons/BrowseIcon.tsx b/client/src/icons/BrowseIcon.tsx index 8d6c212..5cb1a70 100644 --- a/client/src/icons/BrowseIcon.tsx +++ b/client/src/icons/BrowseIcon.tsx @@ -1,4 +1,3 @@ -/* ---------- Props ---------- */ type Props = { size?: number }; /* ---------- BrowseIcon: open book (nav tab for Browse) ---------- */ diff --git a/client/src/icons/SparkleIcon.tsx b/client/src/icons/SparkleIcon.tsx index 8476bb6..990fa32 100644 --- a/client/src/icons/SparkleIcon.tsx +++ b/client/src/icons/SparkleIcon.tsx @@ -1,8 +1,6 @@ -/* ---------- SparkleIcon.tsx ---------- */ type Props = { size?: number; className?: string }; /* ---------- SparkleIcon: four-pointed sparkle, color follows the parent's text color ---------- */ -// size is optional: leave it out when CSS (className) sets the width/height export function SparkleIcon({ size, className }: Props) { return ( Date: Thu, 8 Oct 2026 09:50:06 +0200 Subject: [PATCH 02/18] fix: changed Url to URL in variable names for consistency files - BookCover.tsx - BookCard.tsx This should not affect any other current files, but clears up possible confusion and prevents case errors. --- client/src/components/BookCard/BookCard.tsx | 2 +- client/src/components/BookCover/BookCover.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/src/components/BookCard/BookCard.tsx b/client/src/components/BookCard/BookCard.tsx index decd191..e8c2125 100644 --- a/client/src/components/BookCard/BookCard.tsx +++ b/client/src/components/BookCard/BookCard.tsx @@ -37,7 +37,7 @@ export function BookCard({ id={book.id} title={book.title} author={book.authors[0]} - coverUrl={book.coverURL} + coverURL={book.coverURL} /> {showShelf && - {showShelf && {/* Ribbon: sibling of the link, NOT inside it */} - + ); @@ -73,26 +73,3 @@ function StarRow({ value }: { value: number }) { ); } - -/* ---------- FavoriteRibbon: bookmark toggle ---------- */ -function FavoriteRibbon({ - title, - isSaved, - onToggle, -}: { - title: string; - isSaved: boolean; - onToggle: () => void; -}) { - return ( - - ); -} diff --git a/client/src/components/ShelfRibbon/ShelfRibbon.module.css b/client/src/components/ShelfRibbon/ShelfRibbon.module.css new file mode 100644 index 0000000..2672abd --- /dev/null +++ b/client/src/components/ShelfRibbon/ShelfRibbon.module.css @@ -0,0 +1,38 @@ +/* ===== Ribbon button: tap area and reset ===== */ +/* --ribbon-height lets a parent make the ribbon bigger (default is the card size) */ +.ribbon { + width: 44px; /* 44px-wide tap area; the drawn ribbon is narrower */ + height: calc(var(--ribbon-height, 40px) + 8px); + display: flex; + justify-content: center; + align-items: flex-start; + padding: 0; + border: 0; + background: none; + cursor: pointer; +} + +/* The drawn ribbon keeps the icon's 24:40 shape, whatever the height */ +.ribbon svg { + width: calc(var(--ribbon-height, 40px) * 0.6); + height: var(--ribbon-height, 40px); +} + +.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) ===== */ +.ribbon[aria-pressed='true'] path { + fill: var(--color-ribbon); + stroke: rgb(255 255 255 / 0.8); + stroke-width: 1; +} \ No newline at end of file diff --git a/client/src/components/ShelfRibbon/ShelfRibbon.tsx b/client/src/components/ShelfRibbon/ShelfRibbon.tsx new file mode 100644 index 0000000..731b4be --- /dev/null +++ b/client/src/components/ShelfRibbon/ShelfRibbon.tsx @@ -0,0 +1,24 @@ +import { RibbonIcon } from '../../icons/RibbonIcon'; +import styles from './ShelfRibbon.module.css'; + +type ShelfRibbonProps = { + title: string; + isSaved: boolean; + onToggle: () => void; + className?: string; // lets the parent position it (card corner, cover corner) +}; + +// ===== ShelfRibbon: bookmark toggle for My shelf ===== +export function ShelfRibbon({ title, isSaved, onToggle, className }: ShelfRibbonProps) { + return ( + + ); +} \ No newline at end of file From 0da3d4590a7a7d1920516d05b45717627ea90bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilse=20=C3=85sbakk=20Resell?= Date: Thu, 8 Oct 2026 12:47:38 +0200 Subject: [PATCH 08/18] fix: swap out css color tag that was used incorrectly `--color-chip-bg` changed to more correct `color-header-tint` in SearchField.module.css Allows recoloring of actual color chip bg without inadvertently changing other elements. --- client/src/components/SearchField/SearchField.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/SearchField/SearchField.module.css b/client/src/components/SearchField/SearchField.module.css index b7d1e2d..27283d7 100644 --- a/client/src/components/SearchField/SearchField.module.css +++ b/client/src/components/SearchField/SearchField.module.css @@ -111,7 +111,7 @@ @media (hover: hover) { /* Form hover: the border brightens a touch */ .form:hover { - border-color: color-mix(in srgb, var(--color-chip-bg) 80%, transparent); + border-color: color-mix(in srgb, var(--color-header-tint) 80%, transparent); } /* Button hover: fills gold, icon turns plum */ From 26d1c7a3651d21e99965416e22847a88bf03b06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilse=20=C3=85sbakk=20Resell?= Date: Thu, 8 Oct 2026 12:48:22 +0200 Subject: [PATCH 09/18] feat: add hover color behavior to star rating --- .../StarRatingInput.module.css | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/client/src/components/StarRatingInput/StarRatingInput.module.css b/client/src/components/StarRatingInput/StarRatingInput.module.css index 203716e..ff7fc38 100644 --- a/client/src/components/StarRatingInput/StarRatingInput.module.css +++ b/client/src/components/StarRatingInput/StarRatingInput.module.css @@ -17,6 +17,22 @@ justify-content: center; } -/* Star colors (same values as in BookCard.module.css) */ +/* Star colors */ .starOn { fill: var(--color-star); } -.starOff { fill: var(--color-star-off); } \ No newline at end of file +.starOff { fill: var(--color-star-off); } + +/* ===== Hover preview ===== */ +/* Only on devices that can really hover, so a tap on a phone doesn't leave the preview stuck */ +@media (hover: hover) { + /* Hovering the row: every star turns the preview color... */ + .group:hover .starOn, + .group:hover .starOff { + fill: var(--color-star); + } + + /* ...except the stars after the hovered one, which go back to empty */ + .starButton:hover ~ .starButton .starOn, + .starButton:hover ~ .starButton .starOff { + fill: var(--color-star-off); + } +} \ No newline at end of file From a0c4d813a7b16d1df15cd93b180a3ddb5cf08c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilse=20=C3=85sbakk=20Resell?= Date: Thu, 8 Oct 2026 19:35:35 +0200 Subject: [PATCH 10/18] fix: replace hardcoded values in ribbon with var() from tokens.css For consistency and central management --- client/src/components/ShelfRibbon/ShelfRibbon.module.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/components/ShelfRibbon/ShelfRibbon.module.css b/client/src/components/ShelfRibbon/ShelfRibbon.module.css index 2672abd..a76f1b8 100644 --- a/client/src/components/ShelfRibbon/ShelfRibbon.module.css +++ b/client/src/components/ShelfRibbon/ShelfRibbon.module.css @@ -1,8 +1,8 @@ /* ===== Ribbon button: tap area and reset ===== */ /* --ribbon-height lets a parent make the ribbon bigger (default is the card size) */ .ribbon { - width: 44px; /* 44px-wide tap area; the drawn ribbon is narrower */ - height: calc(var(--ribbon-height, 40px) + 8px); + width: var(--tap-target); + height: calc(var(--ribbon-height, 40px) + var(--space-2)); display: flex; justify-content: center; align-items: flex-start; @@ -25,7 +25,7 @@ /* ===== Not saved: pale outline ribbon ===== */ .ribbon path { - fill: rgb(255 253 248 / 0.9); + fill: color-mix(in srgb, var(--color-card) 90%, transparent); stroke: var(--color-ribbon); stroke-width: 1.8; } @@ -33,6 +33,6 @@ /* ===== Saved: solid ribbon (styled from aria-pressed) ===== */ .ribbon[aria-pressed='true'] path { fill: var(--color-ribbon); - stroke: rgb(255 255 255 / 0.8); + stroke: color-mix(in srgb, var(--color-on-accent) 80%, transparent); stroke-width: 1; } \ No newline at end of file From e387ef2856c52f9eaaae04b90ca43c18a93cb7af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilse=20=C3=85sbakk=20Resell?= Date: Thu, 8 Oct 2026 19:38:21 +0200 Subject: [PATCH 11/18] feat: Add new variant to Button component - Button.tsx - Button.module.css Add a text-only button for various uses across the app (such as "remove rating" on detail page). Default values for button are the same as before, it should not impact any other current files. At the same time clean up hardcoded .css values, replace with var() from tokens.css --- .../src/components/Button/Button.module.css | 21 ++++++++++++------- client/src/components/Button/Button.tsx | 8 ++++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/client/src/components/Button/Button.module.css b/client/src/components/Button/Button.module.css index 0fad132..4296a29 100644 --- a/client/src/components/Button/Button.module.css +++ b/client/src/components/Button/Button.module.css @@ -1,6 +1,6 @@ .button { - height: 44px; - border-radius: 12px; + height: var(--tap-target); + border-radius: var(--radius-md); border: 1.5px solid transparent; font-size: 15px; font-weight: 700; @@ -27,12 +27,17 @@ padding: 0 40px; } -.button:disabled { - opacity: 0.5; - cursor: not-allowed; +/* Underlined text, no box. Placed after the sizes so its padding wins */ +.text { + padding: 0 var(--space-2); + background: none; + font-size: var(--text-sm); + font-weight: 600; + color: var(--color-text-soft); + text-decoration: underline; } -.button:focus-visible { - outline: 2px solid var(--color-focus); - outline-offset: 3px; +.button:disabled { + opacity: 0.5; + cursor: not-allowed; } \ No newline at end of file diff --git a/client/src/components/Button/Button.tsx b/client/src/components/Button/Button.tsx index 711a58f..c8a634f 100644 --- a/client/src/components/Button/Button.tsx +++ b/client/src/components/Button/Button.tsx @@ -2,11 +2,12 @@ import styles from './Button.module.css'; type ButtonProps = { children: React.ReactNode; - variant?: "primary" | "secondary"; + variant?: "primary" | "secondary" | "text"; size?: "narrow" | "wide"; type?: "button" | "submit"; onClick?: () => void; disabled?: boolean; + className?: string; }; export function Button({ @@ -16,15 +17,16 @@ export function Button({ type = "button", onClick, disabled = false, + className, }: ButtonProps) { return ( ); -} +} \ No newline at end of file From fb5874b2729b8e17a84d703bb9f92c49c18dbaf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilse=20=C3=85sbakk=20Resell?= Date: Thu, 8 Oct 2026 19:39:30 +0200 Subject: [PATCH 12/18] feat: new component "Eyebrow" (sparkle icon + text above headers) What a weird name for this thing... - Eyebrow.tsx - Eyebrow.module.css --- .../src/components/Eyebrow/Eyebrow.module.css | 15 +++++++++++++++ client/src/components/Eyebrow/Eyebrow.tsx | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 client/src/components/Eyebrow/Eyebrow.module.css create mode 100644 client/src/components/Eyebrow/Eyebrow.tsx diff --git a/client/src/components/Eyebrow/Eyebrow.module.css b/client/src/components/Eyebrow/Eyebrow.module.css new file mode 100644 index 0000000..26f74db --- /dev/null +++ b/client/src/components/Eyebrow/Eyebrow.module.css @@ -0,0 +1,15 @@ +.eyebrow { + display: flex; + align-items: center; + gap: var(--space-2); + font-size: var(--text-sm); + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--color-accent); +} + +.sparkle { + width: 0.875rem; + height: 0.875rem; +} \ No newline at end of file diff --git a/client/src/components/Eyebrow/Eyebrow.tsx b/client/src/components/Eyebrow/Eyebrow.tsx new file mode 100644 index 0000000..2efa04b --- /dev/null +++ b/client/src/components/Eyebrow/Eyebrow.tsx @@ -0,0 +1,17 @@ +// Weird naming convention, but this is the small piece of text sitting above the header, with a sparkle icon +import { SparkleIcon } from '../../icons/SparkleIcon'; +import styles from './Eyebrow.module.css'; + +type EyebrowProps = { + children: React.ReactNode; +}; + +// ===== Eyebrow: small uppercase label with a sparkle, above a heading ===== +export function Eyebrow({ children }: EyebrowProps) { + return ( +

+ + {children} +

+ ); +} \ No newline at end of file From 9eaf2822dbe733d879a8793da9910ea54e60abbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilse=20=C3=85sbakk=20Resell?= Date: Thu, 8 Oct 2026 19:52:23 +0200 Subject: [PATCH 13/18] feat: new component ConfirmDialog popup for destructive actions For use in book detail page. Why and when it triggers: Removing book from your shelf also deletes your rating and notes Deleting notes cannot be undone The popup is handled by the app itself, overwriting some of the automatic behavior in the browser. --- .../ConfirmDialog/ConfirmDialog.module.css | 30 ++++++++++ .../ConfirmDialog/ConfirmDialog.tsx | 59 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 client/src/components/ConfirmDialog/ConfirmDialog.module.css create mode 100644 client/src/components/ConfirmDialog/ConfirmDialog.tsx diff --git a/client/src/components/ConfirmDialog/ConfirmDialog.module.css b/client/src/components/ConfirmDialog/ConfirmDialog.module.css new file mode 100644 index 0000000..e6f3f38 --- /dev/null +++ b/client/src/components/ConfirmDialog/ConfirmDialog.module.css @@ -0,0 +1,30 @@ +.dialog { + margin: auto; + width: min(26rem, calc(100vw - 2rem)); + padding: var(--space-6); + background: var(--color-card); + color: var(--color-text); + border: 1px solid var(--color-line); + border-radius: var(--radius-md); +} + +.dialog::backdrop { + background: rgb(0 0 0 / 45%); +} + +.title { + margin-block-end: var(--space-3); + font-size: var(--text-lg); +} + +.message { + margin-block-end: var(--space-6); + line-height: var(--leading); + color: var(--color-text-soft); +} + +.actions { + display: flex; + align-items: center; + gap: var(--space-2); +} \ No newline at end of file diff --git a/client/src/components/ConfirmDialog/ConfirmDialog.tsx b/client/src/components/ConfirmDialog/ConfirmDialog.tsx new file mode 100644 index 0000000..4b4388b --- /dev/null +++ b/client/src/components/ConfirmDialog/ConfirmDialog.tsx @@ -0,0 +1,59 @@ +import { useEffect, useId, useRef } from 'react'; +import { Button } from '../Button/Button'; +import styles from './ConfirmDialog.module.css'; + +type ConfirmDialogProps = { + open: boolean; + title: string; + message: string; + confirmLabel: string; + onConfirm: () => void; + onCancel: () => void; +}; + +// ===== ConfirmDialog: "are you sure?" popup for destructive actions ===== +export function ConfirmDialog({ + open, + title, + message, + confirmLabel, + onConfirm, + onCancel, +}: ConfirmDialogProps) { + const dialogRef = useRef(null); + const titleId = useId(); + + // The native dialog has its own open/closed state, so sync it with the `open` prop + useEffect(() => { + const dialog = dialogRef.current; + if (!dialog) return; + if (open && !dialog.open) dialog.showModal(); + if (!open && dialog.open) dialog.close(); + }, [open]); + + return ( + { + // Esc: let our own state do the closing, not the browser + event.preventDefault(); + onCancel(); + }} + > +

+ {title} +

+

{message}

+
+ + +
+
+ ); +} \ No newline at end of file From 16e2607bc70de4a2aba13da1e361a28f6b3e9ad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilse=20=C3=85sbakk=20Resell?= Date: Thu, 8 Oct 2026 19:52:59 +0200 Subject: [PATCH 14/18] feat: new component user notes panel Includes: - Current rating, interactive, can be changed by just clicking the stars. - Button to remove rating - Note status / existing note - Text box to write note when creating new/editing - Buttons to create note, save or discard drafted note, delete existing note. Note: You cannot save a note with zero text. If you want to remove a note, you need to cancel any current edit/draft, and instead click "delete note" --- .../MyNotesPanel/MyNotesPanel.module.css | 97 ++++++++++++ .../components/MyNotesPanel/MyNotesPanel.tsx | 143 ++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 client/src/components/MyNotesPanel/MyNotesPanel.module.css create mode 100644 client/src/components/MyNotesPanel/MyNotesPanel.tsx diff --git a/client/src/components/MyNotesPanel/MyNotesPanel.module.css b/client/src/components/MyNotesPanel/MyNotesPanel.module.css new file mode 100644 index 0000000..53389d4 --- /dev/null +++ b/client/src/components/MyNotesPanel/MyNotesPanel.module.css @@ -0,0 +1,97 @@ +/* ===== Panel card ===== */ +.panel { + display: flex; + flex-direction: column; + gap: var(--space-4); + padding: var(--space-6); + background: var(--color-card); + border: 1px solid var(--color-line); + border-radius: var(--radius-md); +} + +.heading { + font-size: var(--text-lg); +} + +/* ===== Rating ===== */ +.labelRow { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: var(--space-3); + margin-block-end: 2px; +} + +.label { + font-size: var(--text-sm); + font-weight: 700; + color: var(--color-text-soft); +} + +/* The text Button is 44px tall; negative margins cancel that, so the row keeps its height */ +.clearRating { + margin-block: calc(-1 * var(--space-3)); + margin-inline-end: calc(-1 * var(--space-2)); +} + +/* ===== Note: saved state ===== */ +.noteBlock { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.note { + font-size: var(--text-md); + line-height: var(--leading); + white-space: pre-wrap; /* keeps the user's own line breaks */ + overflow-wrap: anywhere; /* a very long word can't push the card wider */ +} + +.updated { + font-size: var(--text-sm); + color: var(--color-text-soft); +} + +/* ===== Note: empty state ===== */ +.empty { + line-height: var(--leading); + color: var(--color-text-soft); +} + +/* ===== Note: editing state ===== */ +.form { + display: flex; + flex-direction: column; + gap: var(--space-4); +} + +.field { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.textarea { + padding: var(--space-3); + background: var(--color-card); + border: 1px solid var(--color-input-border); + border-radius: var(--radius-md); + font-size: var(--text-md); + line-height: var(--leading); + color: var(--color-text); + resize: none; +} + +.counter { + align-self: flex-end; + font-size: var(--text-sm); + color: var(--color-text-soft); +} + +/* ===== Button row under the note ===== */ +.actions { + display: flex; + align-items: center; + gap: var(--space-2); +} \ No newline at end of file diff --git a/client/src/components/MyNotesPanel/MyNotesPanel.tsx b/client/src/components/MyNotesPanel/MyNotesPanel.tsx new file mode 100644 index 0000000..6acc14b --- /dev/null +++ b/client/src/components/MyNotesPanel/MyNotesPanel.tsx @@ -0,0 +1,143 @@ +import { useId, useState } from 'react'; +import { Button } from '../Button/Button'; +import { StarRatingInput } from '../StarRatingInput/StarRatingInput'; +import styles from './MyNotesPanel.module.css'; + +const MAX_NOTE_LENGTH = 1000; + +type MyNotesPanelProps = { + rating?: number; + note?: string; // undefined or "" = no note + updatedAt?: string; // ISO date of the last note save + onRatingChange: (rating: number) => void; + onClearRating: () => void; + onSaveNote: (note: string) => void; + onDeleteNote: () => void; +}; + +// "2026-10-03T12:00:00.000Z" -> "3 Oct 2026" +function formatDate(iso: string) { + return new Date(iso).toLocaleDateString('en-GB', { + day: 'numeric', + month: 'short', + year: 'numeric', + }); +} + +// ===== MyNotesPanel: the user's own rating and note for one book ===== +export function MyNotesPanel({ + rating, + note, + updatedAt, + onRatingChange, + onClearRating, + onSaveNote, + onDeleteNote, +}: MyNotesPanelProps) { + const [isEditing, setIsEditing] = useState(false); + const [draft, setDraft] = useState(''); // text in the textarea; only saved on "Save note" + const noteFieldId = useId(); + + // Which of the three note states to show + const mode = isEditing ? 'editing' : note ? 'saved' : 'empty'; + + // ===== Handlers ===== + const startEditing = () => { + setDraft(note ?? ''); + setIsEditing(true); + }; + + const cancelEditing = () => { + setIsEditing(false); // the draft is simply thrown away + }; + + const saveDraft = () => { + onSaveNote(draft.trim()); + setIsEditing(false); + }; + + return ( +
+

+ Your rating and notes +

+ + {/* ===== Rating ===== */} +
+
+

Your rating

+ {rating !== undefined && ( + + )} +
+ +
+ + {/* ===== Note: empty state ===== */} + {mode === 'empty' && ( + <> +

No notes yet. Rating this book puts it on your shelf.

+
+ +
+ + )} + + {/* ===== Note: saved state ===== */} + {mode === 'saved' && ( + <> +
+

Your note

+

{note}

+ {updatedAt &&

Updated {formatDate(updatedAt)}

} +
+
+ + +
+ + )} + + {/* ===== Note: editing state ===== */} + {mode === 'editing' && ( +
{ + event.preventDefault(); // stop the browser's own submit (page reload) + saveDraft(); + }} + > +
+ +