Composition #2
This commit is contained in:
parent
61ca41d25a
commit
beab02200c
4 changed files with 129 additions and 150 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
|
||||||
async updateProduct(ingredient, product_link) {
|
|
||||||
const product = await parseProduct(ingredient, product_link)
|
const product = await parseProduct(ingredient, product_link)
|
||||||
this.$emit('on-update-ingredient', ingredient, { ...ingredient, product })
|
emit('on-update-ingredient', ingredient, { ...ingredient, product })
|
||||||
},
|
}
|
||||||
async updateIngredient(ingredient, line) {
|
|
||||||
|
async function updateIngredient(ingredient, line) {
|
||||||
const newIngredients = await parseIngredients([line])
|
const newIngredients = await parseIngredients([line])
|
||||||
this.$emit('on-update-ingredient', ingredient, newIngredients[0])
|
emit('on-update-ingredient', ingredient, newIngredients[0])
|
||||||
},
|
}
|
||||||
toggleEditing() {
|
|
||||||
this.editing = !this.editing
|
function toggleEditing() {
|
||||||
this.$emit('on-editing', this.editing)
|
editing.value = !editing.value
|
||||||
},
|
emit('on-editing', editing.value)
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -17,56 +17,44 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref, computed, watch } from 'vue'
|
||||||
props: {
|
|
||||||
date: {
|
const props = defineProps({
|
||||||
type: Date,
|
date: { type: Date, default: () => new Date() },
|
||||||
default: new Date(),
|
})
|
||||||
},
|
const emit = defineEmits(['date-selected'])
|
||||||
},
|
|
||||||
data() {
|
function formatDay(date) {
|
||||||
return {
|
const options = { weekday: 'long', day: 'numeric', month: 'numeric' }
|
||||||
selectedDate: this.formatDay(this.date),
|
return date.toLocaleDateString('en-AU', options)
|
||||||
showDatePicker: false,
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
watch: {
|
const selectedDate = ref(formatDay(props.date))
|
||||||
date(newDate) {
|
const showDatePicker = ref(false)
|
||||||
this.selectedDate = this.formatDay(newDate)
|
|
||||||
},
|
watch(
|
||||||
},
|
() => props.date,
|
||||||
computed: {
|
(newDate) => {
|
||||||
days() {
|
selectedDate.value = formatDay(newDate)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const days = computed(() => {
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
const days = []
|
const result = []
|
||||||
|
|
||||||
for (let i = 0; i < 15; i++) {
|
for (let i = 0; i < 15; i++) {
|
||||||
const date = new Date(today)
|
const d = new Date(today)
|
||||||
date.setDate(today.getDate() + i)
|
d.setDate(today.getDate() + i)
|
||||||
days.push(date)
|
result.push(d)
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
return days
|
function selectDate(date) {
|
||||||
},
|
selectedDate.value = formatDay(date)
|
||||||
},
|
showDatePicker.value = false
|
||||||
methods: {
|
emit('date-selected', date)
|
||||||
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)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: () => [],
|
const isAddingPerson = ref(false)
|
||||||
},
|
const searchName = ref('')
|
||||||
},
|
const searchResults = ref([])
|
||||||
data() {
|
|
||||||
return {
|
// Template refs for DOM elements
|
||||||
isAddingPerson: false,
|
|
||||||
searchName: '',
|
|
||||||
searchResults: [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const searchNameInput = ref(null)
|
const searchNameInput = ref(null)
|
||||||
const persondroplist = ref(null)
|
const persondroplist = ref(null)
|
||||||
return { searchNameInput, persondroplist }
|
|
||||||
},
|
async function updateSearchResults() {
|
||||||
watch: {
|
const results = await searchPerson(searchName.value)
|
||||||
searchName: async function () {
|
const idSet = new Set(props.people.map((p) => p.id))
|
||||||
await this.updateSearchResults()
|
searchResults.value = results.filter((p) => !idSet.has(p.id))
|
||||||
},
|
|
||||||
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)) {
|
function addPerson(person) {
|
||||||
this.$emit('add-person', 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 = ''
|
function removePerson(person) {
|
||||||
this.searchResults = []
|
emit('remove-person', person)
|
||||||
this.isAddingPerson = false
|
|
||||||
},
|
|
||||||
removePerson(person) {
|
|
||||||
this.$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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue