mirror of
https://github.com/clucraft/PriceGhost.git
synced 2026-04-25 00:36:32 +02:00
Add Clear button to notification bell dropdown
- Add notifications_cleared_at column to users table - Clear button marks notifications as seen without deleting history - Badge and dropdown only show notifications after last clear - History page still shows all notifications Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
262a91b558
commit
28d6523959
5 changed files with 69 additions and 11 deletions
|
|
@ -122,6 +122,9 @@ async function runMigrations() {
|
|||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'ai_verification_enabled') THEN
|
||||
ALTER TABLE users ADD COLUMN ai_verification_enabled BOOLEAN DEFAULT false;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'notifications_cleared_at') THEN
|
||||
ALTER TABLE users ADD COLUMN notifications_cleared_at TIMESTAMP;
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
|
|
|
|||
|
|
@ -856,28 +856,41 @@ export const notificationHistoryQueries = {
|
|||
return result.rows;
|
||||
},
|
||||
|
||||
// Get recent notifications (for bell dropdown)
|
||||
// Get recent notifications (for bell dropdown) - respects cleared_at
|
||||
getRecent: async (userId: number, limit: number = 10): Promise<NotificationHistory[]> => {
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM notification_history
|
||||
WHERE user_id = $1
|
||||
ORDER BY triggered_at DESC
|
||||
`SELECT nh.* FROM notification_history nh
|
||||
JOIN users u ON u.id = nh.user_id
|
||||
WHERE nh.user_id = $1
|
||||
AND (u.notifications_cleared_at IS NULL OR nh.triggered_at > u.notifications_cleared_at)
|
||||
ORDER BY nh.triggered_at DESC
|
||||
LIMIT $2`,
|
||||
[userId, limit]
|
||||
);
|
||||
return result.rows;
|
||||
},
|
||||
|
||||
// Count notifications in last 24 hours (for badge)
|
||||
// Count notifications since last clear (for badge)
|
||||
countRecent: async (userId: number, hours: number = 24): Promise<number> => {
|
||||
const result = await pool.query(
|
||||
`SELECT COUNT(*) FROM notification_history
|
||||
WHERE user_id = $1 AND triggered_at > NOW() - INTERVAL '1 hour' * $2`,
|
||||
`SELECT COUNT(*) FROM notification_history nh
|
||||
JOIN users u ON u.id = nh.user_id
|
||||
WHERE nh.user_id = $1
|
||||
AND nh.triggered_at > NOW() - INTERVAL '1 hour' * $2
|
||||
AND (u.notifications_cleared_at IS NULL OR nh.triggered_at > u.notifications_cleared_at)`,
|
||||
[userId, hours]
|
||||
);
|
||||
return parseInt(result.rows[0].count, 10);
|
||||
},
|
||||
|
||||
// Clear notifications (sets timestamp, doesn't delete)
|
||||
clear: async (userId: number): Promise<void> => {
|
||||
await pool.query(
|
||||
`UPDATE users SET notifications_cleared_at = NOW() WHERE id = $1`,
|
||||
[userId]
|
||||
);
|
||||
},
|
||||
|
||||
// Get total count for pagination
|
||||
getTotalCount: async (userId: number): Promise<number> => {
|
||||
const result = await pool.query(
|
||||
|
|
|
|||
|
|
@ -69,4 +69,16 @@ router.get('/count', async (req: AuthRequest, res: Response) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Clear notifications (marks as seen, doesn't delete history)
|
||||
router.post('/clear', async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const userId = req.userId!;
|
||||
await notificationHistoryQueries.clear(userId);
|
||||
res.json({ message: 'Notifications cleared' });
|
||||
} catch (error) {
|
||||
console.error('Error clearing notifications:', error);
|
||||
res.status(500).json({ error: 'Failed to clear notifications' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -313,6 +313,9 @@ export const notificationsApi = {
|
|||
api.get<{ count: number }>('/notifications/count', {
|
||||
params: hours ? { hours } : undefined,
|
||||
}),
|
||||
|
||||
clear: () =>
|
||||
api.post<{ message: string }>('/notifications/clear'),
|
||||
};
|
||||
|
||||
// Admin API
|
||||
|
|
|
|||
|
|
@ -99,6 +99,16 @@ export default function NotificationBell() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleClear = async () => {
|
||||
try {
|
||||
await notificationsApi.clear();
|
||||
setNotifications([]);
|
||||
setRecentCount(0);
|
||||
} catch (error) {
|
||||
console.error('Failed to clear notifications:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="notification-bell" ref={dropdownRef}>
|
||||
<style>{`
|
||||
|
|
@ -171,6 +181,22 @@ export default function NotificationBell() {
|
|||
height: 18px;
|
||||
}
|
||||
|
||||
.notification-clear-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.notification-clear-btn:hover {
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
|
|
@ -300,10 +326,11 @@ export default function NotificationBell() {
|
|||
<div className="notification-dropdown">
|
||||
<div className="notification-dropdown-header">
|
||||
<span>Notifications</span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||
</svg>
|
||||
{notifications.length > 0 && (
|
||||
<button className="notification-clear-btn" onClick={handleClear}>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue