- Added a create menu in the WorkspaceSidebar for creating new documents (live docs, pages, whiteboards, databases, smart links, folders). - Implemented document draft functionality to save and restore drafts for documents across various pages (ShedPage, SiloPage, SketchPage, SteadPage). - Improved error handling in useDashboardDocuments for better user feedback on API errors. - Introduced a NotificationBell component for displaying notifications with a read/unread state. - Updated document update and publish logic to clear drafts upon successful updates. - Enhanced user experience with keyboard shortcuts for editing and saving documents.
76 lines
2.8 KiB
TypeScript
76 lines
2.8 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
||
import { Bell, X } from 'lucide-react'
|
||
import { documentsApi, type ApiNotification } from '../../lib/api'
|
||
|
||
interface NotificationBellProps {
|
||
compact?: boolean
|
||
}
|
||
|
||
export default function NotificationBell({ compact = false }: NotificationBellProps) {
|
||
const [notifications, setNotifications] = useState<ApiNotification[]>([])
|
||
const [open, setOpen] = useState(false)
|
||
const rootRef = useRef<HTMLDivElement>(null)
|
||
|
||
useEffect(() => {
|
||
void documentsApi.listNotifications().then(setNotifications).catch(() => setNotifications([]))
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
function closeOnOutside(event: MouseEvent) {
|
||
if (rootRef.current && !rootRef.current.contains(event.target as Node)) setOpen(false)
|
||
}
|
||
document.addEventListener('mousedown', closeOnOutside)
|
||
return () => document.removeEventListener('mousedown', closeOnOutside)
|
||
}, [])
|
||
|
||
function toggle() {
|
||
const nextOpen = !open
|
||
setOpen(nextOpen)
|
||
if (nextOpen) {
|
||
void documentsApi.markNotificationsRead().then(() => {
|
||
setNotifications((items) => items.map((item) => ({ ...item, read: true })))
|
||
})
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div ref={rootRef} className="relative">
|
||
<button
|
||
type="button"
|
||
onClick={toggle}
|
||
className={`relative rounded-lg text-silos-muted transition-colors hover:bg-silos-cream hover:text-silos-text ${
|
||
compact ? 'p-1.5' : 'p-2'
|
||
}`}
|
||
aria-label="Notifications"
|
||
aria-expanded={open}
|
||
>
|
||
<Bell className={compact ? 'h-3.5 w-3.5' : 'h-5 w-5'} />
|
||
{notifications.some((item) => !item.read) && (
|
||
<span className="absolute right-1 top-1 h-1.5 w-1.5 rounded-full bg-red-500" />
|
||
)}
|
||
</button>
|
||
{open && (
|
||
<div className="absolute right-0 top-10 z-50 w-80 rounded-xl border border-silos-cream-dark bg-white p-2 shadow-xl">
|
||
<div className="flex items-center justify-between px-2 py-1">
|
||
<p className="text-xs font-semibold text-silos-text">Notifications</p>
|
||
<button type="button" onClick={() => setOpen(false)} aria-label="Close notifications">
|
||
<X className="h-3.5 w-3.5 text-silos-muted" />
|
||
</button>
|
||
</div>
|
||
{notifications.length === 0 ? (
|
||
<p className="px-2 py-4 text-xs text-silos-muted">You’re all caught up.</p>
|
||
) : (
|
||
notifications.slice(0, 8).map((notification) => (
|
||
<div key={notification.id} className="border-t border-silos-cream px-2 py-2 text-xs text-silos-text">
|
||
<p>{notification.message}</p>
|
||
<time className="text-[10px] text-silos-muted">
|
||
{new Date(notification.created_at).toLocaleString()}
|
||
</time>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|