From be421d83cb2bb6c887601c4869e35e8779aff891 Mon Sep 17 00:00:00 2001 From: Claudio Schaad Date: Thu, 27 Aug 2026 18:55:51 +0200 Subject: [PATCH] Rename Book to Topic throughout the app The book/topic field was really always a general-purpose tag, and now that notes can come from more than just books (camera-scanned pages, free text), "book" was misleading in the UI as well as the code. Renames files, functions, variables, the API route (/api/books -> /api/topics), the note payload field, and the cache TTL env var (BOOK_CACHE_TTL_MS -> TOPIC_CACHE_TTL_MS) for consistency. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01C3DGg1pvmvf81zXs4pk1J7 --- .env.example | 2 +- README.md | 2 +- client/src/api/books.ts | 6 -- client/src/api/topics.ts | 6 ++ client/src/pages/NoteFormPage.tsx | 64 +++++++++---------- server/src/config.ts | 2 +- server/src/index.ts | 4 +- .../nextcloud/{bookCache.ts => topicCache.ts} | 22 +++---- server/src/routes/books.ts | 14 ---- server/src/routes/notes.ts | 6 +- server/src/routes/topics.ts | 14 ++++ shared/src/schemas.ts | 6 +- 12 files changed, 74 insertions(+), 74 deletions(-) delete mode 100644 client/src/api/books.ts create mode 100644 client/src/api/topics.ts rename server/src/nextcloud/{bookCache.ts => topicCache.ts} (80%) delete mode 100644 server/src/routes/books.ts create mode 100644 server/src/routes/topics.ts diff --git a/.env.example b/.env.example index 377e8b7..160612d 100644 --- a/.env.example +++ b/.env.example @@ -17,4 +17,4 @@ PARENT_PAGE_TITLE=Reading Notes # Optional PORT=3000 HOST_PORT=3000 -BOOK_CACHE_TTL_MS=60000 +TOPIC_CACHE_TTL_MS=60000 diff --git a/README.md b/README.md index 438fa8a..907bb0e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Notes -A small PWA for jotting down short notes while reading — e.g. an interesting sentence from a book. Each note is tagged with a book/topic (existing or new) and can optionally include a page number. Notes are saved as markdown directly into pages of a Nextcloud **Collective**, one page per book/topic, nested under a parent page (default: "Reading Notes"). A note's text can be typed by hand or filled in by scanning a photo of the page (on-device OCR via `tesseract.js` — no image or text ever leaves the device for this step). +A small PWA for jotting down short notes while reading — e.g. an interesting sentence from a book. Each note is tagged with a topic (existing or new) and can optionally include a page number. Notes are saved as markdown directly into pages of a Nextcloud **Collective**, one page per topic, nested under a parent page (default: "Reading Notes"). A note's text can be typed by hand or filled in by scanning a photo of the page (on-device OCR via `tesseract.js` — no image or text ever leaves the device for this step). ## Stack diff --git a/client/src/api/books.ts b/client/src/api/books.ts deleted file mode 100644 index f657a1d..0000000 --- a/client/src/api/books.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Book } from "@notes/shared"; -import { apiFetch } from "./client"; - -export function fetchBooks() { - return apiFetch("/api/books"); -} diff --git a/client/src/api/topics.ts b/client/src/api/topics.ts new file mode 100644 index 0000000..ac78365 --- /dev/null +++ b/client/src/api/topics.ts @@ -0,0 +1,6 @@ +import type { Topic } from "@notes/shared"; +import { apiFetch } from "./client"; + +export function fetchTopics() { + return apiFetch("/api/topics"); +} diff --git a/client/src/pages/NoteFormPage.tsx b/client/src/pages/NoteFormPage.tsx index dba4715..d514285 100644 --- a/client/src/pages/NoteFormPage.tsx +++ b/client/src/pages/NoteFormPage.tsx @@ -1,20 +1,20 @@ import { useRef, useState, type ChangeEvent, type FormEvent } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { fetchBooks } from "../api/books"; +import { fetchTopics } from "../api/topics"; import { saveNote } from "../api/notes"; import { logout } from "../api/auth"; import { recognizeText } from "../lib/ocr"; import { ImageCropper } from "../components/ImageCropper"; -const NEW_BOOK_OPTION = "__new__"; +const NEW_TOPIC_OPTION = "__new__"; export function NoteFormPage({ onLoggedOut }: { onLoggedOut: () => void }) { const queryClient = useQueryClient(); - const booksQuery = useQuery({ queryKey: ["books"], queryFn: fetchBooks }); + const topicsQuery = useQuery({ queryKey: ["topics"], queryFn: fetchTopics }); - const [book, setBook] = useState(""); - const [isAddingNewBook, setIsAddingNewBook] = useState(false); - const [newBookTitle, setNewBookTitle] = useState(""); + const [topic, setTopic] = useState(""); + const [isAddingNewTopic, setIsAddingNewTopic] = useState(false); + const [newTopicTitle, setNewTopicTitle] = useState(""); const [page, setPage] = useState(""); const [text, setText] = useState(""); const [isScanning, setIsScanning] = useState(false); @@ -27,7 +27,7 @@ export function NoteFormPage({ onLoggedOut }: { onLoggedOut: () => void }) { onSuccess: () => { setText(""); setPage(""); - queryClient.invalidateQueries({ queryKey: ["books"] }); + queryClient.invalidateQueries({ queryKey: ["topics"] }); } }); @@ -36,14 +36,14 @@ export function NoteFormPage({ onLoggedOut }: { onLoggedOut: () => void }) { onSuccess: onLoggedOut }); - function handleBookSelect(value: string) { - if (value === NEW_BOOK_OPTION) { - setIsAddingNewBook(true); - setBook(""); + function handleTopicSelect(value: string) { + if (value === NEW_TOPIC_OPTION) { + setIsAddingNewTopic(true); + setTopic(""); } else { - setIsAddingNewBook(false); - setNewBookTitle(""); - setBook(value); + setIsAddingNewTopic(false); + setNewTopicTitle(""); + setTopic(value); } } @@ -71,12 +71,12 @@ export function NoteFormPage({ onLoggedOut }: { onLoggedOut: () => void }) { function handleSubmit(e: FormEvent) { e.preventDefault(); - const trimmedBook = (isAddingNewBook ? newBookTitle : book).trim(); + const trimmedTopic = (isAddingNewTopic ? newTopicTitle : topic).trim(); const trimmedText = text.trim(); - if (!trimmedBook || !trimmedText) return; + if (!trimmedTopic || !trimmedText) return; saveNoteMutation.mutate({ - book: trimmedBook, + topic: trimmedTopic, page: page ? Number(page) : undefined, text: trimmedText }); @@ -132,34 +132,34 @@ export function NoteFormPage({ onLoggedOut }: { onLoggedOut: () => void }) { diff --git a/server/src/config.ts b/server/src/config.ts index d8f6b04..585d83b 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -19,5 +19,5 @@ export const config = { collectiveName: required("COLLECTIVE_NAME"), parentPageTitle: process.env.PARENT_PAGE_TITLE ?? "Reading Notes", - bookCacheTtlMs: Number(process.env.BOOK_CACHE_TTL_MS ?? 60_000) + topicCacheTtlMs: Number(process.env.TOPIC_CACHE_TTL_MS ?? 60_000) }; diff --git a/server/src/index.ts b/server/src/index.ts index 746764c..a94d9ab 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -6,7 +6,7 @@ import { config } from "./config.js"; import { sessionMiddleware } from "./auth/session.js"; import { requireAuth } from "./auth/requireAuth.js"; import { authRoutes } from "./auth/authRoutes.js"; -import { booksRoutes } from "./routes/books.js"; +import { topicsRoutes } from "./routes/topics.js"; import { notesRoutes } from "./routes/notes.js"; const app = express(); @@ -15,7 +15,7 @@ app.use(express.json()); app.use(sessionMiddleware); app.use("/api", authRoutes); -app.use("/api/books", requireAuth, booksRoutes); +app.use("/api/topics", requireAuth, topicsRoutes); app.use("/api/notes", requireAuth, notesRoutes); const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/server/src/nextcloud/bookCache.ts b/server/src/nextcloud/topicCache.ts similarity index 80% rename from server/src/nextcloud/bookCache.ts rename to server/src/nextcloud/topicCache.ts index 37976d2..297e3a0 100644 --- a/server/src/nextcloud/bookCache.ts +++ b/server/src/nextcloud/topicCache.ts @@ -10,7 +10,7 @@ import { let collectiveIdPromise: Promise | null = null; let parentPagePromise: Promise | null = null; -let cachedBooks: Map | null = null; +let cachedTopics: Map | null = null; let cachedAt = 0; function getCollectiveId(): Promise { @@ -50,9 +50,9 @@ export function getParentPage(): Promise { return parentPagePromise; } -async function refreshBooks(force = false): Promise> { - if (!force && cachedBooks && Date.now() - cachedAt < config.bookCacheTtlMs) { - return cachedBooks; +async function refreshTopics(force = false): Promise> { + if (!force && cachedTopics && Date.now() - cachedAt < config.topicCacheTtlMs) { + return cachedTopics; } const collectiveId = await getCollectiveId(); @@ -66,24 +66,24 @@ async function refreshBooks(force = false): Promise> { } } - cachedBooks = map; + cachedTopics = map; cachedAt = Date.now(); return map; } -export async function listBooks(): Promise<{ title: string }[]> { - const map = await refreshBooks(); +export async function listTopics(): Promise<{ title: string }[]> { + const map = await refreshTopics(); return [...map.keys()].sort((a, b) => a.localeCompare(b)).map((title) => ({ title })); } -export async function getOrCreateBookPage(title: string): Promise { - let map = await refreshBooks(); +export async function getOrCreateTopicPage(title: string): Promise { + let map = await refreshTopics(); let page = map.get(title); if (page) { return page; } - map = await refreshBooks(true); + map = await refreshTopics(true); page = map.get(title); if (page) { return page; @@ -94,6 +94,6 @@ export async function getOrCreateBookPage(title: string): Promise { const created = await createPage(collectiveId, parent.id, title); map.set(created.title, created); - cachedBooks = map; + cachedTopics = map; return created; } diff --git a/server/src/routes/books.ts b/server/src/routes/books.ts deleted file mode 100644 index 3fb72f2..0000000 --- a/server/src/routes/books.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Router } from "express"; -import { listBooks } from "../nextcloud/bookCache.js"; - -export const booksRoutes = Router(); - -booksRoutes.get("/", async (_req, res) => { - try { - const books = await listBooks(); - res.json(books); - } catch (err) { - console.error("Failed to list books", err); - res.status(502).json({ error: "Failed to load books from Nextcloud" }); - } -}); diff --git a/server/src/routes/notes.ts b/server/src/routes/notes.ts index b72b8f3..a41ce49 100644 --- a/server/src/routes/notes.ts +++ b/server/src/routes/notes.ts @@ -1,6 +1,6 @@ import { Router } from "express"; import { noteInputSchema } from "@notes/shared"; -import { getOrCreateBookPage } from "../nextcloud/bookCache.js"; +import { getOrCreateTopicPage } from "../nextcloud/topicCache.js"; import { appendEntry } from "../nextcloud/webdavClient.js"; import { formatEntry } from "../nextcloud/noteFormatter.js"; @@ -13,10 +13,10 @@ notesRoutes.post("/", async (req, res) => { return; } - const { book, page, text } = parsed.data; + const { topic, page, text } = parsed.data; try { - const targetPage = await getOrCreateBookPage(book); + const targetPage = await getOrCreateTopicPage(topic); const entry = formatEntry(text, page); await appendEntry(targetPage, entry); res.status(201).json({ ok: true }); diff --git a/server/src/routes/topics.ts b/server/src/routes/topics.ts new file mode 100644 index 0000000..2326865 --- /dev/null +++ b/server/src/routes/topics.ts @@ -0,0 +1,14 @@ +import { Router } from "express"; +import { listTopics } from "../nextcloud/topicCache.js"; + +export const topicsRoutes = Router(); + +topicsRoutes.get("/", async (_req, res) => { + try { + const topics = await listTopics(); + res.json(topics); + } catch (err) { + console.error("Failed to list topics", err); + res.status(502).json({ error: "Failed to load topics from Nextcloud" }); + } +}); diff --git a/shared/src/schemas.ts b/shared/src/schemas.ts index 8da594e..2ce154f 100644 --- a/shared/src/schemas.ts +++ b/shared/src/schemas.ts @@ -7,13 +7,13 @@ export const loginInputSchema = z.object({ export type LoginInput = z.infer; export const noteInputSchema = z.object({ - book: z.string().trim().min(1).max(200), + topic: z.string().trim().min(1).max(200), page: z.number().int().positive().max(1_000_000).optional(), text: z.string().trim().min(1).max(5000) }); export type NoteInput = z.infer; -export const bookSchema = z.object({ +export const topicSchema = z.object({ title: z.string() }); -export type Book = z.infer; +export type Topic = z.infer;