119 lines
2.9 KiB
Vue
119 lines
2.9 KiB
Vue
<script setup>
|
|
import { ref, watch, onUnmounted } from 'vue'
|
|
import { usePage } from '@inertiajs/vue3'
|
|
import { CheckCircleIcon, ExclamationTriangleIcon, XMarkIcon } from '@heroicons/vue/20/solid'
|
|
|
|
const page = usePage()
|
|
|
|
const visible = ref(false)
|
|
const message = ref('')
|
|
const type = ref('success') // 'success' | 'error'
|
|
|
|
let autoDismissTimer = null
|
|
|
|
function clearTimer() {
|
|
if (autoDismissTimer) {
|
|
clearTimeout(autoDismissTimer)
|
|
autoDismissTimer = null
|
|
}
|
|
}
|
|
|
|
function dismiss() {
|
|
visible.value = false
|
|
clearTimer()
|
|
}
|
|
|
|
function showFlash(flash) {
|
|
clearTimer()
|
|
|
|
if (flash?.error) {
|
|
message.value = flash.error
|
|
type.value = 'error'
|
|
visible.value = true
|
|
// Error messages stay until manually dismissed
|
|
} else if (flash?.success) {
|
|
message.value = flash.success
|
|
type.value = 'success'
|
|
visible.value = true
|
|
// Auto-dismiss success after 8 seconds
|
|
autoDismissTimer = setTimeout(() => {
|
|
visible.value = false
|
|
}, 8000)
|
|
} else {
|
|
visible.value = false
|
|
}
|
|
}
|
|
|
|
// Watch for flash prop changes on each Inertia page visit
|
|
watch(
|
|
() => page.props.flash,
|
|
(flash) => showFlash(flash),
|
|
{ immediate: true, deep: true }
|
|
)
|
|
|
|
onUnmounted(() => {
|
|
clearTimer()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<Transition
|
|
enter-active-class="transition duration-300 ease-out"
|
|
enter-from-class="-translate-y-full opacity-0"
|
|
enter-to-class="translate-y-0 opacity-100"
|
|
leave-active-class="transition duration-200 ease-in"
|
|
leave-from-class="translate-y-0 opacity-100"
|
|
leave-to-class="-translate-y-full opacity-0"
|
|
>
|
|
<div
|
|
v-if="visible"
|
|
:class="[
|
|
'relative z-40 w-full shadow-lg',
|
|
type === 'error'
|
|
? 'bg-rose-600'
|
|
: 'bg-emerald-600',
|
|
]"
|
|
role="alert"
|
|
:aria-live="type === 'error' ? 'assertive' : 'polite'"
|
|
>
|
|
<div class="px-6 py-3 flex items-center gap-3 max-w-7xl mx-auto">
|
|
<!-- Icon -->
|
|
<component
|
|
:is="type === 'error' ? ExclamationTriangleIcon : CheckCircleIcon"
|
|
class="w-5 h-5 text-white/90 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
|
|
<!-- Message -->
|
|
<p class="flex-1 text-sm font-medium text-white leading-snug">
|
|
{{ message }}
|
|
</p>
|
|
|
|
<!-- Dismiss button -->
|
|
<button
|
|
type="button"
|
|
class="ml-auto shrink-0 rounded p-1 text-white/70 hover:text-white hover:bg-white/10 transition-colors focus:outline-none focus:ring-2 focus:ring-white/40"
|
|
aria-label="Dismiss notification"
|
|
@click="dismiss"
|
|
>
|
|
<XMarkIcon class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Progress bar for success auto-dismiss -->
|
|
<div
|
|
v-if="type === 'success'"
|
|
class="absolute bottom-0 left-0 h-[2px] bg-white/30 animate-[shrink_8s_linear_forwards]"
|
|
style="width: 100%"
|
|
/>
|
|
</div>
|
|
</Transition>
|
|
</template>
|
|
|
|
<style scoped>
|
|
@keyframes shrink {
|
|
from { width: 100%; }
|
|
to { width: 0%; }
|
|
}
|
|
</style>
|