alert to composable

This commit is contained in:
jableader 2025-10-18 14:12:27 +11:00
parent d1534934c0
commit 72fccdd8a3
6 changed files with 57 additions and 40 deletions

View file

@ -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.
## 2) Alerts as a composable (nice-to-have)
- Replace the simple event bus in `src/alert.js` with a `useAlert` composable (reactive queue API) and adapt `AlertToast.vue`.
- Provide show({ heading, message, type }) and auto-dismiss with clear-on-click.
## 2) Alerts as a composable
- DONE: Replaced the event bus with `useAlert` composable and updated `AlertToast.vue` and callers.
- 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
- 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).
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.

View file

@ -48,8 +48,8 @@
</style>
<script setup>
import { ref, computed, onMounted } from 'vue'
import alert from '@/alert'
import { computed, watch } from 'vue'
import { useAlert } from '@/composables/useAlert'
const alertIcons = {
error: require('@/assets/notification-error.svg'),
@ -57,28 +57,22 @@ const alertIcons = {
info: require('@/assets/notification-info.svg'),
}
const showAlert = ref(false)
const heading = ref('')
const message = ref('')
const type = ref('')
const { current, clear, scheduleAutoDismiss } = useAlert()
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))
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() {
showAlert.value = false
clear()
}
onMounted(() => {
alert.subscribe(show)
})
watch(
() => current.value?._ts,
(ts) => {
if (ts) scheduleAutoDismiss(5000)
}
)
</script>

View file

@ -97,7 +97,7 @@ import { useRoute, useRouter } from 'vue-router'
import { getMeal, saveMeal } from '@/composables/useMeals'
import { getRecipe } from '@/api/recipes'
import { currentUser } from '@/api/auth'
import alert from '@/alert.js'
import { useAlert } from '@/composables/useAlert'
import { ago } from '@/dateformats.js'
@ -116,6 +116,7 @@ function addPersonIfNotExists(list, person) {
const route = useRoute()
const router = useRouter()
const { show: showAlert } = useAlert()
const showIngredients = reactive({})
const meal = reactive({
@ -221,11 +222,11 @@ async function onSaveMeal() {
if (saved?.id >= 0) {
Object.assign(meal, saved)
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
}
alert.show({
showAlert({
heading: 'Error saving meal',
message: 'An error occurred while saving the meal',
type: 'error',

View file

@ -66,7 +66,7 @@ input.recipe-name {
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
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 EditableIngredientsPanel from '@/components/ingredients/EditableIngredientsPanel.vue'
@ -76,6 +76,7 @@ const props = defineProps({
const router = useRouter()
const route = useRoute()
const { show: showAlert } = useAlert()
const link = ref(route.query.url ?? '')
const parse_failed = ref(false)
@ -122,11 +123,11 @@ function deleteIngredient(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' })
showAlert({ 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' })
showAlert({ heading: 'Error saving recipe', message: 'There was an error saving your recipe', type: 'error' })
}
function createFromScratch() {

View file

@ -142,7 +142,7 @@ button img {
<script setup>
import { ref, computed, onBeforeMount } from 'vue'
import { useRouter } from 'vue-router'
import alert from '@/alert.js'
import { useAlert } from '@/composables/useAlert'
import { useShopping } from '@/composables/useShopping'
import { getUpcomingMeals } from '@/api/meals'
import { itemsToGroups, uniqueMeals } from './shopping.js'
@ -150,6 +150,7 @@ import MealSelectionList from './MealSelectionList.vue'
import ShoppingListItem from './ShoppingListItem.vue'
const router = useRouter()
const { show: showAlert } = useAlert()
const { getCurrentShoppingList, requestMeal, unrequestMeal, purchaseFromGroups } = useShopping()
const from = new Date()
@ -202,7 +203,7 @@ async function mealUnselected(meal) {
async function markFound() {
const result = await purchaseFromGroups(selected.value)
if (!result) {
alert.show({ type: 'error', message: 'No items selected.' })
showAlert({ type: 'error', message: 'No items selected.' })
return
}
selected.value = []
@ -212,7 +213,7 @@ async function markFound() {
async function markPurchased() {
const shopping = await purchaseFromGroups(selected.value)
if (!shopping || !shopping.id) {
alert.show({ type: 'error', message: 'Failed to purchase.' })
showAlert({ type: 'error', message: 'Failed to purchase.' })
return
}
selected.value = []

View 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 }
}