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>`:
- 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`
- Ingredients: `ingredients/IngredientLine.vue`, `ingredients/EditableIngredientsPanel.vue`
- Recipes: `components/recipes/RecipesPage.vue`, `components/recipes/RecipeSearchBox.vue`
Remaining to migrate (Options API or mixed):
@ -84,8 +85,6 @@ Remaining to migrate (Options API or mixed):
- `components/LoginPage.vue`
- Recipes
- `components/recipes/RecipesPage.vue`
- `components/recipes/RecipeSearchBox.vue`
- `components/recipes/EditRecipePage.vue` (most complex)
- Meals
@ -143,8 +142,8 @@ Remaining to migrate (Options API or mixed):
- [x] Core: `components/ActionItem.vue`
- [ ] Core: `components/LoginPage.vue`
- [ ] Recipes: `components/recipes/RecipesPage.vue`
- [ ] Recipes: `components/recipes/RecipeSearchBox.vue`
- [x] Recipes: `components/recipes/RecipesPage.vue`
- [x] Recipes: `components/recipes/RecipeSearchBox.vue`
- [x] Recipes: `components/recipes/RecipeCard.vue`
- [ ] Recipes: `components/recipes/EditRecipePage.vue`
@ -158,7 +157,7 @@ Remaining to migrate (Options API or mixed):
- [x] Shopping: `components/shopping/MealSelectionList.vue`
- [ ] Shopping: `components/shopping/ShoppingListItem.vue`
- [x] Shopping: `components/shopping/ShoppingListItem.vue`
## Notes and risks

View file

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

View file

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

View file

@ -148,67 +148,46 @@
}
</style>
<script>
<script setup>
import { computed } from 'vue'
import { ago } from '@/dateformats.js'
import { calculateTotals } from '@/units.js'
export default {
name: 'ShoppingListItem',
props: ['shoppingListItemGroup'], // { product: { ... }, OR name: 'string', shoppingListItems: { person, ingredient, list_id?, meal? }} where list_id is null if not yet purchased
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
)
const props = defineProps({
// { product: { ... }, OR name: 'string', shoppingListItems: [...] }
shoppingListItemGroup: { type: Object, required: true },
})
return calculateTotals(purchasedNotEaten.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 ''
const remainingRequiredTotals = computed(() =>
calculateTotals(props.shoppingListItemGroup.shoppingListItems.map((item) => item.ingredient))
)
return ago(date)
},
formatQuantity(quantity) {
const log10 = Math.log10(quantity)
if (log10 < 0) {
return quantity.toPrecision(2)
} else if (log10 < 1) {
return quantity.toFixed(1)
} else {
return quantity.toFixed(0)
}
},
},
const required = computed(() =>
props.shoppingListItemGroup.shoppingListItems.filter((item) => !item.list_id)
)
const purchased = computed(() =>
props.shoppingListItemGroup.shoppingListItems.filter((item) => item.list_id)
)
const lastPurchased = computed(() => {
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>