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:
19
src/lib/server/db/migrations/002_wishlist.sql
Normal file
19
src/lib/server/db/migrations/002_wishlist.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- Shared family wishlist: recipes someone wants to cook next.
|
||||
-- Each recipe appears at most once; anyone can add/remove and like/unlike.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wishlist (
|
||||
recipe_id INTEGER PRIMARY KEY REFERENCES recipe(id) ON DELETE CASCADE,
|
||||
added_by_profile_id INTEGER REFERENCES profile(id) ON DELETE SET NULL,
|
||||
added_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_wishlist_added_at ON wishlist(added_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wishlist_like (
|
||||
recipe_id INTEGER NOT NULL REFERENCES recipe(id) ON DELETE CASCADE,
|
||||
profile_id INTEGER NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (recipe_id, profile_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_wishlist_like_recipe ON wishlist_like(recipe_id);
|
||||
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