diff --git a/refactor-strategy.md b/refactor-strategy.md
index 8731009..d3ff5ab 100644
--- a/refactor-strategy.md
+++ b/refactor-strategy.md
@@ -16,7 +16,7 @@ Last updated: 2025-10-18
- [x] Extract router into `src/router/index.js` with named routes
- [x] Add route meta `requiresAuth` and a global auth guard
- [x] Remove auth-redirect from `App.vue` (handled by guard instead)
-- [ ] Convert top-level nav to use route names consistently (optional)
+- [x] Convert top-level nav to use route names consistently (optional)
Outcome: Routing logic is centralized and testable; pages redirect consistently based on auth.
@@ -24,9 +24,9 @@ Outcome: Routing logic is centralized and testable; pages redirect consistently
- [x] Add `src/api/http.js` wrapper for JSON fetch with error handling and env-based base URL
- [x] Add `src/api/mappers/mealMapper.js` to normalize Meal data (dates)
- [x] Add `src/api/meals.js` and migrate MealPlan API calls (get upcoming, mark consumed, delete)
-- [ ] Create `src/api/recipes.js` and migrate recipe endpoints
-- [ ] Create `src/api/shopping.js` and migrate shopping endpoints
-- [ ] Create `src/api/auth.js` and migrate auth endpoints
+- [x] Create `src/api/recipes.js` and migrate recipe endpoints
+- [x] Create `src/api/shopping.js` and migrate shopping endpoints
+- [x] Create `src/api/auth.js` and migrate auth endpoints
Outcome: Feature modules call cohesive services; logic for mapping/normalization is isolated and testable.
@@ -60,3 +60,6 @@ Outcome: Confidence in refactors and easier onboarding.
- Removed `App.vue` auth redirect
- Added `api/http.js`, `api/mappers/mealMapper.js`, `api/meals.js`
- Updated `MealPlanPage.vue` to use meals API
+ - Added `api/mappers/recipeMapper.js`, `api/recipes.js`; updated recipes components
+ - Added `api/mappers/shoppingListMapper.js`, `api/shopping.js`; updated shopping components
+ - Added `api/auth.js` and `api/persons.js`; router uses auth API
diff --git a/src/App.vue b/src/App.vue
index 388ea53..970d7fa 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -2,13 +2,13 @@
-
- Recipes
+ Recipes
-
- Meal Plan
+ Meal Plan
-
- Shopping
+ Shopping
diff --git a/src/api/auth.js b/src/api/auth.js
new file mode 100644
index 0000000..428b7a1
--- /dev/null
+++ b/src/api/auth.js
@@ -0,0 +1,21 @@
+import { http } from './http'
+
+let cachedUser = null
+
+export async function currentUser() {
+ if (cachedUser) return cachedUser
+ // Try a refresh if a cookie exists (browser will send it automatically)
+ try {
+ const user = await http.post('/auth/refresh')
+ cachedUser = user
+ } catch (_) {
+ cachedUser = null
+ }
+ return cachedUser
+}
+
+export async function login(username) {
+ const user = await http.post('/auth/login', { username })
+ cachedUser = user
+ return user
+}
diff --git a/src/api/mappers/recipeMapper.js b/src/api/mappers/recipeMapper.js
new file mode 100644
index 0000000..3660412
--- /dev/null
+++ b/src/api/mappers/recipeMapper.js
@@ -0,0 +1,15 @@
+function toDate(value) {
+ return value ? new Date(value) : value
+}
+
+export function mapRecipe(recipe) {
+ if (!recipe) return recipe
+ recipe.date_created = toDate(recipe.date_created)
+ recipe.date_hidden = toDate(recipe.date_hidden)
+ return recipe
+}
+
+export function mapRecipes(list) {
+ if (!Array.isArray(list)) return []
+ return list.map(mapRecipe)
+}
diff --git a/src/api/mappers/shoppingListMapper.js b/src/api/mappers/shoppingListMapper.js
new file mode 100644
index 0000000..51a17e6
--- /dev/null
+++ b/src/api/mappers/shoppingListMapper.js
@@ -0,0 +1,69 @@
+import { mapMeal } from './mealMapper'
+import { mapRecipe } from './recipeMapper'
+
+function toDate(value) {
+ return value ? new Date(value) : value
+}
+
+function attachItemRefs(items, { ingredients_lookup, meals_lookup, recipes_lookup, shopping_list_lookup }) {
+ if (!Array.isArray(items)) return
+ for (const item of items) {
+ if (item.ingredient_id && ingredients_lookup) item.ingredient = ingredients_lookup[item.ingredient_id]
+ if (item.meal_id && meals_lookup) item.meal = meals_lookup[item.meal_id]
+ if (item.recipe_id && recipes_lookup) item.recipe = recipes_lookup[item.recipe_id]
+ if (item.list_id && shopping_list_lookup) item.list = shopping_list_lookup[item.list_id]
+ if (item.created_date) item.created_date = toDate(item.created_date)
+ }
+}
+
+export function mapPurchasedShoppingList(dto) {
+ if (!dto) return dto
+ // Map lookups
+ if (dto.meals_lookup) {
+ for (const [k, v] of Object.entries(dto.meals_lookup)) dto.meals_lookup[k] = mapMeal(v)
+ }
+ if (dto.recipes_lookup) {
+ for (const [k, v] of Object.entries(dto.recipes_lookup)) dto.recipes_lookup[k] = mapRecipe(v)
+ }
+ if (dto.ingredients_lookup) {
+ // no date fields expected on ingredient based on current usage
+ }
+ if (dto.list) {
+ if (dto.list.created_date) dto.list.created_date = toDate(dto.list.created_date)
+ if (Array.isArray(dto.list.items)) {
+ attachItemRefs(dto.list.items, {
+ ingredients_lookup: dto.ingredients_lookup,
+ meals_lookup: dto.meals_lookup,
+ recipes_lookup: dto.recipes_lookup,
+ shopping_list_lookup: { [dto.list.id]: dto.list },
+ })
+ }
+ }
+ return dto
+}
+
+export function mapCurrentShoppingList(dto) {
+ if (!dto) return dto
+ if (dto.meals_lookup) {
+ for (const [k, v] of Object.entries(dto.meals_lookup)) dto.meals_lookup[k] = mapMeal(v)
+ }
+ if (dto.recipes_lookup) {
+ for (const [k, v] of Object.entries(dto.recipes_lookup)) dto.recipes_lookup[k] = mapRecipe(v)
+ }
+ if (dto.shopping_list_lookup) {
+ for (const [, v] of Object.entries(dto.shopping_list_lookup)) {
+ if (v?.created_date) v.created_date = toDate(v.created_date)
+ }
+ }
+
+ const lookups = {
+ ingredients_lookup: dto.ingredients_lookup,
+ meals_lookup: dto.meals_lookup,
+ recipes_lookup: dto.recipes_lookup,
+ shopping_list_lookup: dto.shopping_list_lookup,
+ }
+ attachItemRefs(dto.outstanding_items, lookups)
+ attachItemRefs(dto.requested_meals, lookups)
+ attachItemRefs(dto.purchased_items, lookups)
+ return dto
+}
diff --git a/src/api/persons.js b/src/api/persons.js
new file mode 100644
index 0000000..1c3c3ca
--- /dev/null
+++ b/src/api/persons.js
@@ -0,0 +1,5 @@
+import { http } from './http'
+
+export async function getPersonsInHome() {
+ return http.get('/persons')
+}
diff --git a/src/api/recipes.js b/src/api/recipes.js
new file mode 100644
index 0000000..ae041bf
--- /dev/null
+++ b/src/api/recipes.js
@@ -0,0 +1,36 @@
+import { http } from './http'
+import { mapRecipe, mapRecipes } from './mappers/recipeMapper'
+
+export async function searchRecipes(query) {
+ const recipes = await http.get(`/recipes?q=${encodeURIComponent(query)}`)
+ return mapRecipes(recipes)
+}
+
+export async function getRecipe(id) {
+ const recipe = await http.get(`/recipes/${encodeURIComponent(id)}`)
+ return mapRecipe(recipe)
+}
+
+export async function saveRecipe(recipe) {
+ const saved = await http.post('/recipes', recipe)
+ return mapRecipe(saved)
+}
+
+export async function deleteRecipe(id) {
+ return http.del(`/recipes/${encodeURIComponent(id)}`)
+}
+
+export async function parseRecipe(url) {
+ const recipe = await http.get(`/recipes/parse?url=${encodeURIComponent(url)}`)
+ return mapRecipe(recipe)
+}
+
+export async function parseIngredients(lines) {
+ const params = lines.map(line => `ingredients=${encodeURIComponent(line)}`).join('&')
+ return http.get(`/recipes/ingredients/parse?${params}`)
+}
+
+export async function parseProduct(ingredient, url) {
+ const body = { url, tags: [ingredient.name, ingredient.line] }
+ return http.post('/products', body)
+}
diff --git a/src/api/shopping.js b/src/api/shopping.js
new file mode 100644
index 0000000..fd6bef5
--- /dev/null
+++ b/src/api/shopping.js
@@ -0,0 +1,38 @@
+import { http } from './http'
+import { mapPurchasedShoppingList, mapCurrentShoppingList } from './mappers/shoppingListMapper'
+
+export async function getMyShoppingList() {
+ const ingredients = await http.get('/shopping/current/me/ingredients')
+ return ingredients
+}
+
+export async function saveMyShoppingList(list) {
+ const ingredients = await http.post('/shopping/current/me/ingredients', list)
+ return ingredients
+}
+
+export async function getShoppingList(id) {
+ const dto = await http.get(`/shopping/${encodeURIComponent(id)}`)
+ const mapped = mapPurchasedShoppingList(dto)
+ return mapped.list
+}
+
+export async function getCurrentShoppingList() {
+ const dto = await http.get('/shopping/current')
+ return mapCurrentShoppingList(dto)
+}
+
+export async function purchaseShoppingList(completedRequests) {
+ const dto = await http.post('/shopping/', { items: completedRequests })
+ const mapped = mapPurchasedShoppingList(dto)
+ return mapped.list
+}
+
+export async function requestMeal(mealId) {
+ const items = await http.post('/shopping/current/meals/me', { meal_id: mealId })
+ return items
+}
+
+export async function unrequestMeal(mealId) {
+ await http.del(`/shopping/current/meals/${encodeURIComponent(mealId)}`)
+}
diff --git a/src/components/ingredients/EditableIngredientsPanel.vue b/src/components/ingredients/EditableIngredientsPanel.vue
index 148b9f9..0c6f3f6 100644
--- a/src/components/ingredients/EditableIngredientsPanel.vue
+++ b/src/components/ingredients/EditableIngredientsPanel.vue
@@ -75,7 +75,7 @@ li > div {