151 lines
5.9 KiB
JavaScript
151 lines
5.9 KiB
JavaScript
import { Link } from 'react-router-dom'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
|
|
import Dropdown, { DropdownGroup } from '../ui/Dropdown'
|
|
import GlobalSearch from './GlobalSearch'
|
|
import { Avatar, Icon } from '../ui/primitives'
|
|
import { seedQuery, useSeedMutation } from '../data/seedQueries'
|
|
import { useToast } from '../ui/Toast'
|
|
import { useTheme } from '../theme/ThemeProvider'
|
|
import { useAuth } from '../auth/AuthContext'
|
|
|
|
function initialsFromName(name) {
|
|
const parts = String(name || '').trim().split(/\s+/).filter(Boolean)
|
|
if (!parts.length) return '?'
|
|
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
|
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
|
|
}
|
|
|
|
export default function Topbar({ onOpenNav, searchRef }) {
|
|
const { theme, toggleTheme } = useTheme()
|
|
const { user, signOut } = useAuth()
|
|
const { toast } = useToast()
|
|
|
|
const { data: notifications = [] } = useQuery(seedQuery('notifications'))
|
|
const { data: messages = [] } = useQuery(seedQuery('messages'))
|
|
const updateNotifications = useSeedMutation('notifications')
|
|
|
|
// App.hydrateProfile's DOM sweep is gone — the session is read directly.
|
|
const name = user?.name || 'Guest'
|
|
const email = user?.email || ''
|
|
const role = user?.role_name || user?.role || 'Member'
|
|
|
|
function markAllRead() {
|
|
updateNotifications((ns) => ns.map((n) => ({ ...n, unread: false })))
|
|
toast('All notifications marked as read', 'success')
|
|
}
|
|
|
|
return (
|
|
<header className="topbar">
|
|
<button className="icon-btn menu-toggle" onClick={onOpenNav} aria-label="Toggle menu">
|
|
<Icon name="menu" />
|
|
</button>
|
|
|
|
<GlobalSearch inputRef={searchRef} />
|
|
|
|
<div className="topbar-actions">
|
|
<button
|
|
className="icon-btn"
|
|
onClick={toggleTheme}
|
|
aria-pressed={theme === 'dark'}
|
|
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
>
|
|
<span className="icon-sun"><Icon name="sun" /></span>
|
|
<span className="icon-moon"><Icon name="moon" /></span>
|
|
</button>
|
|
|
|
<DropdownGroup>
|
|
<Dropdown
|
|
panelClassName="dropdown-menu-wide"
|
|
trigger={({ toggle }) => (
|
|
<button className="icon-btn" onClick={toggle} title="Messages" aria-label="Messages">
|
|
<Icon name="message" />
|
|
<span className="dot dot-blue" />
|
|
</button>
|
|
)}
|
|
>
|
|
<div className="dropdown-head">Messages</div>
|
|
<div className="dd-scroll">
|
|
{messages.map((m) => (
|
|
<div key={m.id ?? m.name} className={`notif-row${m.unread ? ' unread' : ''}`}>
|
|
<Avatar name={m.name} initials={m.initials} color={m.color} />
|
|
<div className="notif-body">
|
|
<div className="notif-title">{m.name}</div>
|
|
<div className="notif-text">{m.text}</div>
|
|
<div className="notif-time">{m.time} ago</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="dropdown-foot">
|
|
<Link to="/notifications">Open inbox</Link>
|
|
</div>
|
|
</Dropdown>
|
|
|
|
<Dropdown
|
|
panelClassName="dropdown-menu-wide"
|
|
trigger={({ toggle }) => (
|
|
<button className="icon-btn" onClick={toggle} title="Notifications" aria-label="Notifications">
|
|
<Icon name="bell" />
|
|
<span className="dot dot-red" />
|
|
</button>
|
|
)}
|
|
>
|
|
<div className="dropdown-head">
|
|
Notifications
|
|
<button className="link-btn" onClick={markAllRead}>Mark all read</button>
|
|
</div>
|
|
<div className="dd-scroll">
|
|
{notifications.slice(0, 6).map((n) => (
|
|
<div key={n.id ?? n.title} className={`notif-row${n.unread ? ' unread' : ''}`}>
|
|
<span className={`notif-icn ${n.color}`}><Icon name={n.icon} /></span>
|
|
<div className="notif-body">
|
|
<div className="notif-title">{n.title}</div>
|
|
<div className="notif-text">{n.text}</div>
|
|
<div className="notif-time">{n.time}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="dropdown-foot">
|
|
<Link to="/notifications">View all</Link>
|
|
</div>
|
|
</Dropdown>
|
|
|
|
<div className="topbar-divider" />
|
|
|
|
<Dropdown
|
|
trigger={({ toggle }) => (
|
|
<button className="profile-btn" onClick={toggle}>
|
|
<span className="avatar avatar-grad">{initialsFromName(name)}</span>
|
|
<span className="profile-meta">
|
|
<span className="profile-name">{name}</span>
|
|
<span className="profile-role">{role}</span>
|
|
</span>
|
|
<Icon name="chevron-down" className="chev" />
|
|
</button>
|
|
)}
|
|
>
|
|
<div className="dropdown-profile">
|
|
<span className="avatar avatar-grad avatar-lg">{initialsFromName(name)}</span>
|
|
<div>
|
|
<div className="dp-name">{name}</div>
|
|
<div className="dp-email">{email}</div>
|
|
</div>
|
|
</div>
|
|
<div className="dropdown-divider" />
|
|
<Link className="dropdown-link" to="/settings"><Icon name="user" />My Profile</Link>
|
|
<Link className="dropdown-link" to="/settings"><Icon name="settings" />Settings</Link>
|
|
<Link className="dropdown-link" to="/help"><Icon name="help" />Help Center</Link>
|
|
<div className="dropdown-divider" />
|
|
<button className="dropdown-link danger" onClick={signOut}>
|
|
<Icon name="logout" />Sign out
|
|
</button>
|
|
</Dropdown>
|
|
</DropdownGroup>
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|