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:
parent
73ed2e2cf1
commit
47d9113f3c
4 changed files with 56 additions and 17 deletions
|
|
@ -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)
|
||||
<input
|
||||
list="book-options"
|
||||
value={book}
|
||||
onChange={(e) => setBook(e.target.value)}
|
||||
placeholder="Select or type a new one"
|
||||
/>
|
||||
<datalist id="book-options">
|
||||
{booksQuery.data?.map((b) => <option key={b.title} value={b.title} />)}
|
||||
</datalist>
|
||||
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
|
||||
value={newBookTitle}
|
||||
onChange={(e) => setNewBookTitle(e.target.value)}
|
||||
placeholder="New book / topic name"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
)}
|
||||
{booksQuery.isError && (
|
||||
<span className="error">Couldn't load existing books/topics — you can still add a new one.</span>
|
||||
)}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue