feat(wishlist): add shared family wishlist with likes

Each recipe appears at most once on the wishlist. Any profile can add,
remove, like, and unlike. Ratings and cooking log stay independent.

Data model: wishlist(recipe_id PK, added_by_profile_id, added_at)
            wishlist_like(recipe_id, profile_id, created_at)

Why: 'das will ich essen' — family members pick candidates, everyone
can +1 to signal agreement, cook decides based on popularity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 17:08:22 +02:00
parent 72019f9cb7
commit 18547a7301
4 changed files with 257 additions and 2 deletions

View File

@@ -0,0 +1,98 @@
import type Database from 'better-sqlite3';
export type WishlistEntry = {
recipe_id: number;
title: string;
image_path: string | null;
source_domain: string | null;
added_by_profile_id: number | null;
added_by_name: string | null;
added_at: string;
like_count: number;
liked_by_me: 0 | 1;
avg_stars: number | null;
};
export type SortKey = 'popular' | 'newest' | 'oldest';
export function listWishlist(
db: Database.Database,
activeProfileId: number | null,
sort: SortKey = 'popular'
): WishlistEntry[] {
const orderBy = {
popular: 'like_count DESC, w.added_at DESC',
newest: 'w.added_at DESC',
oldest: 'w.added_at ASC'
}[sort];
return db
.prepare(
`SELECT
w.recipe_id,
r.title,
r.image_path,
r.source_domain,
w.added_by_profile_id,
p.name AS added_by_name,
w.added_at,
(SELECT COUNT(*) FROM wishlist_like wl WHERE wl.recipe_id = w.recipe_id) AS like_count,
CASE
WHEN ? IS NULL THEN 0
WHEN EXISTS (SELECT 1 FROM wishlist_like wl
WHERE wl.recipe_id = w.recipe_id AND wl.profile_id = ?)
THEN 1
ELSE 0
END AS liked_by_me,
(SELECT AVG(stars) FROM rating WHERE recipe_id = w.recipe_id) AS avg_stars
FROM wishlist w
JOIN recipe r ON r.id = w.recipe_id
LEFT JOIN profile p ON p.id = w.added_by_profile_id
ORDER BY ${orderBy}`
)
.all(activeProfileId, activeProfileId) as WishlistEntry[];
}
export function isOnWishlist(db: Database.Database, recipeId: number): boolean {
return (
db
.prepare('SELECT 1 AS ok FROM wishlist WHERE recipe_id = ?')
.get(recipeId) !== undefined
);
}
export function addToWishlist(
db: Database.Database,
recipeId: number,
profileId: number | null
): void {
db.prepare(
`INSERT INTO wishlist(recipe_id, added_by_profile_id)
VALUES (?, ?)
ON CONFLICT(recipe_id) DO NOTHING`
).run(recipeId, profileId);
}
export function removeFromWishlist(db: Database.Database, recipeId: number): void {
db.prepare('DELETE FROM wishlist WHERE recipe_id = ?').run(recipeId);
}
export function likeWish(
db: Database.Database,
recipeId: number,
profileId: number
): void {
db.prepare(
'INSERT OR IGNORE INTO wishlist_like(recipe_id, profile_id) VALUES (?, ?)'
).run(recipeId, profileId);
}
export function unlikeWish(
db: Database.Database,
recipeId: number,
profileId: number
): void {
db.prepare(
'DELETE FROM wishlist_like WHERE recipe_id = ? AND profile_id = ?'
).run(recipeId, profileId);
}