kiwi/frontend/src/stores/savedRecipes.ts
pyr0ball ed04b655be fix(saved-recipes): resolve FK constraint, null title, and load reliability
- 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.
2026-04-25 21:44:10 -07:00

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,
}
})