Composition #2

This commit is contained in:
jableader 2025-10-18 13:47:56 +11:00
parent 61ca41d25a
commit beab02200c
4 changed files with 129 additions and 150 deletions

View file

@ -71,35 +71,31 @@ Last updated: 2025-10-18
## Current status ## Current status
Already using `<script setup>`: Already using `<script setup>`:
- Meals: `MealPlanPage.vue`, `EditMealPage.vue` - Meals: `MealPlanPage.vue`, `EditMealPage.vue`, `meals/MealCard.vue`
- Shopping: `CurrentShoppingListPage.vue`, `MyShoppingPage.vue`, `PurchasedShoppingListPage.vue` - Shopping: `CurrentShoppingListPage.vue`, `MyShoppingPage.vue`, `PurchasedShoppingListPage.vue`, `shopping/MealSelectionList.vue`
- Core/Leaf: `components/ActionItem.vue`, `recipes/RecipeCard.vue`, `ingredients/CompactParsedIngredient.vue`
- Ingredients: `ingredients/IngredientLine.vue`, `ingredients/EditableIngredientsPanel.vue`
Remaining to migrate (Options API or mixed): Remaining to migrate (Options API or mixed):
- Core - Core
- `App.vue` - `App.vue`
- `components/AlertToast.vue` - `components/AlertToast.vue`
- `components/ActionItem.vue`
- `components/LoginPage.vue` - `components/LoginPage.vue`
- Recipes - Recipes
- `components/recipes/RecipesPage.vue` - `components/recipes/RecipesPage.vue`
- `components/recipes/RecipeSearchBox.vue` - `components/recipes/RecipeSearchBox.vue`
- `components/recipes/RecipeCard.vue`
- `components/recipes/EditRecipePage.vue` (most complex) - `components/recipes/EditRecipePage.vue` (most complex)
- Meals - Meals
- `components/meals/MealCard.vue`
- `components/meals/DatePicker.vue` - `components/meals/DatePicker.vue`
- `components/meals/PersonList.vue` (mixed Options+setup, unify under `<script setup>`) - `components/meals/PersonList.vue` (mixed Options+setup, unify under `<script setup>`)
- Ingredients - Ingredients
- `components/ingredients/CompactParsedIngredient.vue`
- `components/ingredients/IngredientLine.vue`
- `components/ingredients/EditableIngredientsPanel.vue` - `components/ingredients/EditableIngredientsPanel.vue`
- Shopping - Shopping
- `components/shopping/MealSelectionList.vue`
- `components/shopping/ShoppingListItem.vue` - `components/shopping/ShoppingListItem.vue`
## Migration order (batches) ## Migration order (batches)
@ -144,20 +140,24 @@ Remaining to migrate (Options API or mixed):
- [ ] Core: `App.vue` - [ ] Core: `App.vue`
- [ ] Core: `components/AlertToast.vue` - [ ] Core: `components/AlertToast.vue`
- [ ] Core: `components/ActionItem.vue` - [x] Core: `components/ActionItem.vue`
- [ ] Core: `components/LoginPage.vue` - [ ] Core: `components/LoginPage.vue`
- [ ] Recipes: `components/recipes/RecipesPage.vue` - [ ] Recipes: `components/recipes/RecipesPage.vue`
- [ ] Recipes: `components/recipes/RecipeSearchBox.vue` - [ ] Recipes: `components/recipes/RecipeSearchBox.vue`
- [ ] Recipes: `components/recipes/RecipeCard.vue` - [x] Recipes: `components/recipes/RecipeCard.vue`
- [ ] Recipes: `components/recipes/EditRecipePage.vue` - [ ] Recipes: `components/recipes/EditRecipePage.vue`
- [ ] Meals: `components/meals/MealCard.vue` - [x] Meals: `components/meals/MealCard.vue`
- [ ] Meals: `components/meals/DatePicker.vue` - [x] Meals: `components/meals/DatePicker.vue`
- [ ] Meals: `components/meals/PersonList.vue` - [x] Meals: `components/meals/PersonList.vue`
- [x] Ingredients: `components/ingredients/CompactParsedIngredient.vue`
- [x] Ingredients: `components/ingredients/IngredientLine.vue`
- [x] Ingredients: `components/ingredients/EditableIngredientsPanel.vue`
- [ ] Shopping: `components/shopping/MealSelectionList.vue` - [x] Shopping: `components/shopping/MealSelectionList.vue`
- [ ] Shopping: `components/shopping/ShoppingListItem.vue` - [ ] Shopping: `components/shopping/ShoppingListItem.vue`
## Notes and risks ## Notes and risks

