Files
HarborForge.Frontend/src/components/Sidebar.tsx
hzhang 8e52e2bf74 feat(ui): overridable favicon/logo with branded default
Replace the  emoji with a real logo image used as the in-app brand
mark and the favicon. Default bundled public/logo.svg is the
HangmanLab mark recolored to the Foundry-Deck ember (#ff6a1a).
Override at deploy time via HARBORFORGE_LOGO_URL (injected into
runtime-config.js; getLogoUrl() + favicon swap), no rebuild needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:39:58 +01:00

79 lines
2.7 KiB
TypeScript

import { useState, useEffect } from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import api from '@/services/api'
import { useAuthConfig, oidcLinkHref } from '@/hooks/useAuthConfig'
import { getLogoUrl } from '@/runtime'
import type { User } from '@/types'
interface Props {
user: User | null
onLogout: () => void
}
export default function Sidebar({ user, onLogout }: Props) {
const { pathname } = useLocation()
const navigate = useNavigate()
const { config: authCfg } = useAuthConfig()
const [unreadCount, setUnreadCount] = useState(0)
useEffect(() => {
if (!user) {
setUnreadCount(0)
return
}
api.get<{ count: number }>('/notifications/count')
.then(({ data }) => setUnreadCount(data.count))
.catch(() => {})
const timer = setInterval(() => {
api.get<{ count: number }>('/notifications/count')
.then(({ data }) => setUnreadCount(data.count))
.catch(() => {})
}, 30000)
return () => clearInterval(timer)
}, [user])
const links = user ? [
{ to: '/', icon: '📊', label: 'Dashboard' },
{ to: '/projects', icon: '📁', label: 'Projects' },
{ to: '/proposals', icon: '💡', label: 'Proposals' },
{ to: '/calendar', icon: '📅', label: 'Calendar' },
{ to: '/notifications', icon: '🔔', label: 'Notifications' + (unreadCount > 0 ? ' (' + unreadCount + ')' : '') },
{ to: '/monitor', icon: '📡', label: 'Monitor' },
...(user.is_admin ? [
{ to: '/users', icon: '👥', label: 'Users' },
{ to: '/roles', icon: '🔐', label: 'Roles' },
{ to: '/settings/oidc', icon: '🪪', label: 'OIDC' },
] : []),
] : [
{ to: '/monitor', icon: '📡', label: 'Monitor' },
]
return (
<nav className="sidebar">
<div className="sidebar-header">
<h1><img src={getLogoUrl()} className="brand-logo" alt="" /> HarborForge</h1>
</div>
<ul className="nav-links">
{links.map((l) => (
<li key={l.to} className={pathname === l.to || (l.to !== '/' && pathname.startsWith(l.to)) ? 'active' : ''}>
<Link to={l.to}>{l.icon} {l.label}</Link>
</li>
))}
</ul>
<div className="sidebar-footer">
<span>👤 {user ? user.username : 'Guest'}</span>
{user ? (
<button onClick={onLogout}>Log out</button>
) : (
<button onClick={() => navigate('/login')}>Log in</button>
)}
</div>
{user && authCfg.oidcEnabled && !authCfg.oidcOnly && (
<div className="sidebar-footer" style={{ borderTop: 'none', paddingTop: 0 }}>
<a href={oidcLinkHref()} title="Link your account to an OIDC identity">🔗 Link OIDC account</a>
</div>
)}
</nav>
)
}