Files
Sunshine/src_assets/common/assets/web/Notification.vue
T

151 lines
5.0 KiB
Vue

<template>
<div class="notification-container" v-if="state.notifications.length > 0">
<div
v-for="n in state.notifications"
:key="n.id"
class="alert d-flex align-items-start gap-2 mb-2"
:class="'alert-' + n.type"
role="alert"
>
<component :is="iconFor(n.type)" :size="18" class="icon flex-shrink-0 mt-1"></component>
<div class="flex-grow-1">
<div v-if="n.titleKey || n.title">
<strong>{{ n.titleKey ? $t(n.titleKey) : n.title }}</strong>
</div>
<span>{{ n.messageKey ? $t(n.messageKey) : n.message }}</span>
</div>
<button type="button" class="btn-close" :aria-label="$t('_common.dismiss')" @click="dismiss(n.id)"></button>
</div>
</div>
</template>
<script>
import { reactive } from 'vue'
import { AlertCircle, AlertTriangle, CheckCircle, Info } from '@lucide/vue'
/**
* Singleton reactive notification state shared across the app instance.
* Using reactive() at module scope means all consumers — including plain JS
* modules like fetch_utils.js — mutate the same reactive object, and any
* mounted Notification component will update automatically.
*/
export const state = reactive({
notifications: [],
_nextId: 1,
})
/**
* Push a notification using raw strings.
*
* @param {'danger'|'warning'|'success'|'info'} type - Bootstrap color variant.
* @param {string} message - The notification body text.
* @param {string} [title] - Optional bold title prefix.
*/
function push(type, message, title) {
state.notifications.push({ id: state._nextId++, type, message, title: title || null, messageKey: null, titleKey: null })
}
/**
* Push a notification using i18n translation keys.
*
* @param {'danger'|'warning'|'success'|'info'} type - Bootstrap color variant.
* @param {string} messageKey - i18n key for the notification body.
* @param {string} [titleKey] - Optional i18n key for the bold title prefix.
*/
function pushKey(type, messageKey, titleKey) {
state.notifications.push({ id: state._nextId++, type, message: null, title: null, messageKey, titleKey: titleKey || null })
}
/**
* Map a Bootstrap color variant to its corresponding @lucide/vue icon name.
*
* @param {'danger'|'warning'|'success'|'info'} type - Bootstrap color variant.
* @returns {string} The lucide icon component name.
*/
function iconFor(type) {
return { danger: 'AlertCircle', warning: 'AlertTriangle', success: 'CheckCircle', info: 'Info' }[type] || 'Info'
}
/**
* Convenience helpers for the four common variants using raw strings.
*/
export const notify = {
/**
* @param {string} message - The notification body text.
* @param {string} [title] - Optional bold title shown above the message.
*/
error: (message, title) => push('danger', message, title),
/**
* @param {string} message - The notification body text.
* @param {string} [title] - Optional bold title shown above the message.
*/
warning: (message, title) => push('warning', message, title),
/**
* @param {string} message - The notification body text.
* @param {string} [title] - Optional bold title shown above the message.
*/
success: (message, title) => push('success', message, title),
/**
* @param {string} message - The notification body text.
* @param {string} [title] - Optional bold title shown above the message.
*/
info: (message, title) => push('info', message, title),
}
/**
* Convenience helpers for the four common variants using i18n keys.
*/
export const notifyKey = {
/**
* @param {string} messageKey - i18n key for the notification body text.
* @param {string} [titleKey] - Optional i18n key for the bold title shown above the message.
*/
error: (messageKey, titleKey) => pushKey('danger', messageKey, titleKey),
/**
* @param {string} messageKey - i18n key for the notification body text.
* @param {string} [titleKey] - Optional i18n key for the bold title shown above the message.
*/
warning: (messageKey, titleKey) => pushKey('warning', messageKey, titleKey),
/**
* @param {string} messageKey - i18n key for the notification body text.
* @param {string} [titleKey] - Optional i18n key for the bold title shown above the message.
*/
success: (messageKey, titleKey) => pushKey('success', messageKey, titleKey),
/**
* @param {string} messageKey - i18n key for the notification body text.
* @param {string} [titleKey] - Optional i18n key for the bold title shown above the message.
*/
info: (messageKey, titleKey) => pushKey('info', messageKey, titleKey),
}
export default {
components: { AlertCircle, AlertTriangle, CheckCircle, Info },
setup() {
function dismiss(id) {
const idx = state.notifications.findIndex(n => n.id === id)
if (idx !== -1) state.notifications.splice(idx, 1)
}
return { state, dismiss, iconFor }
},
}
</script>
<style scoped>
.notification-container {
position: fixed;
bottom: 1rem;
right: 1rem;
left: 1rem;
z-index: 1090;
}
@media (min-width: 576px) {
.notification-container {
left: auto;
width: 22rem;
}
}
</style>