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:
parent
e905184c7f
commit
eed9504a53
21 changed files with 891 additions and 125 deletions
76
frontend/src/components/notifications/NotificationBell.tsx
Normal file
76
frontend/src/components/notifications/NotificationBell.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue