From 1e70b4b1f67ac509381330fe3c159e140a6c4acb Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 31 Mar 2026 19:20:13 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20recipe=20+=20settings=20frontend=20?= =?UTF-8?q?=E2=80=94=20Recipes=20and=20Settings=20tabs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RecipesView: level selector (1-4), constraints/allergies tag inputs, hard day mode toggle, max missing input, expiry-first pantry extraction, recipe cards with collapsible swaps/directions, grocery links, rate limit banner - SettingsView: cooking equipment tag input with quick-add chips, save with confirmation feedback - stores/recipes.ts: Pinia store for recipe state + suggest() action - stores/settings.ts: Pinia store for cooking_equipment persistence - api.ts: RecipeRequest/Result/Suggestion types + recipesAPI + settingsAPI - App.vue: two new tabs (Recipes, Settings), lazy inventory load on tab switch --- frontend/src/App.vue | 33 +- frontend/src/components/RecipesView.vue | 524 +++++++++++++++++++++++ frontend/src/components/SettingsView.vue | 162 +++++++ frontend/src/services/api.ts | 90 ++++ frontend/src/stores/recipes.ts | 78 ++++ frontend/src/stores/settings.ts | 57 +++ 6 files changed, 942 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/RecipesView.vue create mode 100644 frontend/src/components/SettingsView.vue create mode 100644 frontend/src/stores/recipes.ts create mode 100644 frontend/src/stores/settings.ts diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 81aa886..bc62189 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -23,6 +23,18 @@ > 🧾 Receipts + + @@ -33,6 +45,14 @@
+ +
+ +
+ +
+ +
@@ -48,11 +68,20 @@ import { ref } from 'vue' import InventoryList from './components/InventoryList.vue' import ReceiptsView from './components/ReceiptsView.vue' +import RecipesView from './components/RecipesView.vue' +import SettingsView from './components/SettingsView.vue' +import { useInventoryStore } from './stores/inventory' -const currentTab = ref<'inventory' | 'receipts'>('inventory') +type Tab = 'inventory' | 'receipts' | 'recipes' | 'settings' -function switchTab(tab: 'inventory' | 'receipts') { +const currentTab = ref('inventory') +const inventoryStore = useInventoryStore() + +async function switchTab(tab: Tab) { currentTab.value = tab + if (tab === 'recipes' && inventoryStore.items.length === 0) { + await inventoryStore.fetchItems() + } } diff --git a/frontend/src/components/RecipesView.vue b/frontend/src/components/RecipesView.vue new file mode 100644 index 0000000..1b0a7ca --- /dev/null +++ b/frontend/src/components/RecipesView.vue @@ -0,0 +1,524 @@ + + + + + diff --git a/frontend/src/components/SettingsView.vue b/frontend/src/components/SettingsView.vue new file mode 100644 index 0000000..058cb83 --- /dev/null +++ b/frontend/src/components/SettingsView.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 02fc61a..80ebe27 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -404,4 +404,94 @@ export const exportAPI = { }, } +// ========== Recipes & Settings Types ========== + +export interface SwapCandidate { + original_name: string + substitute_name: string + constraint_label: string + explanation: string + compensation_hints: Record[] +} + +export interface RecipeSuggestion { + id: number + title: string + match_count: number + element_coverage: Record + swap_candidates: SwapCandidate[] + missing_ingredients: string[] + directions: string[] + notes: string + level: number + is_wildcard: boolean +} + +export interface GroceryLink { + ingredient: string + retailer: string + url: string +} + +export interface RecipeResult { + suggestions: RecipeSuggestion[] + element_gaps: string[] + grocery_list: string[] + grocery_links: GroceryLink[] + rate_limited: boolean + rate_limit_count: number +} + +export interface RecipeRequest { + pantry_items: string[] + level: number + constraints: string[] + allergies: string[] + expiry_first: boolean + hard_day_mode: boolean + max_missing: number | null + style_id: string | null + wildcard_confirmed: boolean +} + +export interface Staple { + slug: string + name: string + category: string + dietary_tags: string[] +} + +// ========== Recipes API ========== + +export const recipesAPI = { + async suggest(req: RecipeRequest): Promise { + const response = await api.post('/recipes/suggest', req) + return response.data + }, + async getRecipe(id: number): Promise { + const response = await api.get(`/recipes/${id}`) + return response.data + }, + async listStaples(dietary?: string): Promise { + const response = await api.get('/staples/', { params: dietary ? { dietary } : undefined }) + return response.data + }, +} + +// ========== Settings API ========== + +export const settingsAPI = { + async getSetting(key: string): Promise { + try { + const response = await api.get(`/settings/${key}`) + return response.data.value + } catch { + return null + } + }, + async setSetting(key: string, value: string): Promise { + await api.put(`/settings/${key}`, { value }) + }, +} + export default api diff --git a/frontend/src/stores/recipes.ts b/frontend/src/stores/recipes.ts new file mode 100644 index 0000000..56ff397 --- /dev/null +++ b/frontend/src/stores/recipes.ts @@ -0,0 +1,78 @@ +/** + * Recipes Store + * + * Manages recipe suggestion state and request parameters using Pinia. + */ + +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { recipesAPI, type RecipeResult, type RecipeRequest } from '../services/api' + +export const useRecipesStore = defineStore('recipes', () => { + // State + const result = ref(null) + const loading = ref(false) + const error = ref(null) + const level = ref(1) + const constraints = ref([]) + const allergies = ref([]) + const hardDayMode = ref(false) + const maxMissing = ref(null) + const styleId = ref(null) + const wildcardConfirmed = ref(false) + + // Actions + async function suggest(pantryItems: string[]) { + loading.value = true + error.value = null + + const req: RecipeRequest = { + pantry_items: pantryItems, + level: level.value, + constraints: constraints.value, + allergies: allergies.value, + expiry_first: true, + hard_day_mode: hardDayMode.value, + max_missing: maxMissing.value, + style_id: styleId.value, + wildcard_confirmed: wildcardConfirmed.value, + } + + try { + result.value = await recipesAPI.suggest(req) + } catch (err: unknown) { + if (err instanceof Error) { + error.value = err.message + } else { + error.value = 'Failed to get recipe suggestions' + } + console.error('Error fetching recipe suggestions:', err) + } finally { + loading.value = false + } + } + + function clearResult() { + result.value = null + error.value = null + wildcardConfirmed.value = false + } + + return { + // State + result, + loading, + error, + level, + constraints, + allergies, + hardDayMode, + maxMissing, + styleId, + wildcardConfirmed, + + // Actions + suggest, + clearResult, + } +}) diff --git a/frontend/src/stores/settings.ts b/frontend/src/stores/settings.ts new file mode 100644 index 0000000..8daeb86 --- /dev/null +++ b/frontend/src/stores/settings.ts @@ -0,0 +1,57 @@ +/** + * Settings Store + * + * Manages user settings (cooking equipment, preferences) using Pinia. + */ + +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { settingsAPI } from '../services/api' + +export const useSettingsStore = defineStore('settings', () => { + // State + const cookingEquipment = ref([]) + const loading = ref(false) + const saved = ref(false) + + // Actions + async function load() { + loading.value = true + try { + const raw = await settingsAPI.getSetting('cooking_equipment') + if (raw) { + cookingEquipment.value = JSON.parse(raw) + } + } catch (err: unknown) { + console.error('Failed to load settings:', err) + } finally { + loading.value = false + } + } + + async function save() { + loading.value = true + try { + await settingsAPI.setSetting('cooking_equipment', JSON.stringify(cookingEquipment.value)) + saved.value = true + setTimeout(() => { + saved.value = false + }, 2000) + } catch (err: unknown) { + console.error('Failed to save settings:', err) + } finally { + loading.value = false + } + } + + return { + // State + cookingEquipment, + loading, + saved, + + // Actions + load, + save, + } +})