Composition #4
This commit is contained in:
parent
d31182dff0
commit
68e82f1fa2
5 changed files with 159 additions and 174 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
14
src/App.vue
14
src/App.vue
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
alert.subscribe(this.show)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
show({ heading, message, type }) {
|
|
||||||
this.heading = heading
|
|
||||||
this.message = message
|
|
||||||
this.type = type
|
|
||||||
this.showAlert = true
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.showAlert = false
|
showAlert.value = false
|
||||||
}, 5000)
|
}, 5000)
|
||||||
},
|
|
||||||
dismiss() {
|
|
||||||
this.showAlert = false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
showAlert.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
alert.subscribe(show)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
const router = useRouter()
|
||||||
default: '/',
|
const persons = ref([])
|
||||||
},
|
|
||||||
},
|
onMounted(async () => {
|
||||||
data() {
|
persons.value = await getPersonsInHome()
|
||||||
return {
|
})
|
||||||
persons: [],
|
|
||||||
}
|
async function onLogin(selectedPerson) {
|
||||||
},
|
const person = await loginApi(selectedPerson.name)
|
||||||
async beforeMount() {
|
|
||||||
this.persons = await getPersonsInHome()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async login(selectedPerson) {
|
|
||||||
const person = await login(selectedPerson.name)
|
|
||||||
if (person?.id >= 0) {
|
if (person?.id >= 0) {
|
||||||
this.$router.push(this.redirect)
|
router.push(props.redirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
alert('Login failed')
|
alert('Login failed')
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -63,84 +63,74 @@ 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 },
|
const router = useRouter()
|
||||||
data() {
|
const route = useRoute()
|
||||||
return {
|
|
||||||
link: this.$route.query.url ?? '',
|
const link = ref(route.query.url ?? '')
|
||||||
parse_failed: false,
|
const parse_failed = ref(false)
|
||||||
recipe: null,
|
const recipe = ref(null)
|
||||||
chefs: [],
|
|
||||||
}
|
const image_styling = computed(() => {
|
||||||
},
|
if (recipe.value?.image_urls && recipe.value.image_urls[0]) {
|
||||||
mounted() {
|
const image = recipe.value.image_urls[0]
|
||||||
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 {
|
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`,
|
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
|
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
|
function parseLink() {
|
||||||
|
router.push({ path: '/recipes/add', query: { url: link.value } })
|
||||||
|
refreshRecipe()
|
||||||
}
|
}
|
||||||
|
|
||||||
alert.show({
|
async function refreshRecipe() {
|
||||||
heading: 'Error saving recipe',
|
if (props.id >= 0) {
|
||||||
message: 'There was an error saving your recipe',
|
recipe.value = await getRecipe(props.id)
|
||||||
type: 'error',
|
link.value = recipe.value.link
|
||||||
})
|
return
|
||||||
},
|
} else if (link.value) {
|
||||||
async createFromScratch() {
|
recipe.value = await parseRecipe(link.value)
|
||||||
this.recipe = {
|
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,
|
id: -1,
|
||||||
name: 'My new recipe',
|
name: 'My new recipe',
|
||||||
created_by_id: -1,
|
created_by_id: -1,
|
||||||
|
|
@ -148,16 +138,33 @@ export default {
|
||||||
ingredients: [],
|
ingredients: [],
|
||||||
image_urls: [],
|
image_urls: [],
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
addIngredient() {
|
|
||||||
this.recipe.ingredients = [{ line: '', product: null }, ...this.recipe.ingredients]
|
function addIngredient() {
|
||||||
},
|
recipe.value.ingredients = [{ line: '', product: null }, ...recipe.value.ingredients]
|
||||||
async deleteRecipe() {
|
}
|
||||||
|
|
||||||
|
async function deleteRecipe() {
|
||||||
if (confirm('Are you sure you want to delete this recipe?')) {
|
if (confirm('Are you sure you want to delete this recipe?')) {
|
||||||
await deleteRecipeApi(this.recipe.id)
|
await deleteRecipeApi(recipe.value.id)
|
||||||
this.$router.push('/recipes')
|
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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue