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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<template>
<span class="person-list">
<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 }}
</button>
</span>
@ -117,71 +117,63 @@
}
</style>
<script>
import { ref } from 'vue'
<script setup>
import { ref, watch } from 'vue'
import { searchPerson } from '@/api/persons'
export default {
name: 'PersonList',
props: {
people: {
type: Array,
default: () => [],
},
},
data() {
return {
isAddingPerson: false,
searchName: '',
searchResults: [],
}
},
setup() {
const props = defineProps({
people: { type: Array, default: () => [] },
})
const emit = defineEmits(['add-person', 'remove-person'])
const isAddingPerson = ref(false)
const searchName = ref('')
const searchResults = ref([])
// Template refs for DOM elements
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]
async function updateSearchResults() {
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))
}
if (person?.id >= 0 && !this.people.find((p) => p.id === person.id)) {
this.$emit('add-person', person)
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
}
this.searchName = ''
this.searchResults = []
this.isAddingPerson = false
},
removePerson(person) {
this.$emit('remove-person', person)
},
},
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>