Composition #3

This commit is contained in:
jableader 2025-10-18 13:53:24 +11:00
parent beab02200c
commit d31182dff0
4 changed files with 105 additions and 110 deletions

View file

@ -72,9 +72,10 @@ Last updated: 2025-10-18
Already using `<script setup>`: Already using `<script setup>`:
- Meals: `MealPlanPage.vue`, `EditMealPage.vue`, `meals/MealCard.vue` - Meals: `MealPlanPage.vue`, `EditMealPage.vue`, `meals/MealCard.vue`
- Shopping: `CurrentShoppingListPage.vue`, `MyShoppingPage.vue`, `PurchasedShoppingListPage.vue`, `shopping/MealSelectionList.vue` - Shopping: `CurrentShoppingListPage.vue`, `MyShoppingPage.vue`, `PurchasedShoppingListPage.vue`, `shopping/MealSelectionList.vue`, `shopping/ShoppingListItem.vue`
- Core/Leaf: `components/ActionItem.vue`, `recipes/RecipeCard.vue`, `ingredients/CompactParsedIngredient.vue` - Core/Leaf: `components/ActionItem.vue`, `recipes/RecipeCard.vue`, `ingredients/CompactParsedIngredient.vue`
- Ingredients: `ingredients/IngredientLine.vue`, `ingredients/EditableIngredientsPanel.vue` - Ingredients: `ingredients/IngredientLine.vue`, `ingredients/EditableIngredientsPanel.vue`
- Recipes: `components/recipes/RecipesPage.vue`, `components/recipes/RecipeSearchBox.vue`
Remaining to migrate (Options API or mixed): Remaining to migrate (Options API or mixed):
@ -84,8 +85,6 @@ Remaining to migrate (Options API or mixed):
- `components/LoginPage.vue` - `components/LoginPage.vue`
- Recipes - Recipes
- `components/recipes/RecipesPage.vue`
- `components/recipes/RecipeSearchBox.vue`
- `components/recipes/EditRecipePage.vue` (most complex) - `components/recipes/EditRecipePage.vue` (most complex)
- Meals - Meals
@ -143,8 +142,8 @@ Remaining to migrate (Options API or mixed):
- [x] Core: `components/ActionItem.vue` - [x] Core: `components/ActionItem.vue`
- [ ] Core: `components/LoginPage.vue` - [ ] Core: `components/LoginPage.vue`
- [ ] Recipes: `components/recipes/RecipesPage.vue` - [x] Recipes: `components/recipes/RecipesPage.vue`
- [ ] Recipes: `components/recipes/RecipeSearchBox.vue` - [x] Recipes: `components/recipes/RecipeSearchBox.vue`
- [x] Recipes: `components/recipes/RecipeCard.vue` - [x] Recipes: `components/recipes/RecipeCard.vue`
- [ ] Recipes: `components/recipes/EditRecipePage.vue` - [ ] Recipes: `components/recipes/EditRecipePage.vue`
@ -158,7 +157,7 @@ Remaining to migrate (Options API or mixed):
- [x] Shopping: `components/shopping/MealSelectionList.vue` - [x] Shopping: `components/shopping/MealSelectionList.vue`
- [ ] Shopping: `components/shopping/ShoppingListItem.vue` - [x] Shopping: `components/shopping/ShoppingListItem.vue`
## Notes and risks ## Notes and risks

View file

@ -1,10 +1,10 @@
<template> <template>
<div class="recipe-search-box" @focusout="recipes = []"> <div class="recipe-search-box" @focusout="onFocusOut">
<input <input
type="text" type="text"
v-model="searchTerm" v-model="searchTerm"
@keyup.enter="search" @keyup.enter="search"
@keyup.exit="clear" @keyup.esc="clear"
@focusin="search" @focusin="search"
:placeholder="placeholder" :placeholder="placeholder"
/> />
@ -21,48 +21,62 @@
</div> </div>
</template> </template>
<script> <script setup>
import { ref, watch, onBeforeUnmount } from 'vue'
import { searchRecipes } from '@/api/recipes' import { searchRecipes } from '@/api/recipes'
import RecipeCard from './RecipeCard.vue' import RecipeCard from './RecipeCard.vue'
export default { defineProps({
name: 'RecipeSearchBox', placeholder: { type: String, default: 'Add a recipe...' },
components: { RecipeCard }, })
props: {
placeholder: { type: String, default: 'Add a recipe...' }, const emit = defineEmits(['select-recipe'])
},
data() { const searchTerm = ref('')
return { const recipes = ref([])
searchTerm: '',
recipes: [], let debounceId = null
timeouts: [],
watch(
searchTerm,
(newVal) => {
if (!newVal) {
recipes.value = []
if (debounceId) clearTimeout(debounceId)
return
} }
}, if (debounceId) clearTimeout(debounceId)
watch: { debounceId = setTimeout(() => {
searchTerm() { if (newVal === searchTerm.value) {
const searchTerm = this.searchTerm search()
if (searchTerm) {
this.timeouts.push(
setTimeout(() => {
if (searchTerm === this.searchTerm) {
this.search()
}
}, 200)
)
} }
}, }, 200)
}, }
methods: { )
async search() {
this.recipes = (await searchRecipes(this.searchTerm)) ?? this.recipes async function search() {
}, const result = await searchRecipes(searchTerm.value)
selectRecipe(recipe) { recipes.value = result ?? recipes.value
this.$emit('select-recipe', recipe)
this.searchTerm = ''
this.recipes = []
},
},
} }
function selectRecipe(recipe) {
emit('select-recipe', recipe)
searchTerm.value = ''
recipes.value = []
}
function clear() {
searchTerm.value = ''
recipes.value = []
}
function onFocusOut() {
recipes.value = []
}
onBeforeUnmount(() => {
if (debounceId) clearTimeout(debounceId)
})
</script> </script>
<style scoped> <style scoped>

