feat: add user preference for audio notifications and play sound on new alerts
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m53s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m53s
- Added sound_enabled column to users table with a default of true. - Implemented a pleasant pop sound (notification.mp3) that plays when a new unread notification arrives. - Added a toggle in the User Profile page allowing users to enable/disable the sound.
This commit is contained in:
@@ -4,7 +4,11 @@ import {
|
||||
LayoutDashboard, Users, UserCircle, Bell, Search, Menu, X, LogOut,
|
||||
Hexagon, Settings, Building2, Sun, Moon, Loader2
|
||||
} from 'lucide-react';
|
||||
import { getAttendances, getUsers, getUserById, logout, searchGlobal, getNotifications, markNotificationAsRead, markAllNotificationsAsRead } from '../services/dataService';
|
||||
import {
|
||||
getAttendances, getUsers, getUserById, logout, searchGlobal,
|
||||
getNotifications, markNotificationAsRead, markAllNotificationsAsRead,
|
||||
deleteNotification, clearAllNotifications
|
||||
} from '../services/dataService';
|
||||
import { User } from '../types';
|
||||
|
||||
const SidebarItem = ({ to, icon: Icon, label, collapsed }: { to: string, icon: any, label: string, collapsed: boolean }) => (
|
||||
@@ -40,10 +44,26 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
|
||||
const [notifications, setNotifications] = useState<any[]>([]);
|
||||
const [showNotifications, setShowNotifications] = useState(false);
|
||||
const unreadCount = notifications.filter(n => !n.is_read).length;
|
||||
const previousUnreadCountRef = React.useRef(unreadCount);
|
||||
|
||||
const playNotificationSound = () => {
|
||||
if (currentUser?.sound_enabled !== false) {
|
||||
const audio = new Audio('/audio/notification.mp3');
|
||||
audio.volume = 0.5;
|
||||
audio.play().catch(e => console.log('Audio play failed (browser policy):', e));
|
||||
}
|
||||
};
|
||||
|
||||
const loadNotifications = async () => {
|
||||
const data = await getNotifications();
|
||||
setNotifications(data);
|
||||
|
||||
// Check if there are new unread notifications
|
||||
const newUnreadCount = data.filter((n: any) => !n.is_read).length;
|
||||
if (newUnreadCount > previousUnreadCountRef.current) {
|
||||
playNotificationSound();
|
||||
}
|
||||
previousUnreadCountRef.current = newUnreadCount;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -389,48 +409,80 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
|
||||
|
||||
{showNotifications && (
|
||||
<div className="absolute top-full mt-2 right-0 w-80 bg-white dark:bg-dark-card border border-zinc-200 dark:border-dark-border rounded-2xl shadow-2xl overflow-hidden z-50 animate-in fade-in slide-in-from-top-2 duration-200">
|
||||
<div className="p-4 border-b border-zinc-100 dark:border-dark-border flex justify-between items-center">
|
||||
<div className="p-4 border-b border-zinc-100 dark:border-dark-border flex justify-between items-center bg-zinc-50/50 dark:bg-dark-bg/50">
|
||||
<h3 className="font-bold text-zinc-900 dark:text-dark-text">Notificações</h3>
|
||||
{unreadCount > 0 && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
await markAllNotificationsAsRead();
|
||||
loadNotifications();
|
||||
}}
|
||||
className="text-xs text-brand-yellow hover:underline"
|
||||
>
|
||||
Marcar todas como lidas
|
||||
</button>
|
||||
)}
|
||||
<div className="flex gap-3">
|
||||
{unreadCount > 0 && (
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await markAllNotificationsAsRead();
|
||||
loadNotifications();
|
||||
}}
|
||||
className="text-xs text-brand-yellow hover:underline"
|
||||
>
|
||||
Marcar lidas
|
||||
</button>
|
||||
)}
|
||||
{notifications.length > 0 && (
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await clearAllNotifications();
|
||||
loadNotifications();
|
||||
}}
|
||||
className="text-xs text-zinc-400 hover:text-red-500 hover:underline transition-colors"
|
||||
>
|
||||
Limpar tudo
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{notifications.length > 0 ? (
|
||||
notifications.map(n => (
|
||||
<button
|
||||
<div
|
||||
key={n.id}
|
||||
onClick={async () => {
|
||||
if (!n.is_read) await markNotificationAsRead(n.id);
|
||||
if (n.link) navigate(n.link);
|
||||
setShowNotifications(false);
|
||||
loadNotifications();
|
||||
}}
|
||||
className={`w-full p-4 text-left hover:bg-zinc-50 dark:hover:bg-dark-border transition-colors border-b border-zinc-50 dark:border-dark-border/50 last:border-0 ${!n.is_read ? 'bg-brand-yellow/5 dark:bg-brand-yellow/5' : ''}`}
|
||||
className={`w-full relative group p-4 text-left hover:bg-zinc-50 dark:hover:bg-dark-border transition-colors border-b border-zinc-50 dark:border-dark-border/50 last:border-0 ${!n.is_read ? 'bg-brand-yellow/5 dark:bg-brand-yellow/5' : ''}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<span className={`text-xs font-bold uppercase tracking-wider ${
|
||||
n.type === 'success' ? 'text-green-500' :
|
||||
n.type === 'warning' ? 'text-orange-500' :
|
||||
n.type === 'error' ? 'text-red-500' : 'text-blue-500'
|
||||
}`}>
|
||||
{n.type}
|
||||
</span>
|
||||
<span className="text-[10px] text-zinc-400 dark:text-dark-muted">
|
||||
{new Date(n.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
<div
|
||||
className="cursor-pointer pr-6"
|
||||
onClick={async () => {
|
||||
if (!n.is_read) await markNotificationAsRead(n.id);
|
||||
if (n.link) navigate(n.link);
|
||||
setShowNotifications(false);
|
||||
loadNotifications();
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<span className={`text-xs font-bold uppercase tracking-wider ${
|
||||
n.type === 'success' ? 'text-green-500' :
|
||||
n.type === 'warning' ? 'text-orange-500' :
|
||||
n.type === 'error' ? 'text-red-500' : 'text-blue-500'
|
||||
}`}>
|
||||
{n.type}
|
||||
</span>
|
||||
<span className="text-[10px] text-zinc-400 dark:text-dark-muted">
|
||||
{new Date(n.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm font-bold text-zinc-900 dark:text-dark-text mb-0.5">{n.title}</div>
|
||||
<p className="text-xs text-zinc-500 dark:text-dark-muted line-clamp-2">{n.message}</p>
|
||||
</div>
|
||||
<div className="text-sm font-bold text-zinc-900 dark:text-dark-text mb-0.5">{n.title}</div>
|
||||
<p className="text-xs text-zinc-500 dark:text-dark-muted line-clamp-2">{n.message}</p>
|
||||
</button>
|
||||
|
||||
{/* Delete Button */}
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await deleteNotification(n.id);
|
||||
loadNotifications();
|
||||
}}
|
||||
className="absolute top-4 right-4 p-1 text-zinc-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-all rounded-md hover:bg-red-50 dark:hover:bg-red-900/30"
|
||||
title="Remover notificação"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="p-8 text-center text-zinc-500 dark:text-dark-muted text-sm">
|
||||
|
||||
Reference in New Issue
Block a user