Composition #4

This commit is contained in:
jableader 2025-10-18 13:57:40 +11:00
parent d31182dff0
commit 68e82f1fa2
5 changed files with 159 additions and 174 deletions

View file

@ -76,16 +76,17 @@ Already using `<script setup>`:
- 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` - Recipes: `components/recipes/RecipesPage.vue`, `components/recipes/RecipeSearchBox.vue`
- Recipes: `components/recipes/RecipesPage.vue`, `components/recipes/RecipeSearchBox.vue`, `components/recipes/EditRecipePage.vue`
Remaining to migrate (Options API or mixed): Remaining to migrate (Options API or mixed):
- Core - Core
- `App.vue` -
- `components/AlertToast.vue` -
- `components/LoginPage.vue` -
- Recipes - Recipes
- `components/recipes/EditRecipePage.vue` (most complex) -
- Meals - Meals
- `components/meals/DatePicker.vue` - `components/meals/DatePicker.vue`
@ -137,15 +138,15 @@ Remaining to migrate (Options API or mixed):
## Tracking checklist ## Tracking checklist
- [ ] Core: `App.vue` - [x] Core: `App.vue`
- [ ] Core: `components/AlertToast.vue` - [x] Core: `components/AlertToast.vue`
- [x] Core: `components/ActionItem.vue` - [x] Core: `components/ActionItem.vue`
- [ ] Core: `components/LoginPage.vue` - [x] Core: `components/LoginPage.vue`
- [x] Recipes: `components/recipes/RecipesPage.vue` - [x] Recipes: `components/recipes/RecipesPage.vue`
- [x] 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` - [x] Recipes: `components/recipes/EditRecipePage.vue`
- [x] Meals: `components/meals/MealCard.vue` - [x] Meals: `components/meals/MealCard.vue`
- [x] Meals: `components/meals/DatePicker.vue` - [x] Meals: `components/meals/DatePicker.vue`
@ -159,6 +160,8 @@ Remaining to migrate (Options API or mixed):
- [x] Shopping: `components/shopping/MealSelectionList.vue` - [x] Shopping: `components/shopping/MealSelectionList.vue`
- [x] Shopping: `components/shopping/ShoppingListItem.vue` - [x] Shopping: `components/shopping/ShoppingListItem.vue`
All components are now migrated to `<script setup>`.
## Notes and risks ## Notes and risks
- `PersonList.vue` aligns a dropdown to an input via DOM measurements; ensure the ref-based approach updates positions correctly on focus/resize. - `PersonList.vue` aligns a dropdown to an input via DOM measurements; ensure the ref-based approach updates positions correctly on focus/resize.

View file

@ -25,20 +25,10 @@
<alert-toast /> <alert-toast />
</template> </template>
<script> <script setup>
import AlertToast from './components/AlertToast.vue' import AlertToast from './components/AlertToast.vue'
export default { // components in <script setup> are auto-registered by import + usage
name: 'App',
components: {
'alert-toast': AlertToast,
},
computed: {
currentRoute() {
return this.$route.path
},
},
}
</script> </script>
<style> <style>

View file

@ -47,7 +47,8 @@
} }
</style> </style>
<script> <script setup>
import { ref, computed, onMounted } from 'vue'
import alert from '@/alert' import alert from '@/alert'
const alertIcons = { const alertIcons = {
@ -56,37 +57,28 @@ const alertIcons = {
info: require('@/assets/notification-info.svg'), info: require('@/assets/notification-info.svg'),
} }
export default { const showAlert = ref(false)
name: 'AlertToast', const heading = ref('')
data() { const message = ref('')
return { const type = ref('')
showAlert: false,
heading: '', const icon = computed(() => (type.value && alertIcons[type.value] ? alertIcons[type.value] : null))
message: '',
type: '', function show({ heading: h, message: m, type: t }) {
} heading.value = h
}, message.value = m
computed: { type.value = t
icon() { showAlert.value = true
return this.type && alertIcons[this.type] ? alertIcons[this.type] : null setTimeout(() => {
}, showAlert.value = false
}, }, 5000)
mounted() {
alert.subscribe(this.show)
},
methods: {
show({ heading, message, type }) {
this.heading = heading
this.message = message
this.type = type
this.showAlert = true
setTimeout(() => {
this.showAlert = false
}, 5000)
},
dismiss() {
this.showAlert = false
},
},
} }
function dismiss() {
showAlert.value = false
}
onMounted(() => {
alert.subscribe(show)
})
</script> </script>

View file

@ -3,7 +3,7 @@
<h1>Login Page</h1> <h1>Login Page</h1>
<ul class="button-group"> <ul class="button-group">
<li v-for="person in persons" :key="person.id"> <li v-for="person in persons" :key="person.id">
<button type="button" class="btn btn-primary" @click="login(person)"> <button type="button" class="btn btn-primary" @click="onLogin(person)">
{{ person.name }} {{ person.name }}
</button> </button>
</li> </li>
@ -67,36 +67,29 @@ li:nth-child(4) > button {
} }
</style> </style>
<script> <script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getPersonsInHome } from '@/api/persons' import { getPersonsInHome } from '@/api/persons'
import { login } from '@/api/auth' import { login as loginApi } from '@/api/auth'
export default { const props = defineProps({
name: 'LoginVue', redirect: { type: String, default: '/' },
props: { })
redirect: {
type: String,
default: '/',
},
},
data() {
return {
persons: [],
}
},
async beforeMount() {
this.persons = await getPersonsInHome()
},
methods: {
async login(selectedPerson) {
const person = await login(selectedPerson.name)
if (person?.id >= 0) {
this.$router.push(this.redirect)
return
}
alert('Login failed') const router = useRouter()
}, const persons = ref([])
},
onMounted(async () => {
persons.value = await getPersonsInHome()
})
async function onLogin(selectedPerson) {
const person = await loginApi(selectedPerson.name)
if (person?.id >= 0) {
router.push(props.redirect)
return
}
alert('Login failed')
} }
</script> </script>

