+
+
{{ lastCookedLabel(recipe.recipe_id) }}
@@ -165,20 +166,32 @@ const recipesStore = useRecipesStore()
const editingRecipe = ref
(null)
function lastCookedLabel(recipeId: number): string | null {
- const entries = recipesStore.cookLog.filter((e) => e.id === recipeId)
- if (entries.length === 0) return null
- const latestMs = Math.max(...entries.map((e) => e.cookedAt))
- const diffMs = Date.now() - latestMs
- const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
- if (diffDays === 0) return 'Last made: today'
- if (diffDays === 1) return 'Last made: yesterday'
- if (diffDays < 7) return `Last made: ${diffDays} days ago`
- if (diffDays < 14) return 'Last made: 1 week ago'
- const diffWeeks = Math.floor(diffDays / 7)
- if (diffDays < 60) return `Last made: ${diffWeeks} weeks ago`
- const diffMonths = Math.floor(diffDays / 30)
- return `Last made: ${diffMonths} month${diffMonths !== 1 ? 's' : ''} ago`
+ const days = recipesStore.lastCookedDaysAgo(recipeId)
+ if (days === null) return null
+ if (days === 0) return 'made today'
+ if (days === 1) return 'made yesterday'
+ if (days < 7) return `made ${days} days ago`
+ if (days < 14) return 'made 1 week ago'
+ const weeks = Math.floor(days / 7)
+ if (days < 60) return `made ${weeks} weeks ago`
+ const months = Math.floor(days / 30)
+ return `made ${months} month${months !== 1 ? 's' : ''} ago`
}
+
+// Client-side last_cooked sort — resolves from localStorage cook log so no API change needed.
+// Recipes with a cook date surface oldest-first (natural "due for a revisit" order without
+// framing it that way). Recipes never cooked sort to the end.
+const sortedSaved = computed(() => {
+ if (store.sortBy !== 'last_cooked') return store.saved
+ return [...store.saved].sort((a, b) => {
+ const daysA = recipesStore.lastCookedDaysAgo(a.recipe_id)
+ const daysB = recipesStore.lastCookedDaysAgo(b.recipe_id)
+ if (daysA === null && daysB === null) return 0
+ if (daysA === null) return 1 // never cooked → end
+ if (daysB === null) return -1 // never cooked → end
+ return daysB - daysA // oldest cooked first (largest days value first)
+ })
+})
const showNewCollection = ref(false)
// #44: two-step remove confirmation
@@ -363,9 +376,14 @@ async function createCollection() {
padding: var(--spacing-xl);
}
-.last-cooked-hint {
- font-style: italic;
- opacity: 0.75;
+.last-cooked-chip {
+ display: inline-block;
+ color: var(--color-text-muted, var(--color-secondary, #888));
+ background: var(--color-surface-subtle, transparent);
+ border-radius: var(--radius-sm, 4px);
+ padding: 0 var(--spacing-xs, 4px);
+ font-style: normal;
+ opacity: 0.8;
}
.modal-overlay {
diff --git a/frontend/src/stores/recipes.ts b/frontend/src/stores/recipes.ts
index 5046b34..f85df6b 100644
--- a/frontend/src/stores/recipes.ts
+++ b/frontend/src/stores/recipes.ts
@@ -318,6 +318,8 @@ export const useRecipesStore = defineStore('recipes', () => {
localStorage.removeItem(DISMISSED_KEY)
}
+ // Orbital cadence: cookedAt anchors to completion, not to a schedule.
+ // Days-since display measures from this timestamp — no debt accumulates.
function logCook(id: number, title: string) {
const entry: CookLogEntry = { id, title, cookedAt: Date.now() }
cookLog.value = [...cookLog.value, entry]
@@ -329,6 +331,13 @@ export const useRecipesStore = defineStore('recipes', () => {
localStorage.removeItem(COOK_LOG_KEY)
}
+ function lastCookedDaysAgo(recipeId: number): number | null {
+ const entries = cookLog.value.filter((e) => e.id === recipeId)
+ if (entries.length === 0) return null
+ const latestMs = Math.max(...entries.map((e) => e.cookedAt))
+ return Math.floor((Date.now() - latestMs) / 86_400_000)
+ }
+
function isBookmarked(id: number): boolean {
return bookmarks.value.some((b) => b.id === id)
}
@@ -393,6 +402,7 @@ export const useRecipesStore = defineStore('recipes', () => {
cookLog,
logCook,
clearCookLog,
+ lastCookedDaysAgo,
bookmarks,
isBookmarked,
toggleBookmark,
diff --git a/frontend/src/stores/savedRecipes.ts b/frontend/src/stores/savedRecipes.ts
index af7f2e5..56acbb6 100644
--- a/frontend/src/stores/savedRecipes.ts
+++ b/frontend/src/stores/savedRecipes.ts
@@ -11,7 +11,7 @@ export const useSavedRecipesStore = defineStore('savedRecipes', () => {
const saved = ref([])
const collections = ref([])
const loading = ref(false)
- const sortBy = ref<'saved_at' | 'rating' | 'title'>('saved_at')
+ const sortBy = ref<'saved_at' | 'rating' | 'title' | 'last_cooked'>('saved_at')
const activeCollectionId = ref(null)
const savedIds = computed(() => new Set(saved.value.map((s) => s.recipe_id)))
@@ -31,7 +31,7 @@ export const useSavedRecipesStore = defineStore('savedRecipes', () => {
// 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.list({ sort_by: sortBy.value === 'last_cooked' ? 'saved_at' : sortBy.value, collection_id: activeCollectionId.value ?? undefined }),
savedRecipesAPI.listCollections(),
])
if (itemsResult.status === 'fulfilled') saved.value = itemsResult.value