Lesezeichen, Import/Export, Favoriten-Sortierung im Frontend, Favicon-Fix, sortierbare Spalten, Kategorie-Dropdown

This commit is contained in:
2026-07-19 21:56:25 +02:00
parent 31e17ff77b
commit dc91a9aba9
24 changed files with 1655 additions and 180 deletions

View File

@@ -0,0 +1,86 @@
import type { FastifyInstance } from "fastify";
import { BookmarkCreateSchema, BookmarkReorderSchema, BookmarkUpdateSchema } from "@launchpad/shared";
import * as bookmarkRepo from "../db/repositories/bookmarks.js";
import * as categoryRepo from "../db/repositories/categories.js";
import { probeHttp } from "../scanner/http.js";
export async function bookmarkRoutes(app: FastifyInstance): Promise<void> {
app.get("/api/bookmarks", async () => {
return bookmarkRepo.listBookmarks();
});
app.get("/api/bookmarks/:id", async (request, reply) => {
const { id } = request.params as { id: string };
const bookmark = bookmarkRepo.getBookmark(id);
if (!bookmark) {
return reply.code(404).send({ error: "Lesezeichen nicht gefunden" });
}
return bookmark;
});
app.post("/api/bookmarks", async (request, reply) => {
const parsed = BookmarkCreateSchema.safeParse(request.body);
if (!parsed.success) {
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
}
let displayName = parsed.data.displayName;
let favicon: string | null = null;
// Titel/Favicon automatisch ziehen, falls kein Name angegeben wurde oder
// schlicht um ein Favicon zu bekommen (derselbe Mechanismus wie beim
// Netzwerk-Scanner, siehe apps/backend/src/scanner/http.ts).
try {
const probe = await probeHttp(parsed.data.url, 5000);
if (!displayName) {
displayName = probe.title ?? bookmarkRepo.extractHostname(parsed.data.url);
}
favicon = probe.faviconUrl ?? null;
} catch {
if (!displayName) {
displayName = bookmarkRepo.extractHostname(parsed.data.url);
}
}
if (parsed.data.category) {
categoryRepo.ensureCategory(parsed.data.category);
}
const bookmark = bookmarkRepo.createBookmark(parsed.data, { displayName, favicon });
return reply.code(201).send(bookmark);
});
// Muss vor der /:id-Route stehen, damit "reorder" nicht als ID interpretiert wird.
app.patch("/api/bookmarks/reorder", async (request, reply) => {
const parsed = BookmarkReorderSchema.safeParse(request.body);
if (!parsed.success) {
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
}
return bookmarkRepo.reorderBookmarks(parsed.data);
});
app.patch("/api/bookmarks/:id", async (request, reply) => {
const { id } = request.params as { id: string };
const parsed = BookmarkUpdateSchema.safeParse(request.body);
if (!parsed.success) {
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
}
if (parsed.data.category) {
categoryRepo.ensureCategory(parsed.data.category);
}
const bookmark = bookmarkRepo.updateBookmark(id, parsed.data);
if (!bookmark) {
return reply.code(404).send({ error: "Lesezeichen nicht gefunden" });
}
return bookmark;
});
app.delete("/api/bookmarks/:id", async (request, reply) => {
const { id } = request.params as { id: string };
const deleted = bookmarkRepo.deleteBookmark(id);
if (!deleted) {
return reply.code(404).send({ error: "Lesezeichen nicht gefunden" });
}
return reply.code(204).send();
});
}