Composition #3
This commit is contained in:
parent
beab02200c
commit
d31182dff0
4 changed files with 105 additions and 110 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue