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 @@
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 {