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:
98
src/lib/server/wishlist/repository.ts
Normal file
98
src/lib/server/wishlist/repository.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user