feat: enhance workspace sidebar with create menu and document draft management

- 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.
This commit is contained in:
Anurag Jangid 2026-10-23 21:54:48 +05:30
parent e905184c7f
commit eed9504a53
21 changed files with 891 additions and 125 deletions

View file

@ -0,0 +1,76 @@
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>
)
}