feat(home): „Alle Rezepte"-Sektion mit Sortierung und Endless-Scroll
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m21s

Neue Sektion unter „Zuletzt hinzugefügt": sortierbar nach Name,
Bewertung, zuletzt gekocht und Hinzugefügt. Auswahl persistiert in
localStorage (kochwas.allSort).

- Neuer Endpoint GET /api/recipes/all?sort=name&limit=10&offset=0.
- listAllRecipesPaginated(db, sort, limit, offset) im repository:
  NULLS-last-Emulation per CASE für rating/cooked — funktioniert auch
  auf älteren SQLite-Versionen.
- Endless Scroll per IntersectionObserver auf ein Sentinel-Element am
  Listen-Ende (rootMargin 200px, damit schon vor dem harten Rand
  nachgeladen wird). Pagesize 10.
- 4 neue Tests: Name-Sort, Rating-Sort, Cooked-Sort, Pagination-Offset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-18 11:14:44 +02:00
parent a1d91943c6
commit 09c0270c64
4 changed files with 276 additions and 1 deletions

View File

@@ -95,6 +95,43 @@ export function listAllRecipes(db: Database.Database): SearchHit[] {
.all() as SearchHit[];
}
export type AllRecipesSort = 'name' | 'rating' | 'cooked' | 'created';
export function listAllRecipesPaginated(
db: Database.Database,
sort: AllRecipesSort,
limit: number,
offset: number
): SearchHit[] {
// NULLS-last-Emulation per CASE-Expression — SQLite unterstützt NULLS LAST
// zwar seit 3.30, aber der Pi könnte auf einer älteren Version laufen und
// CASE ist überall zuverlässig.
const orderBy: Record<AllRecipesSort, string> = {
name: 'r.title COLLATE NOCASE ASC',
rating:
'CASE WHEN (SELECT AVG(stars) FROM rating WHERE recipe_id = r.id) IS NULL THEN 1 ELSE 0 END, ' +
'(SELECT AVG(stars) FROM rating WHERE recipe_id = r.id) DESC, r.title COLLATE NOCASE ASC',
cooked:
'CASE WHEN (SELECT MAX(cooked_at) FROM cooking_log WHERE recipe_id = r.id) IS NULL THEN 1 ELSE 0 END, ' +
'(SELECT MAX(cooked_at) FROM cooking_log WHERE recipe_id = r.id) DESC, r.title COLLATE NOCASE ASC',
created: 'r.created_at DESC, r.id DESC'
};
return db
.prepare(
`SELECT r.id,
r.title,
r.description,
r.image_path,
r.source_domain,
(SELECT AVG(stars) FROM rating WHERE recipe_id = r.id) AS avg_stars,
(SELECT MAX(cooked_at) FROM cooking_log WHERE recipe_id = r.id) AS last_cooked_at
FROM recipe r
ORDER BY ${orderBy[sort]}
LIMIT ? OFFSET ?`
)
.all(limit, offset) as SearchHit[];
}
export function listFavoritesForProfile(
db: Database.Database,
profileId: number