View file

@ -63,101 +63,108 @@ input.recipe-name {
} }
</style> </style>
<script> <script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import alert from '@/alert.js' import alert from '@/alert.js'
import { getRecipe, parseRecipe, saveRecipe, deleteRecipe as deleteRecipeApi } from '@/api/recipes' import { getRecipe, parseRecipe, saveRecipe as saveRecipeApi, deleteRecipe as deleteRecipeApi } from '@/api/recipes'
import EditableIngredientsPanel from '@/components/ingredients/EditableIngredientsPanel.vue' import EditableIngredientsPanel from '@/components/ingredients/EditableIngredientsPanel.vue'
export default { const props = defineProps({
props: { id: { type: Number, required: false },
id: { type: Number, optional: true }, })
},
components: { EditableIngredientsPanel },
data() {
return {
link: this.$route.query.url ?? '',
parse_failed: false,
recipe: null,
chefs: [],
}
},
mounted() {
this.refreshRecipe()
},
computed: {
image_styling() {
if (this.recipe?.image_urls && this.recipe.image_urls[0]) {
const image = this.recipe.image_urls[0]
// linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0) 100%), url('https://i2.wp.com/www.downshiftology.com/wp-content/uploads/2019/04/steamed-broccoli-4.jpg') center/cover no-repeat;
return {
background: `linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0) 100%), url('${image}') center/cover no-repeat`,
}
}
return null
},
},
methods: {
parseLink() {
this.$router.push({ path: '/recipes/add', query: { url: this.link } })
this.refreshRecipe()
},
async refreshRecipe() {
if (this.id >= 0) {
this.recipe = await getRecipe(this.id)
this.link = this.recipe.link
return
} else if (this.link) {
this.recipe = await parseRecipe(this.link)
this.parse_failed = !this.recipe
} else {
this.recipe = null
}
},
async updateIngredient(ingredient, newIngredient) {
this.recipe.ingredients = this.recipe.ingredients.map((i) =>
i == ingredient ? newIngredient : i
)
},
deleteIngredient(ingredient) {
this.recipe.ingredients = this.recipe.ingredients.filter((i) => i != ingredient)
},
async saveRecipe() {
const recipe = await saveRecipe(this.recipe)
if (recipe?.id >= 0) {
alert.show({
heading: 'Recipe saved',
message: 'Your recipe has been saved',
type: 'success',
})
this.$router.push(`/recipes/${recipe.id}`)
return
}
alert.show({ const router = useRouter()
heading: 'Error saving recipe', const route = useRoute()
message: 'There was an error saving your recipe',
type: 'error', const link = ref(route.query.url ?? '')
}) const parse_failed = ref(false)
}, const recipe = ref(null)
async createFromScratch() {
this.recipe = { const image_styling = computed(() => {
id: -1, if (recipe.value?.image_urls && recipe.value.image_urls[0]) {
name: 'My new recipe', const image = recipe.value.image_urls[0]
created_by_id: -1, return {
link: '', background: `linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0) 100%), url('${image}') center/cover no-repeat`,
ingredients: [], }
image_urls: [], }
} return null
}, })
addIngredient() {
this.recipe.ingredients = [{ line: '', product: null }, ...this.recipe.ingredients] function parseLink() {
}, router.push({ path: '/recipes/add', query: { url: link.value } })
async deleteRecipe() { refreshRecipe()
if (confirm('Are you sure you want to delete this recipe?')) {
await deleteRecipeApi(this.recipe.id)
this.$router.push('/recipes')
}
},
},
} }
async function refreshRecipe() {
if (props.id >= 0) {
recipe.value = await getRecipe(props.id)
link.value = recipe.value.link
return
} else if (link.value) {
recipe.value = await parseRecipe(link.value)
parse_failed.value = !recipe.value
} else {
recipe.value = null
}
}
async function updateIngredient(ingredient, newIngredient) {
recipe.value.ingredients = recipe.value.ingredients.map((i) =>
i == ingredient ? newIngredient : i
)
}
function deleteIngredient(ingredient) {
recipe.value.ingredients = recipe.value.ingredients.filter((i) => i != ingredient)
}
async function saveRecipe() {
const saved = await saveRecipeApi(recipe.value)
if (saved?.id >= 0) {
alert.show({ heading: 'Recipe saved', message: 'Your recipe has been saved', type: 'success' })
router.push(`/recipes/${saved.id}`)
return
}
alert.show({ heading: 'Error saving recipe', message: 'There was an error saving your recipe', type: 'error' })
}
function createFromScratch() {
recipe.value = {
id: -1,
name: 'My new recipe',
created_by_id: -1,
link: '',
ingredients: [],
image_urls: [],
}
}
function addIngredient() {
recipe.value.ingredients = [{ line: '', product: null }, ...recipe.value.ingredients]
}
async function deleteRecipe() {
if (confirm('Are you sure you want to delete this recipe?')) {
await deleteRecipeApi(recipe.value.id)
router.push('/recipes')
}
}
onMounted(() => {
refreshRecipe()
})
// Keep recipe in sync if link query changes while on page
watch(
() => route.query.url,
(newUrl) => {
if (typeof newUrl === 'string') {
link.value = newUrl
refreshRecipe()
}
}
)
// expose functions for template binding names (automatic in <script setup>)
</script> </script>