feat(ui): add snackbar for realtime notifications

This commit is contained in:
josedario87
2025-06-11 04:12:02 -06:00
parent dce714e778
commit 74b9c0ad7e
4 changed files with 97 additions and 0 deletions

View File

@@ -2,6 +2,7 @@
import { watchEffect, computed } from 'vue' // Added computed import { watchEffect, computed } from 'vue' // Added computed
import TopBar from '@/components/ui/TopBar.vue' import TopBar from '@/components/ui/TopBar.vue'
import NavBar from '@/components/ui/NavBar.vue' import NavBar from '@/components/ui/NavBar.vue'
import SnackbarContainer from '@/components/ui/SnackbarContainer.vue'
import { useUi } from '@/stores/useUi' import { useUi } from '@/stores/useUi'
const ui = useUi() const ui = useUi()
@@ -80,6 +81,7 @@ const transitionDurationStyle = computed(() => {
</transition> </transition>
</router-view> </router-view>
</main> </main>
<SnackbarContainer />
</div> </div>
</template> </template>

View File

@@ -0,0 +1,67 @@
<template>
<div class="snackbar-wrapper pointer-events-none">
<transition-group name="snackbar" tag="div" class="snackbar-container">
<div
v-for="snack in snackbar.snacks"
:key="snack.id"
:class="['snackbar', snack.type]"
>
{{ snack.text }}
</div>
</transition-group>
</div>
</template>
<script setup>
import { useSnackbarStore } from '@/stores/useSnackbar'
const snackbar = useSnackbarStore()
</script>
<style scoped>
.snackbar-wrapper {
position: fixed;
bottom: 1rem;
right: 1rem;
z-index: 1000;
}
.snackbar-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.snackbar {
pointer-events: auto;
background-color: #333;
color: white;
padding: 0.75rem 1rem;
border-radius: 0.375rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.snackbar.info {
background-color: #323232;
}
.snackbar.success {
background-color: #22c55e;
color: #fff;
}
.snackbar.error {
background-color: #ef4444;
color: #fff;
}
.snackbar-enter-active,
.snackbar-leave-active {
transition: opacity 0.3s, transform 0.3s;
}
.snackbar-enter-from,
.snackbar-leave-to {
opacity: 0;
transform: translateY(20px);
}
</style>

View File

@@ -3,6 +3,7 @@ import { usePlanillasStore } from './usePlanillas'
import { useEmpleadosStore } from './useEmpleados' import { useEmpleadosStore } from './useEmpleados'
import { useTareasStore } from './useTareas' import { useTareasStore } from './useTareas'
import { useAsistenciasStore } from './useAsistencias' import { useAsistenciasStore } from './useAsistencias'
import { useSnackbarStore } from './useSnackbar'
export const useRealtimeStore = defineStore('realtime', { export const useRealtimeStore = defineStore('realtime', {
state: () => ({ state: () => ({
@@ -55,6 +56,11 @@ export const useRealtimeStore = defineStore('realtime', {
addEvent(); addEvent();
} }
const snackbar = useSnackbarStore();
const idPart = payload.new?.id || payload.old?.id;
const message = `${payload.operation} ${payload.table}${idPart ? ' #' + idPart : ''}`;
snackbar.push({ text: message });
// mark badge for module and operation // mark badge for module and operation
if (this.badges[payload.table]) { if (this.badges[payload.table]) {
this.badges[payload.table][payload.operation] = true; this.badges[payload.table][payload.operation] = true;

View File

@@ -0,0 +1,22 @@
import { defineStore } from 'pinia'
export const useSnackbarStore = defineStore('snackbar', {
state: () => ({
snacks: []
}),
actions: {
push({ text, type = 'info', duration = 4000 }) {
const id = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2)
const snack = { id, text, type }
this.snacks.push(snack)
if (duration > 0) {
setTimeout(() => this.remove(id), duration)
}
return id
},
remove(id) {
const index = this.snacks.findIndex(s => s.id === id)
if (index !== -1) this.snacks.splice(index, 1)
}
}
})