Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 0 additions & 25 deletions src/app/api/notifications/stream/route.ts

This file was deleted.

45 changes: 18 additions & 27 deletions src/components/notifications/NotificationCenter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Bell, X } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useState, useEffect, type MouseEvent } from 'react'
import { createPortal } from 'react-dom'
import { POLLING_INTERVALS } from '@/data/constants'
import { CACHE_DURATIONS, POLLING_INTERVALS } from '@/data/constants'
import { api } from '@/lib/api'
import toast from '@/lib/toast'
import { cn } from '@/lib/utils'
Expand All @@ -27,18 +27,20 @@ function NotificationCenter(props: Props) {
const notificationsQuery = api.notifications.get.useQuery(
{ limit: 10, offset: 0 },
{
enabled: !!user,
refetchOnWindowFocus: true,
refetchInterval: POLLING_INTERVALS.NOTIFICATIONS,
enabled: !!user && isOpen,
refetchOnWindowFocus: isOpen,
refetchInterval: isOpen ? POLLING_INTERVALS.SHORT : false,
staleTime: CACHE_DURATIONS.VERY_SHORT,
},
)
const unreadCountQuery = api.notifications.getUnreadCount.useQuery(undefined, {
enabled: !!user,
staleTime: CACHE_DURATIONS.SHORT,
refetchOnWindowFocus: true,
refetchInterval: POLLING_INTERVALS.NOTIFICATIONS,
refetchInterval: isOpen ? false : POLLING_INTERVALS.EXTRA_LONG,
refetchIntervalInBackground: false,
})

// Mutations
const markAsReadMutation = api.notifications.markAsRead.useMutation({
onMutate: () => setIsLoading(true),
onSuccess: () => {
Expand Down Expand Up @@ -85,7 +87,15 @@ function NotificationCenter(props: Props) {
router.push('/notifications')
}

// Add escape key handler
const handleToggleNotifications = () => {
const nextIsOpen = !isOpen
setIsOpen(nextIsOpen)

if (nextIsOpen) {
void utils.notifications.getUnreadCount.invalidate()
}
}

useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen) {
Expand All @@ -104,21 +114,17 @@ function NotificationCenter(props: Props) {
}
}, [isOpen])

// Don't render anything if user is not authenticated
if (!user) return null

const handleNotificationClick = (notification: (typeof notifications)[0]) => {
// Mark as read if not already read (swallow error if it fails)
if (!notification.isRead) {
markAsReadMutation.mutateAsync({ notificationId: notification.id }).catch(console.error)
}

setIsOpen(false)

// Navigate based on actionUrl if available
if (notification.actionUrl) return router.push(notification.actionUrl)

// Try to extract route from metadata if actionUrl is not available
const metadata = notification.metadata as Record<string, unknown>
if (typeof metadata?.listingId === 'string') {
router.push(`/listings/${metadata.listingId}`)
Expand All @@ -127,7 +133,6 @@ function NotificationCenter(props: Props) {
} else if (typeof metadata?.userId === 'string') {
router.push(`/users/${metadata.userId}`)
} else {
// Default to notifications page if no specific route
router.push('/notifications')
}
}
Expand All @@ -141,7 +146,6 @@ function NotificationCenter(props: Props) {
}

const handleBackdropClick = (ev: MouseEvent) => {
// Only close if the click is directly on the backdrop, not bubbling from child elements
if (ev.target !== ev.currentTarget) return
setIsOpen(false)
}
Expand All @@ -151,14 +155,13 @@ function NotificationCenter(props: Props) {

return (
<div className={cn('relative', props.className)}>
{/* Notification Bell Button */}
<button
aria-label={
unreadCount > 0 ? `Open notifications (${unreadCount} unread)` : 'Open notifications'
}
onClick={(ev) => {
ev.stopPropagation()
setIsOpen(!isOpen)
handleToggleNotifications()
}}
className="relative p-2 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 transition-colors"
disabled={isLoading}
Expand All @@ -175,7 +178,6 @@ function NotificationCenter(props: Props) {
)}
</button>

{/* Desktop Dropdown */}
<AnimatePresence>
{isOpen && (
<motion.div
Expand All @@ -189,7 +191,6 @@ function NotificationCenter(props: Props) {
role="region"
aria-label="Notifications"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Notifications</h3>
<div className="flex items-center gap-2">
Expand All @@ -212,7 +213,6 @@ function NotificationCenter(props: Props) {
</div>
</div>

{/* Notifications List */}
<div className="max-h-72 overflow-y-auto">
<NotificationList
notifications={notifications}
Expand All @@ -225,7 +225,6 @@ function NotificationCenter(props: Props) {
/>
</div>

{/* Footer */}
{notifications.length > 0 && (
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 sticky bottom-0">
<button
Expand All @@ -240,13 +239,11 @@ function NotificationCenter(props: Props) {
)}
</AnimatePresence>

{/* Mobile Bottom Sheet */}
{typeof document !== 'undefined' &&
createPortal(
<AnimatePresence mode="wait">
{isOpen && (
<>
{/* Backdrop for mobile */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
Expand All @@ -257,7 +254,6 @@ function NotificationCenter(props: Props) {
aria-hidden="true"
/>

{/* Bottom Sheet */}
<motion.div
initial={{ translateY: '100%' }}
animate={{ translateY: 0 }}
Expand All @@ -273,12 +269,10 @@ function NotificationCenter(props: Props) {
aria-modal="true"
aria-label="Notifications"
>
{/* Handle Bar */}
<div className="flex flex-shrink-0 justify-center pb-2 pt-3">
<div className="h-1 w-12 rounded-full bg-gray-300 dark:bg-gray-600" />
</div>

{/* Header */}
<div className="flex flex-shrink-0 items-center justify-between border-b border-gray-200 px-4 py-3 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Notifications
Expand All @@ -303,7 +297,6 @@ function NotificationCenter(props: Props) {
</div>
</div>

{/* Notifications List */}
<div className="min-h-0 flex-1 overflow-y-auto">
<NotificationList
notifications={notifications}
Expand All @@ -316,7 +309,6 @@ function NotificationCenter(props: Props) {
/>
</div>

{/* Footer */}
{notifications.length > 0 && (
<div className="flex-shrink-0 border-t border-gray-200 p-4 pb-safe dark:border-gray-700">
<button
Expand All @@ -334,7 +326,6 @@ function NotificationCenter(props: Props) {
document.body,
)}

{/* Desktop Backdrop */}
<AnimatePresence>
{isOpen && (
<motion.div
Expand Down
1 change: 0 additions & 1 deletion src/data/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { ms } from '@/utils/time'
// Polling intervals in milliseconds
export const POLLING_INTERVALS = {
SHORT: ms.minutes(1),
NOTIFICATIONS: ms.minutes(3),
LONG: ms.minutes(5),
EXTRA_LONG: ms.minutes(10),
} as const
Expand Down
Loading
Loading