Files
redlight/src/components/Sidebar.jsx
Michelle 71557280f5
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m32s
feat(sidebar): update user initials display to show first two letters of first and last name
2026-03-10 22:50:47 +01:00

167 lines
6.5 KiB
JavaScript

import { NavLink } from 'react-router-dom';
import { LayoutDashboard, Settings, Shield, X, Palette, Globe, CalendarDays, FileText, Lock } from 'lucide-react';
import BrandLogo from './BrandLogo';
import { useAuth } from '../contexts/AuthContext';
import { useLanguage } from '../contexts/LanguageContext';
import { useBranding } from '../contexts/BrandingContext';
import ThemeSelector from './ThemeSelector';
import { useState, useEffect } from 'react';
import api from '../services/api';
export default function Sidebar({ open, onClose }) {
const { user } = useAuth();
const { t } = useLanguage();
const { imprintUrl, privacyUrl } = useBranding();
const [themeOpen, setThemeOpen] = useState(false);
const [federationCount, setFederationCount] = useState(0);
// Fetch pending federation invitation count
useEffect(() => {
const fetchCount = async () => {
try {
const res = await api.get('/federation/invitations/pending-count');
setFederationCount(res.data.count || 0);
} catch {
// Ignore — federation may not be enabled
}
};
fetchCount();
const interval = setInterval(fetchCount, 30000);
return () => clearInterval(interval);
}, []);
const navItems = [
{ to: '/dashboard', icon: LayoutDashboard, label: t('nav.dashboard') },
{ to: '/calendar', icon: CalendarDays, label: t('nav.calendar') },
{ to: '/federation/inbox', icon: Globe, label: t('nav.federation'), badge: federationCount },
{ to: '/settings', icon: Settings, label: t('nav.settings') },
];
if (user?.role === 'admin') {
navItems.push({ to: '/admin', icon: Shield, label: t('nav.admin') });
}
const linkClasses = ({ isActive }) =>
`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${isActive
? 'bg-th-accent text-th-accent-t shadow-sm'
: 'text-th-text-s hover:text-th-text hover:bg-th-hover'
}`;
return (
<>
<aside
className={`fixed top-0 left-0 z-40 h-full w-64 bg-th-side border-r border-th-border
transition-transform duration-300 ease-in-out
${open ? 'translate-x-0' : '-translate-x-full'} lg:translate-x-0`}
>
<div className="flex flex-col h-full">
{/* Logo */}
<div className="flex items-center justify-between h-16 px-4 border-b border-th-border">
<BrandLogo size="md" className="flex-1 min-w-0" />
<button
onClick={onClose}
className="lg:hidden p-1.5 rounded-lg hover:bg-th-hover text-th-text-s transition-colors"
>
<X size={18} />
</button>
</div>
{/* Navigation */}
<nav className="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
<p className="px-3 mb-2 text-xs font-semibold text-th-text-s uppercase tracking-wider">
{t('nav.navigation')}
</p>
{navItems.map(item => (
<NavLink
key={item.to}
to={item.to}
className={linkClasses}
onClick={onClose}
>
<item.icon size={18} />
{item.label}
{item.badge > 0 && (
<span className="ml-auto px-1.5 py-0.5 rounded-full bg-th-accent text-th-accent-t text-xs font-bold">
{item.badge}
</span>
)}
</NavLink>
))}
<div className="pt-4">
<p className="px-3 mb-2 text-xs font-semibold text-th-text-s uppercase tracking-wider">
{t('nav.appearance')}
</p>
<button
onClick={() => setThemeOpen(!themeOpen)}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-th-text-s hover:text-th-text hover:bg-th-hover transition-all duration-200"
>
<Palette size={18} />
{t('nav.changeTheme')}
</button>
</div>
</nav>
{/* User info */}
<div className="p-4 border-t border-th-border">
<div className="flex items-center gap-3">
<div
className="w-9 h-9 rounded-full flex items-center justify-center text-white text-sm font-bold flex-shrink-0 overflow-hidden"
style={{ backgroundColor: user?.avatar_color || '#6366f1' }}
>
{user?.avatar_image ? (
<img
src={`${api.defaults.baseURL}/auth/avatar/${user.avatar_image}`}
alt="Avatar"
className="w-full h-full object-cover"
/>
) : (
(user?.display_name || user?.name)?.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) || '?'
)}
</div>
<div className="min-w-0">
<p className="text-sm font-medium text-th-text truncate">{user?.display_name || user?.name}</p>
<p className="text-xs text-th-text-s truncate">@{user?.name}</p>
</div>
</div>
{/* Imprint / Privacy Policy links */}
{(imprintUrl || privacyUrl) && (
<div className="flex items-center gap-3 mt-3 pt-3 border-t border-th-border/60">
{imprintUrl && (
<a
href={imprintUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-xs text-th-text-s hover:text-th-accent transition-colors"
>
<FileText size={11} />
{t('nav.imprint')}
</a>
)}
{imprintUrl && privacyUrl && (
<span className="text-th-border text-xs">·</span>
)}
{privacyUrl && (
<a
href={privacyUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-xs text-th-text-s hover:text-th-accent transition-colors"
>
<Lock size={11} />
{t('nav.privacy')}
</a>
)}
</div>
)}
</div>
</div>
</aside>
{/* Theme Selector Modal */}
{themeOpen && <ThemeSelector onClose={() => setThemeOpen(false)} />}
</>
);
}