Make book/topic a required select instead of datalist input

Datalist suggestions are unreliably supported on mobile/PWA browsers,
so existing books/topics silently failed to show up when starting the
app on a new device. Replaced with a native <select> (plus an "add
new" option) and made the field mandatory end-to-end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3DGg1pvmvf81zXs4pk1J7
This commit is contained in:
Claudio Schaad 2026-08-27 17:43:11 +02:00
parent 73ed2e2cf1
commit 47d9113f3c
4 changed files with 56 additions and 17 deletions

View file

@ -4,11 +4,15 @@ import { fetchBooks } from "../api/books";
import { saveNote } from "../api/notes";
import { logout } from "../api/auth";
const NEW_BOOK_OPTION = "__new__";
export function NoteFormPage({ onLoggedOut }: { onLoggedOut: () => void }) {
const queryClient = useQueryClient();
const booksQuery = useQuery({ queryKey: ["books"], queryFn: fetchBooks });
const [book, setBook] = useState("");
const [isAddingNewBook, setIsAddingNewBook] = useState(false);
const [newBookTitle, setNewBookTitle] = useState("");
const [page, setPage] = useState("");
const [text, setText] = useState("");
@ -26,14 +30,25 @@ export function NoteFormPage({ onLoggedOut }: { onLoggedOut: () => void }) {
onSuccess: onLoggedOut
});
function handleBookSelect(value: string) {
if (value === NEW_BOOK_OPTION) {
setIsAddingNewBook(true);
setBook("");
} else {
setIsAddingNewBook(false);
setNewBookTitle("");
setBook(value);
}
}
function handleSubmit(e: FormEvent) {
e.preventDefault();
const trimmedBook = book.trim();
const trimmedBook = (isAddingNewBook ? newBookTitle : book).trim();
const trimmedText = text.trim();
if (!trimmedText) return;
if (!trimmedBook || !trimmedText) return;
saveNoteMutation.mutate({
book: trimmedBook || undefined,
book: trimmedBook,
page: page ? Number(page) : undefined,
text: trimmedText
});
@ -61,16 +76,35 @@ export function NoteFormPage({ onLoggedOut }: { onLoggedOut: () => void }) {
</label>
<label>
Book / topic (optional)
Book / topic
<select
value={isAddingNewBook ? NEW_BOOK_OPTION : book}
onChange={(e) => handleBookSelect(e.target.value)}
disabled={booksQuery.isLoading}
required
>
<option value="" disabled>
{booksQuery.isLoading ? "Loading…" : "Select a book / topic"}
</option>
{booksQuery.data?.map((b) => (
<option key={b.title} value={b.title}>
{b.title}
</option>
))}
<option value={NEW_BOOK_OPTION}>+ Add new book / topic…</option>
</select>
{isAddingNewBook && (
<input
list="book-options"
value={book}
onChange={(e) => setBook(e.target.value)}
placeholder="Select or type a new one"
value={newBookTitle}
onChange={(e) => setNewBookTitle(e.target.value)}
placeholder="New book / topic name"
autoFocus
required
/>
<datalist id="book-options">
{booksQuery.data?.map((b) => <option key={b.title} value={b.title} />)}
</datalist>
)}
{booksQuery.isError && (
<span className="error">Couldn't load existing books/topics — you can still add a new one.</span>
)}
</label>
<label>

View file

@ -71,7 +71,8 @@ label {
}
input,
textarea {
textarea,
select {
font: inherit;
color: var(--text);
background: transparent;
@ -80,6 +81,10 @@ textarea {
padding: 0.55rem 0.65rem;
}
select {
color-scheme: light dark;
}
textarea {
resize: vertical;
}

View file

@ -1,6 +1,6 @@
import { Router } from "express";
import { noteInputSchema } from "@notes/shared";
import { getOrCreateBookPage, getParentPage } from "../nextcloud/bookCache.js";
import { getOrCreateBookPage } from "../nextcloud/bookCache.js";
import { appendEntry } from "../nextcloud/webdavClient.js";
import { formatEntry } from "../nextcloud/noteFormatter.js";
@ -16,7 +16,7 @@ notesRoutes.post("/", async (req, res) => {
const { book, page, text } = parsed.data;
try {
const targetPage = book ? await getOrCreateBookPage(book) : await getParentPage();
const targetPage = await getOrCreateBookPage(book);
const entry = formatEntry(text, page);
await appendEntry(targetPage, entry);
res.status(201).json({ ok: true });

View file

@ -7,7 +7,7 @@ export const loginInputSchema = z.object({
export type LoginInput = z.infer<typeof loginInputSchema>;
export const noteInputSchema = z.object({
book: z.string().trim().min(1).max(200).optional(),
book: 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)
});