View file

@ -2,24 +2,27 @@
<div> <div>
<recipe-search-box <recipe-search-box
placeholder="Search for a recipe..." placeholder="Search for a recipe..."
@select-recipe="(r) => this.$router.push(`/recipes/${r.id}`)" @select-recipe="onSelectRecipe"
/> />
<action-item <action-item
title="Add new Recipe" title="Add new Recipe"
:image="require('@/assets/add-recipe.svg')" :image="require('@/assets/add-recipe.svg')"
@click="() => this.$router.push('/recipes/add')" @click="onAddRecipe"
/> />
</div> </div>
</template> </template>
<script> <script setup>
import { useRouter } from 'vue-router'
import ActionItem from '@/components/ActionItem.vue' import ActionItem from '@/components/ActionItem.vue'
import RecipeSearchBox from './RecipeSearchBox.vue' import RecipeSearchBox from './RecipeSearchBox.vue'
export default { const router = useRouter()
name: 'ActionsPage',
components: { function onSelectRecipe(r) {
ActionItem, router.push(`/recipes/${r.id}`)
RecipeSearchBox, }
},
function onAddRecipe() {
router.push('/recipes/add')
} }
</script> </script>

View file

@ -148,67 +148,46 @@
} }
</style> </style>
<script> <script setup>
import { computed } from 'vue'
import { ago } from '@/dateformats.js' import { ago } from '@/dateformats.js'
import { calculateTotals } from '@/units.js' import { calculateTotals } from '@/units.js'
export default { const props = defineProps({
name: 'ShoppingListItem', // { product: { ... }, OR name: 'string', shoppingListItems: [...] }
props: ['shoppingListItemGroup'], // { product: { ... }, OR name: 'string', shoppingListItems: { person, ingredient, list_id?, meal? }} where list_id is null if not yet purchased shoppingListItemGroup: { type: Object, required: true },
data() { })
return {
expanded: false,
}
},
computed: {
remainingRequiredTotals() {
return calculateTotals(
this.shoppingListItemGroup.shoppingListItems.map((item) => item.ingredient)
)
},
expectedExistingTotals() {
const purchasedNotEaten = this.shoppingListItemGroup.shoppingListItems.filter(
(item) => item.list_id && !item?.meal?.consumed_date
)
return calculateTotals(purchasedNotEaten.map((item) => item.ingredient)) const remainingRequiredTotals = computed(() =>
}, calculateTotals(props.shoppingListItemGroup.shoppingListItems.map((item) => item.ingredient))
required() { )
return this.shoppingListItemGroup.shoppingListItems.filter((item) => !item.list_id)
},
purchased() {
return this.shoppingListItemGroup.shoppingListItems.filter((item) => item.list_id)
},
lastPurchased() {
const purchasedItems = this.shoppingListItemGroup.shoppingListItems.filter(
(item) => item.list_id
)
if (purchasedItems.length === 0) {
return null
}
// Find the most recently purchased item
return purchasedItems.reduce((latest, item) => {
const itemDate = item?.meal?.suggested_date || item?.created_at
return !latest || (itemDate && itemDate > latest) ? itemDate : latest
}, null)
},
},
methods: {
getFriendlyDate(date) {
if (!date) return ''
return ago(date) const required = computed(() =>
}, props.shoppingListItemGroup.shoppingListItems.filter((item) => !item.list_id)
formatQuantity(quantity) { )
const log10 = Math.log10(quantity)
if (log10 < 0) { const purchased = computed(() =>
return quantity.toPrecision(2) props.shoppingListItemGroup.shoppingListItems.filter((item) => item.list_id)
} else if (log10 < 1) { )
return quantity.toFixed(1)
} else { const lastPurchased = computed(() => {
return quantity.toFixed(0) const purchasedItems = props.shoppingListItemGroup.shoppingListItems.filter((item) => item.list_id)
} if (purchasedItems.length === 0) return null
}, return purchasedItems.reduce((latest, item) => {
}, const itemDate = item?.meal?.suggested_date || item?.created_at
return !latest || (itemDate && itemDate > latest) ? itemDate : latest
}, null)
})
function getFriendlyDate(date) {
if (!date) return ''
return ago(date)
}
function formatQuantity(quantity) {
const log10 = Math.log10(quantity)
if (log10 < 0) return quantity.toPrecision(2)
if (log10 < 1) return quantity.toFixed(1)
return quantity.toFixed(0)
} }
</script> </script>