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.
|
||||
|
||||
## 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
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