- Migration 039: drop saved_recipes.recipe_id FK (SQLite table rebuild).
The FK referenced main.recipes but corpus lives in an ATTACH'd DB — caused
500 on every POST /recipes/saved in cloud mode.
- _to_summary: row.get("title") or "" to handle corpus JOIN returning NULL
title (e.g. placeholder recipe_id 99999).
- list_collections: return [] for Free tier instead of 403 — prevents
Promise.all in savedStore.load() from aborting the saved-recipes fetch.
- savedStore.load(): switched to Promise.allSettled so a collections failure
never blocks the saved list from populating.
- RecipesView: star indicator now reflects savedStore.isSaved() (server-side
saved state) rather than localStorage bookmarks; changed to <span> since
the star is now read-only visual feedback.
- Removed { immediate: true } from saved-tab watcher — premature bounce to
Build Your Own before onMounted load() completes.
86 lines
3.2 KiB
TypeScript
86 lines
3.2 KiB
TypeScript
/**
|
|
* Saved Recipes Store
|
|
*
|
|
* Manages bookmarked recipes, ratings, style tags, and collections.
|
|
*/
|
|
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import { savedRecipesAPI, type SavedRecipe, type RecipeCollection } from '../services/api'
|
|
|
|
export const useSavedRecipesStore = defineStore('savedRecipes', () => {
|
|
const saved = ref<SavedRecipe[]>([])
|
|
const collections = ref<RecipeCollection[]>([])
|
|
const loading = ref(false)
|
|
const sortBy = ref<'saved_at' | 'rating' | 'title'>('saved_at')
|
|
const activeCollectionId = ref<number | null>(null)
|
|
|
|
const savedIds = computed(() => new Set(saved.value.map((s) => s.recipe_id)))
|
|
|
|
function isSaved(recipeId: number): boolean {
|
|
return savedIds.value.has(recipeId)
|
|
}
|
|
|
|
function getSaved(recipeId: number): SavedRecipe | undefined {
|
|
return saved.value.find((s) => s.recipe_id === recipeId)
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
try {
|
|
// Fetch independently — a collections 403 (Free tier) must not prevent
|
|
// saved recipes from loading. Backend now returns [] for Free, but guard
|
|
// here too in case an older API version is deployed.
|
|
const [itemsResult, colsResult] = await Promise.allSettled([
|
|
savedRecipesAPI.list({ sort_by: sortBy.value, collection_id: activeCollectionId.value ?? undefined }),
|
|
savedRecipesAPI.listCollections(),
|
|
])
|
|
if (itemsResult.status === 'fulfilled') saved.value = itemsResult.value
|
|
if (colsResult.status === 'fulfilled') collections.value = colsResult.value
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function save(recipeId: number, notes?: string, rating?: number): Promise<SavedRecipe> {
|
|
const result = await savedRecipesAPI.save(recipeId, notes, rating)
|
|
const idx = saved.value.findIndex((s) => s.recipe_id === recipeId)
|
|
if (idx >= 0) {
|
|
saved.value = [...saved.value.slice(0, idx), result, ...saved.value.slice(idx + 1)]
|
|
} else {
|
|
saved.value = [result, ...saved.value]
|
|
}
|
|
return result
|
|
}
|
|
|
|
async function unsave(recipeId: number): Promise<void> {
|
|
await savedRecipesAPI.unsave(recipeId)
|
|
saved.value = saved.value.filter((s) => s.recipe_id !== recipeId)
|
|
}
|
|
|
|
async function update(recipeId: number, data: { notes?: string | null; rating?: number | null; style_tags?: string[] }): Promise<SavedRecipe> {
|
|
const result = await savedRecipesAPI.update(recipeId, data)
|
|
const idx = saved.value.findIndex((s) => s.recipe_id === recipeId)
|
|
if (idx >= 0) {
|
|
saved.value = [...saved.value.slice(0, idx), result, ...saved.value.slice(idx + 1)]
|
|
}
|
|
return result
|
|
}
|
|
|
|
async function createCollection(name: string, description?: string): Promise<RecipeCollection> {
|
|
const col = await savedRecipesAPI.createCollection(name, description)
|
|
collections.value = [...collections.value, col]
|
|
return col
|
|
}
|
|
|
|
async function deleteCollection(id: number): Promise<void> {
|
|
await savedRecipesAPI.deleteCollection(id)
|
|
collections.value = collections.value.filter((c) => c.id !== id)
|
|
if (activeCollectionId.value === id) activeCollectionId.value = null
|
|
}
|
|
|
|
return {
|
|
saved, collections, loading, sortBy, activeCollectionId,
|
|
savedIds, isSaved, getSaved,
|
|
load, save, unsave, update, createCollection, deleteCollection,
|
|
}
|
|
})
|