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>`:
|
||||
- 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
defineProps({
|
||||
placeholder: { type: String, default: 'Add a recipe...' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
recipes: [],
|
||||
timeouts: [],
|
||||
})
|
||||
|
||||
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()
|
||||
if (debounceId) clearTimeout(debounceId)
|
||||
debounceId = setTimeout(() => {
|
||||
if (newVal === searchTerm.value) {
|
||||
search()
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
)
|
||||
|
||||
async function search() {
|
||||
const result = await searchRecipes(searchTerm.value)
|
||||
recipes.value = result ?? recipes.value
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async search() {
|
||||
this.recipes = (await searchRecipes(this.searchTerm)) ?? this.recipes
|
||||
},
|
||||
selectRecipe(recipe) {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
})
|
||||
|
||||
const remainingRequiredTotals = computed(() =>
|
||||
calculateTotals(props.shoppingListItemGroup.shoppingListItems.map((item) => item.ingredient))
|
||||
)
|
||||
|
||||
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
|
||||
const required = computed(() =>
|
||||
props.shoppingListItemGroup.shoppingListItems.filter((item) => !item.list_id)
|
||||
)
|
||||
if (purchasedItems.length === 0) {
|
||||
return null
|
||||
}
|
||||
// Find the most recently purchased item
|
||||
|
||||
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)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getFriendlyDate(date) {
|
||||
if (!date) return ''
|
||||
})
|
||||
|
||||
function getFriendlyDate(date) {
|
||||
if (!date) return ''
|
||||
return ago(date)
|
||||
},
|
||||
formatQuantity(quantity) {
|
||||
}
|
||||
|
||||
function formatQuantity(quantity) {
|
||||
const log10 = Math.log10(quantity)
|
||||
if (log10 < 0) {
|
||||
return quantity.toPrecision(2)
|
||||
} else if (log10 < 1) {
|
||||
return quantity.toFixed(1)
|
||||
} else {
|
||||
if (log10 < 0) return quantity.toPrecision(2)
|
||||
if (log10 < 1) return quantity.toFixed(1)
|
||||
return quantity.toFixed(0)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue