Skip to content

Comments

feat: advanced in-app notification system (closes #139)#293

Merged
Ogstevyn merged 2 commits intoOgstevyn:mainfrom
Tijesunimi004:feature/notification-system-139
Feb 25, 2026
Merged

feat: advanced in-app notification system (closes #139)#293
Ogstevyn merged 2 commits intoOgstevyn:mainfrom
Tijesunimi004:feature/notification-system-139

Conversation

@Tijesunimi004
Copy link
Contributor

🔔 Advanced In-App Notification System

Resolves #139 · 150 points · Labels: features type:ui complexity:MEDIUM


Overview

Full end-to-end notification system with real-time delivery, toast alerts, a notification center drawer, per-type preferences, read/unread state management, and action buttons.


Changes

🗄️ Database (migrations/013_create_notifications.sql)

  • notifications table — type enum, read/unread, action URL/label, metadata, read_at
  • notification_preferences table — per-type toggles + sound/email globals
  • Row-Level Security (users own their rows only)
  • Indexes for fast unread queries
  • Supabase Realtime publication enabled

🔑 Types

  • lib/types/database.ts: NotificationType, Notification, NotificationRow, NotificationInsert/Update, NotificationPreferences, NotificationPreferencesRow/Update
  • lib/types/index.ts: barrel exports added

🌐 API Routes

Method Path Description
GET /api/notifications List (paginated, unread filter)
POST /api/notifications Create
DELETE /api/notifications Bulk-delete read
PATCH /api/notifications/[id] Mark as read
DELETE /api/notifications/[id] Delete single
POST /api/notifications/mark-all-read Mark all read
GET/PUT /api/notifications/preferences Get/upsert preferences

⚛️ State Management

  • contexts/NotificationContext.tsx — typed context shape
  • providers/NotificationProvider.tsx — Supabase Realtime subscription (INSERT/UPDATE/DELETE), optimistic updates, per-type preference filtering, Web AudioContext sounds
  • hooks/useNotifications.ts + barrel export in hooks/index.ts

🎨 UI Components

Component Features
NotificationBell.tsx Animated unread badge (99+ cap), aria-expanded, toggles drawer
NotificationToast.tsx Type-coloured icon, action link, dismiss button
NotificationCenter.tsx Slide-over drawer, All/Unread tabs, per-item hover actions (mark read, delete), bulk mark-all-read & clear-read, loading skeletons, empty states, Escape-to-close, mobile scroll-lock
NotificationPreferencesPanel.tsx Per-category toggles, sound & email global options, custom accessible ARIA switch

🏗️ Layout

  • app/layout.tsxNotificationProvider wraps the app tree; NotificationCenter mounted globally

🧪 Tests (__tests__/components/NotificationCenter.test.tsx)

16 test cases covering:

  • NotificationBell: badge, 99+ cap, setIsOpen, aria-expanded
  • NotificationCenter: open/close transitions, empty state, item rendering, loading skeleton, bulk actions, Escape key, tab switching
  • NotificationPreferencesPanel: all category toggles, null-preferences guard, onClose callback

Acceptance Criteria

  • Notifications display with type-specific icons & colours
  • Toast messages work (react-hot-toast)
  • Bell badge updates with unread count
  • Notification center accessible via bell icon
  • Mark as read / mark all as read
  • Delete individual / clear all read
  • Action buttons functional (action URL + label)
  • Preferences respected (per-type, sound, email)
  • Sound alerts via Web AudioContext
  • Mobile responsive (full-screen xs, max-w-md sm+)
  • Keyboard accessible (Escape closes drawer)
  • ARIA labels on all interactive elements
  • Real-time delivery via Supabase Realtime

## Summary
Build a full in-app notification system with real-time delivery,
toast alerts, bell badge, notification center, and user preferences.

## Changes

### Database
- migrations/013_create_notifications.sql
  -  table with type enum, read/unread status, action URL
  -  table with per-type + sound/email toggles
  - RLS policies (users own their notifications & preferences)
  - Indexes for efficient unread queries
  - Supabase Realtime publication for live delivery

### Types
- lib/types/database.ts: NotificationType, Notification, NotificationRow,
  NotificationInsert/Update, NotificationPreferences(Row/Update)
- lib/types/index.ts: barrel-export all new types

### API Routes
- GET/POST/DELETE /api/notifications        – list, create, bulk-clear read
- PATCH/DELETE    /api/notifications/[id]   – mark read, delete single
- POST            /api/notifications/mark-all-read
- GET/PUT         /api/notifications/preferences  – upsert prefs

### Context & Provider
- contexts/NotificationContext.tsx: typed context shape
- providers/NotificationProvider.tsx:
  - Loads notifications + preferences on mount
  - Supabase Realtime subscription (INSERT/UPDATE/DELETE)
  - Respects per-type preferences before showing toasts
  - Web AudioContext sound alerts (sound_enabled pref)
  - Optimistic updates for all mutations

### Hook
- hooks/useNotifications.ts: consumer hook with error guard
- hooks/index.ts: barrel export added

### Components
- components/NotificationToast.tsx: rich custom toast (type icon, action link)
- components/NotificationBell.tsx: bell icon with animated unread badge,
  aria-expanded, toggles notification center
- components/NotificationCenter.tsx: slide-over drawer with:
  - All/Unread tabs
  - Per-item mark-as-read & delete (revealed on hover)
  - Bulk mark-all-read & clear-read
  - Inline preferences panel toggle
  - Empty states, loading skeletons
  - Keyboard (Escape) close, body scroll lock on mobile
- components/NotificationPreferencesPanel.tsx:
  - Toggle per category (messages/payments/listings/agreements/favorites/system)
  - Sound & email global options
  - Custom accessible toggle switch component

### Layout
- app/layout.tsx: wrap tree with NotificationProvider; mount NotificationCenter

### Tests
- __tests__/components/NotificationCenter.test.tsx:
  - NotificationBell: badge, 99+ cap, setIsOpen, aria-expanded
  - NotificationCenter: open/close, empty state, items, loading, bulk actions,
    Escape key, tab switching
  - NotificationPreferencesPanel: toggles, null guard, onClose

Resolves: Ogstevyn#139
Points: 150
Labels: features, type:ui, complexity:MEDIUM
Copilot AI review requested due to automatic review settings February 25, 2026 04:47
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a comprehensive in-app notification system with real-time delivery via Supabase Realtime, a notification center drawer, toast notifications, and user preferences. The system integrates database tables, API routes, React context/providers, UI components, and automated tests.

Changes:

  • Database schema with notifications and notification_preferences tables, RLS policies, indexes, and realtime publication
  • Complete API layer with 7 endpoints for CRUD operations and preference management
  • React context and provider for state management with Supabase Realtime subscriptions
  • Four UI components: NotificationBell, NotificationCenter, NotificationToast, and NotificationPreferencesPanel
  • Integration into app layout with global provider wrapping
  • Comprehensive test suite covering all major components

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 20 comments.

Show a summary per file
File Description
migrations/013_create_notifications.sql Database schema for notifications and preferences with RLS and indexes
lib/types/database.ts TypeScript type definitions for notification entities
lib/types/index.ts Barrel export of notification types
app/api/notifications/route.ts List, create, and bulk delete endpoints
app/api/notifications/[id]/route.ts Update and delete single notification endpoints
app/api/notifications/mark-all-read/route.ts Mark all notifications as read endpoint
app/api/notifications/preferences/route.ts Get and update user preferences endpoint
contexts/NotificationContext.tsx React context type definition
providers/NotificationProvider.tsx State management with Realtime subscriptions and preference filtering
hooks/useNotifications.ts Custom hook for accessing notification context
hooks/index.ts Barrel export for useNotifications hook
components/NotificationBell.tsx Bell icon with unread badge
components/NotificationCenter.tsx Slide-over drawer with notification list and preferences
components/NotificationToast.tsx Toast notification component
components/NotificationPreferencesPanel.tsx Preferences panel with category toggles
app/layout.tsx Integration of NotificationProvider and NotificationCenter
tests/components/NotificationCenter.test.tsx Test suite for notification components

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +44 to +67
export async function POST(request: Request) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()

if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const body: Partial<NotificationInsert> = await request.json()

const { data, error } = await supabase
.from('notifications')
.insert({
user_id: user.id,
type: body.type ?? 'system',
title: body.title ?? '',
message: body.message ?? '',
action_url: body.action_url ?? null,
action_label: body.action_label ?? null,
metadata: body.metadata ?? {},
is_read: false,
})
.select()
.single()
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation: The POST endpoint doesn't validate that required fields like title and message are non-empty strings. The code defaults to empty strings if these fields are missing, which could allow creation of notifications with no content.

Consider adding validation to ensure title and message are present and non-empty before inserting into the database.

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +149
{notification.action_url && notification.action_label && (
<Link
href={notification.action_url}
onClick={() => !notification.is_read && onRead(notification.id)}
className="mt-1 inline-flex w-fit items-center gap-1 text-xs font-medium text-primary-400 hover:text-primary-300 hover:underline underline-offset-2"
>
{notification.action_label}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</Link>
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential security issue: The action_url from the notification is passed directly to Next.js Link without validation. This could allow malicious URLs like javascript: protocols if a notification is created with a crafted action_url.

Consider validating the action_url to ensure it's a safe URL (e.g., starts with http://, https://, or is a relative path starting with /) before rendering the Link component.

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +141
setPreferences((currentPrefs) => {
const typeKey = `${newNotif.type}_enabled` as keyof NotificationPreferences
const enabled = currentPrefs ? (currentPrefs[typeKey] as boolean ?? true) : true

if (enabled) {
const icon = TYPE_ICON[newNotif.type] ?? '🔔'
toast(newNotif.title, {
icon,
duration: 5000,
})

if (currentPrefs?.sound_enabled) {
playNotificationSound()
}
}

return currentPrefs
})
},
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential race condition: Using setPreferences with a callback inside the realtime INSERT handler to access current preferences could lead to stale closures. The preferences value captured in the callback might not be the most up-to-date if multiple notifications arrive rapidly or if preferences are updated concurrently.

Consider using a ref for preferences (e.g., preferencesRef.current) to always access the latest value, or restructure the logic to avoid this callback pattern.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +46
function playNotificationSound() {
try {
const ctx = new AudioContext()
const oscillator = ctx.createOscillator()
const gainNode = ctx.createGain()
oscillator.connect(gainNode)
gainNode.connect(ctx.destination)
oscillator.type = 'sine'
oscillator.frequency.setValueAtTime(880, ctx.currentTime)
oscillator.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + 0.15)
gainNode.gain.setValueAtTime(0.3, ctx.currentTime)
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3)
oscillator.start(ctx.currentTime)
oscillator.stop(ctx.currentTime + 0.3)
} catch {
// AudioContext not available (e.g. SSR / restricted environments)
}
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential memory leak: Each AudioContext instance created in playNotificationSound() is never explicitly closed. While modern browsers do eventually garbage collect these, creating a new AudioContext for every notification can accumulate resources unnecessarily.

Consider reusing a single AudioContext instance, or explicitly calling ctx.close() after the sound completes (e.g., in a setTimeout after 0.3s).

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@Ogstevyn
Copy link
Owner

this is nice

@Ogstevyn Ogstevyn merged commit a274f18 into Ogstevyn:main Feb 25, 2026
0 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement In-App Notifications V2

2 participants