View file

@ -1,6 +1,6 @@
<template> <template>
<div :class="{ editing: editing }"> <div :class="{ editing: editing }">
<button v-if="editing" @click="$emit('on-add')"> <button v-if="editing" @click="emit('on-add')">
<img class="icon" :src="require('@/assets/add-cart.svg')" /> <br /> <img class="icon" :src="require('@/assets/add-cart.svg')" /> <br />
Add Ingredient Add Ingredient
</button> </button>
@ -24,7 +24,7 @@
@update-product-link="updateProduct" @update-product-link="updateProduct"
/> />
</p> </p>
<button @click="$emit('on-delete', ingredient)"> <button @click="emit('on-delete', ingredient)">
<img class="icon" :src="require('@/assets/trash.svg')" /> <img class="icon" :src="require('@/assets/trash.svg')" />
</button> </button>
</div> </div>
@ -70,33 +70,32 @@ li > div {
} }
</style> </style>
<script> <script setup>
import { ref } from 'vue'
import { parseProduct, parseIngredients } from '@/api/recipes' import { parseProduct, parseIngredients } from '@/api/recipes'
import IngredientLine from './IngredientLine.vue' import IngredientLine from './IngredientLine.vue'
import CompactParsedIngredient from './CompactParsedIngredient.vue' import CompactParsedIngredient from './CompactParsedIngredient.vue'
export default { const props = defineProps({
name: 'EditableIngredientsPanel', ingredients: { type: Array, required: true },
components: { IngredientLine, CompactParsedIngredient }, editOnly: { type: Boolean, default: false },
props: ['ingredients', 'editOnly'], })
data() { const emit = defineEmits(['on-add', 'on-delete', 'on-update-ingredient', 'on-editing'])
return {
editing: this.editOnly ?? false, const editing = ref(props.editOnly ?? false)
}
}, async function updateProduct(ingredient, product_link) {
methods: { const product = await parseProduct(ingredient, product_link)
async updateProduct(ingredient, product_link) { emit('on-update-ingredient', ingredient, { ...ingredient, product })
const product = await parseProduct(ingredient, product_link) }
this.$emit('on-update-ingredient', ingredient, { ...ingredient, product })
}, async function updateIngredient(ingredient, line) {
async updateIngredient(ingredient, line) { const newIngredients = await parseIngredients([line])
const newIngredients = await parseIngredients([line]) emit('on-update-ingredient', ingredient, newIngredients[0])
this.$emit('on-update-ingredient', ingredient, newIngredients[0]) }
},
toggleEditing() { function toggleEditing() {
this.editing = !this.editing editing.value = !editing.value
this.$emit('on-editing', this.editing) emit('on-editing', editing.value)
},
},
} }
</script> </script>

View file

@ -17,56 +17,44 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { import { ref, computed, watch } from 'vue'
props: {
date: {
type: Date,
default: new Date(),
},
},
data() {
return {
selectedDate: this.formatDay(this.date),
showDatePicker: false,
}
},
watch: {
date(newDate) {
this.selectedDate = this.formatDay(newDate)
},
},
computed: {
days() {
const today = new Date()
const days = []
for (let i = 0; i < 15; i++) { const props = defineProps({
const date = new Date(today) date: { type: Date, default: () => new Date() },
date.setDate(today.getDate() + i) })
days.push(date) const emit = defineEmits(['date-selected'])
}
return days function formatDay(date) {
}, const options = { weekday: 'long', day: 'numeric', month: 'numeric' }
}, return date.toLocaleDateString('en-AU', options)
methods: { }
formatDay(date) {
const options = { weekday: 'long', day: 'numeric', month: 'numeric' }
return date.toLocaleDateString('en-AU', options)
},
selectDate(date) {
this.selectedDate = this.formatSelectedDate(date)
this.showDatePicker = false
// Emit custom event const selectedDate = ref(formatDay(props.date))
this.$emit('date-selected', date) const showDatePicker = ref(false)
},
formatSelectedDate(date) { watch(
const options = { weekday: 'long', day: 'numeric', month: 'numeric' } () => props.date,
return date.toLocaleDateString('en-AU', options) (newDate) => {
}, selectedDate.value = formatDay(newDate)
}, }
)
const days = computed(() => {
const today = new Date()
const result = []
for (let i = 0; i < 15; i++) {
const d = new Date(today)
d.setDate(today.getDate() + i)
result.push(d)
}
return result
})
function selectDate(date) {
selectedDate.value = formatDay(date)
showDatePicker.value = false
emit('date-selected', date)
} }
</script> </script>

View file

@ -1,7 +1,7 @@
<template> <template>
<span class="person-list"> <span class="person-list">
<span v-for="person in people" :key="person.id"> <span v-for="person in people" :key="person.id">
<button class="person-circle remove-person" @click="$emit('remove-person', person)"> <button class="person-circle remove-person" @click="removePerson(person)">
{{ person.name }} {{ person.name }}
</button> </button>
</span> </span>
@ -117,71 +117,63 @@
} }
</style> </style>
<script> <script setup>
import { ref } from 'vue' import { ref, watch } from 'vue'
import { searchPerson } from '@/api/persons' import { searchPerson } from '@/api/persons'
export default { const props = defineProps({
name: 'PersonList', people: { type: Array, default: () => [] },
props: { })
people: { const emit = defineEmits(['add-person', 'remove-person'])
type: Array,
default: () => [],
},
},
data() {
return {
isAddingPerson: false,
searchName: '',
searchResults: [],
}
},
setup() {
const searchNameInput = ref(null)
const persondroplist = ref(null)
return { searchNameInput, persondroplist }
},
watch: {
searchName: async function () {
await this.updateSearchResults()
},
searchNameInput: async function () {
this.searchNameInput?.focus()
await this.updateSearchResults()
},
persondroplist: function () {
if (this.persondroplist && this.searchNameInput) {
// Align the droplist to the input field & its size
const inputRect = this.searchNameInput.getBoundingClientRect()
this.persondroplist.style.left = `${inputRect.left}px`
this.persondroplist.style.top = `${inputRect.bottom}px`
this.persondroplist.style.width = `${inputRect.width}px`
}
},
},
methods: {
async updateSearchResults() {
const results = await searchPerson(this.searchName)
// Exclude people already in the list
const idSet = new Set(this.people.map((p) => p.id))
this.searchResults = results.filter((p) => !idSet.has(p.id))
},
addPerson(person) {
if (!person && this.searchResults.length > 0) {
person = this.searchResults[0]
}
if (person?.id >= 0 && !this.people.find((p) => p.id === person.id)) { const isAddingPerson = ref(false)
this.$emit('add-person', person) const searchName = ref('')
} const searchResults = ref([])
this.searchName = '' // Template refs for DOM elements
this.searchResults = [] const searchNameInput = ref(null)
this.isAddingPerson = false const persondroplist = ref(null)
},
removePerson(person) { async function updateSearchResults() {
this.$emit('remove-person', person) const results = await searchPerson(searchName.value)
}, const idSet = new Set(props.people.map((p) => p.id))
}, searchResults.value = results.filter((p) => !idSet.has(p.id))
} }
function addPerson(person) {
if (!person && searchResults.value.length > 0) {
person = searchResults.value[0]
}
if (person?.id >= 0 && !props.people.find((p) => p.id === person.id)) {
emit('add-person', person)
}
searchName.value = ''
searchResults.value = []
isAddingPerson.value = false
}
function removePerson(person) {
emit('remove-person', person)
}
// Watchers
watch(searchName, async () => {
await updateSearchResults()
})
watch(searchNameInput, async (el) => {
if (el) {
el.focus()
await updateSearchResults()
}
})
watch([persondroplist, searchNameInput], ([drop, input]) => {
if (drop && input) {
const inputRect = input.getBoundingClientRect()
drop.style.left = `${inputRect.left}px`
drop.style.top = `${inputRect.bottom}px`
drop.style.width = `${inputRect.width}px`
}
})
</script> </script>