alert to composable
This commit is contained in:
parent
d1534934c0
commit
72fccdd8a3
6 changed files with 57 additions and 40 deletions
|
|
@ -27,11 +27,11 @@ Only outstanding, actionable steps are listed below. Completed work has been rem
|
||||||
|
|
||||||
Acceptance: ESLint upgrade plan decided (or implemented), rules apply cleanly, lint passes.
|
Acceptance: ESLint upgrade plan decided (or implemented), rules apply cleanly, lint passes.
|
||||||
|
|
||||||
## 2) Alerts as a composable (nice-to-have)
|
## 2) Alerts as a composable
|
||||||
- Replace the simple event bus in `src/alert.js` with a `useAlert` composable (reactive queue API) and adapt `AlertToast.vue`.
|
- DONE: Replaced the event bus with `useAlert` composable and updated `AlertToast.vue` and callers.
|
||||||
- Provide show({ heading, message, type }) and auto-dismiss with clear-on-click.
|
- Follow-up: consider queuing multiple alerts if needed (current behavior shows latest only).
|
||||||
|
|
||||||
Acceptance: AlertToast driven by composable; no global mutable arrays; behavior unchanged.
|
Acceptance: N/A (completed). Optional enhancement if queuing desired.
|
||||||
|
|
||||||
## 3) Incremental test coverage
|
## 3) Incremental test coverage
|
||||||
- Add unit tests for new utilities/composables when added (e.g., `useAlert`).
|
- Add unit tests for new utilities/composables when added (e.g., `useAlert`).
|
||||||
|
|
@ -44,10 +44,3 @@ Acceptance: New logic lands with tests; existing tests stay green.
|
||||||
- Add Volar as a recommended extension in the project docs (README updated).
|
- Add Volar as a recommended extension in the project docs (README updated).
|
||||||
|
|
||||||
Acceptance: Clear env setup; editor help consistent.
|
Acceptance: Clear env setup; editor help consistent.
|
||||||
|
|
||||||
## 5) Optional: Migrate from Vue CLI to Vite
|
|
||||||
- If desired, migrate build tooling to Vite for faster dev server and simpler config.
|
|
||||||
- Update scripts, configure vitest (already in place), and resolve aliasing.
|
|
||||||
|
|
||||||
Acceptance: Dev/build parity maintained; cold/hot start noticeably faster.
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,8 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
import alert from '@/alert'
|
import { useAlert } from '@/composables/useAlert'
|
||||||
|
|
||||||
const alertIcons = {
|
const alertIcons = {
|
||||||
error: require('@/assets/notification-error.svg'),
|
error: require('@/assets/notification-error.svg'),
|
||||||
|
|
@ -57,28 +57,22 @@ const alertIcons = {
|
||||||
info: require('@/assets/notification-info.svg'),
|
info: require('@/assets/notification-info.svg'),
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAlert = ref(false)
|
const { current, clear, scheduleAutoDismiss } = useAlert()
|
||||||
const heading = ref('')
|
|
||||||
const message = ref('')
|
|
||||||
const type = ref('')
|
|
||||||
|
|
||||||
|
const showAlert = computed(() => !!current.value)
|
||||||
|
const heading = computed(() => current.value?.heading ?? '')
|
||||||
|
const message = computed(() => current.value?.message ?? '')
|
||||||
|
const type = computed(() => current.value?.type ?? '')
|
||||||
const icon = computed(() => (type.value && alertIcons[type.value] ? alertIcons[type.value] : null))
|
const icon = computed(() => (type.value && alertIcons[type.value] ? alertIcons[type.value] : null))
|
||||||
|
|
||||||
function show({ heading: h, message: m, type: t }) {
|
|
||||||
heading.value = h
|
|
||||||
message.value = m
|
|
||||||
type.value = t
|
|
||||||
showAlert.value = true
|
|
||||||
setTimeout(() => {
|
|
||||||
showAlert.value = false
|
|
||||||
}, 5000)
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismiss() {
|
function dismiss() {
|
||||||
showAlert.value = false
|
clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
watch(
|
||||||
alert.subscribe(show)
|
() => current.value?._ts,
|
||||||
})
|
(ts) => {
|
||||||
|
if (ts) scheduleAutoDismiss(5000)
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||||
import { getMeal, saveMeal } from '@/composables/useMeals'
|
import { getMeal, saveMeal } from '@/composables/useMeals'
|
||||||
import { getRecipe } from '@/api/recipes'
|
import { getRecipe } from '@/api/recipes'
|
||||||
import { currentUser } from '@/api/auth'
|
import { currentUser } from '@/api/auth'
|
||||||
import alert from '@/alert.js'
|
import { useAlert } from '@/composables/useAlert'
|
||||||
|
|
||||||
import { ago } from '@/dateformats.js'
|
import { ago } from '@/dateformats.js'
|
||||||
|
|
||||||
|
|
@ -116,6 +116,7 @@ function addPersonIfNotExists(list, person) {
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { show: showAlert } = useAlert()
|
||||||
|
|
||||||
const showIngredients = reactive({})
|
const showIngredients = reactive({})
|
||||||
const meal = reactive({
|
const meal = reactive({
|
||||||
|
|
@ -221,11 +222,11 @@ async function onSaveMeal() {
|
||||||
if (saved?.id >= 0) {
|
if (saved?.id >= 0) {
|
||||||
Object.assign(meal, saved)
|
Object.assign(meal, saved)
|
||||||
router.push(`/meals/${saved.id}`)
|
router.push(`/meals/${saved.id}`)
|
||||||
alert.show({ heading: 'Meal saved', message: 'Meal saved successfully', type: 'success' })
|
showAlert({ heading: 'Meal saved', message: 'Meal saved successfully', type: 'success' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
alert.show({
|
showAlert({
|
||||||
heading: 'Error saving meal',
|
heading: 'Error saving meal',
|
||||||
message: 'An error occurred while saving the meal',
|
message: 'An error occurred while saving the meal',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ input.recipe-name {
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import alert from '@/alert.js'
|
import { useAlert } from '@/composables/useAlert'
|
||||||
import { getRecipe, parseRecipe, saveRecipe as saveRecipeApi, 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'
|
||||||
|
|
||||||
|
|
@ -76,6 +76,7 @@ const props = defineProps({
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const { show: showAlert } = useAlert()
|
||||||
|
|
||||||
const link = ref(route.query.url ?? '')
|
const link = ref(route.query.url ?? '')
|
||||||
const parse_failed = ref(false)
|
const parse_failed = ref(false)
|
||||||
|
|
@ -122,11 +123,11 @@ function deleteIngredient(ingredient) {
|
||||||
async function saveRecipe() {
|
async function saveRecipe() {
|
||||||
const saved = await saveRecipeApi(recipe.value)
|
const saved = await saveRecipeApi(recipe.value)
|
||||||
if (saved?.id >= 0) {
|
if (saved?.id >= 0) {
|
||||||
alert.show({ heading: 'Recipe saved', message: 'Your recipe has been saved', type: 'success' })
|
showAlert({ heading: 'Recipe saved', message: 'Your recipe has been saved', type: 'success' })
|
||||||
router.push(`/recipes/${saved.id}`)
|
router.push(`/recipes/${saved.id}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
alert.show({ heading: 'Error saving recipe', message: 'There was an error saving your recipe', type: 'error' })
|
showAlert({ heading: 'Error saving recipe', message: 'There was an error saving your recipe', type: 'error' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFromScratch() {
|
function createFromScratch() {
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ button img {
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onBeforeMount } from 'vue'
|
import { ref, computed, onBeforeMount } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import alert from '@/alert.js'
|
import { useAlert } from '@/composables/useAlert'
|
||||||
import { useShopping } from '@/composables/useShopping'
|
import { useShopping } from '@/composables/useShopping'
|
||||||
import { getUpcomingMeals } from '@/api/meals'
|
import { getUpcomingMeals } from '@/api/meals'
|
||||||
import { itemsToGroups, uniqueMeals } from './shopping.js'
|
import { itemsToGroups, uniqueMeals } from './shopping.js'
|
||||||
|
|
@ -150,6 +150,7 @@ import MealSelectionList from './MealSelectionList.vue'
|
||||||
import ShoppingListItem from './ShoppingListItem.vue'
|
import ShoppingListItem from './ShoppingListItem.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { show: showAlert } = useAlert()
|
||||||
const { getCurrentShoppingList, requestMeal, unrequestMeal, purchaseFromGroups } = useShopping()
|
const { getCurrentShoppingList, requestMeal, unrequestMeal, purchaseFromGroups } = useShopping()
|
||||||
|
|
||||||
const from = new Date()
|
const from = new Date()
|
||||||
|
|
@ -202,7 +203,7 @@ async function mealUnselected(meal) {
|
||||||
async function markFound() {
|
async function markFound() {
|
||||||
const result = await purchaseFromGroups(selected.value)
|
const result = await purchaseFromGroups(selected.value)
|
||||||
if (!result) {
|
if (!result) {
|
||||||
alert.show({ type: 'error', message: 'No items selected.' })
|
showAlert({ type: 'error', message: 'No items selected.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selected.value = []
|
selected.value = []
|
||||||
|
|
@ -212,7 +213,7 @@ async function markFound() {
|
||||||
async function markPurchased() {
|
async function markPurchased() {
|
||||||
const shopping = await purchaseFromGroups(selected.value)
|
const shopping = await purchaseFromGroups(selected.value)
|
||||||
if (!shopping || !shopping.id) {
|
if (!shopping || !shopping.id) {
|
||||||
alert.show({ type: 'error', message: 'Failed to purchase.' })
|
showAlert({ type: 'error', message: 'Failed to purchase.' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selected.value = []
|
selected.value = []
|
||||||
|
|
|
||||||
27
src/composables/useAlert.js
Normal file
27
src/composables/useAlert.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// Singleton reactive alert state for the app
|
||||||
|
const current = ref(null)
|
||||||
|
let timeoutId = null
|
||||||
|
|
||||||
|
function show(message) {
|
||||||
|
// message: { heading, message, type: 'success' | 'error' | 'info' }
|
||||||
|
current.value = { ...message, _ts: Date.now() }
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
current.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleAutoDismiss(ms = 5000) {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId)
|
||||||
|
if (!current.value) return
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
clear()
|
||||||
|
timeoutId = null
|
||||||
|
}, ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAlert() {
|
||||||
|
return { current, show, clear, scheduleAutoDismiss }
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue