Session 2026-05-28: profile page overhaul, nav fixes, dashboard activity links
- Fix nav links not working from profile page (useEffect infinite re-render via unstable profile object ref)
- Fix nav hover/active: gold icon highlight, no background change; active links non-clickable
- Fix hover layout shift: add border: 1px solid transparent to all interactive elements
- Header icon buttons (search, theme toggle) now highlight gold on hover
- Profile page: replace calendar with activity feed (60/40 grid), add stat cards (tasks completed, active projects, revision requests, submissions)
- Profile card: title field, icon rows for location/email/linkedin, member since + role bottom-right, edit button top-right
- Profile portrait: remove wrapper column, fix left-gap alignment
- Add profiles.title migration
- Dashboard recent activity: name → /profile/{id}, task → /requests/{id} (clickable links)
- Icon-only sidebar with gold active/hover state, pointer-events: none on active links
- layout.md updated with profile page geometry rules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+186
-95
@@ -1,24 +1,42 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { NavLink, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const ICONS = {
|
||||
dashboard: <svg viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="6" height="6" rx="1.5"/><rect x="9" y="1" width="6" height="6" rx="1.5"/><rect x="1" y="9" width="6" height="6" rx="1.5"/><rect x="9" y="9" width="6" height="6" rx="1.5"/></svg>,
|
||||
requests: <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><rect x="2" y="2" width="12" height="12" rx="1.5"/><line x1="5" y1="5.5" x2="11" y2="5.5"/><line x1="5" y1="8" x2="11" y2="8"/><line x1="5" y1="10.5" x2="8" y2="10.5"/></svg>,
|
||||
projects: <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M1.5 5.5C1.5 4.67 2.17 4 3 4h2.5l1.5 2H13c.83 0 1.5.67 1.5 1.5v5.5c0 .83-.67 1.5-1.5 1.5H3c-.83 0-1.5-.67-1.5-1.5V5.5z"/></svg>,
|
||||
fileSharing: <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M9 2H4a1.5 1.5 0 00-1.5 1.5v9A1.5 1.5 0 004 14h8a1.5 1.5 0 001.5-1.5V6L9 2z"/><polyline points="9,2 9,6 13.5,6"/></svg>,
|
||||
invoices: <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><rect x="2" y="1.5" width="12" height="13" rx="1"/><line x1="8" y1="4" x2="8" y2="12"/><path d="M10 5.5H7a1.5 1.5 0 000 3h2a1.5 1.5 0 010 3H5.5"/></svg>,
|
||||
notes: <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><rect x="3" y="1.5" width="10" height="13" rx="1"/><line x1="5.5" y1="5" x2="10.5" y2="5"/><line x1="5.5" y1="7.5" x2="10.5" y2="7.5"/><line x1="5.5" y1="10" x2="8.5" y2="10"/></svg>,
|
||||
survey: <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="1.5" y="9.5" width="3" height="5" rx="0.5"/><rect x="6.5" y="6" width="3" height="8.5" rx="0.5"/><rect x="11.5" y="2.5" width="3" height="12" rx="0.5"/></svg>,
|
||||
brandBook: <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M2.5 2.5h4a2 2 0 012 2v9a2 2 0 00-2-2h-4V2.5z"/><path d="M13.5 2.5h-4a2 2 0 00-2 2v9a2 2 0 012-2h4V2.5z"/></svg>,
|
||||
converter: <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><circle cx="5.5" cy="5.5" r="1.5"/><path d="M1.5 11.5l3.5-3.5 3 3L11 7.5l3 3"/></svg>,
|
||||
passwords: <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="7" width="10" height="7.5" rx="1"/><path d="M5 7V5.5a3 3 0 016 0V7"/><circle cx="8" cy="10.5" r="1" fill="currentColor" stroke="none"/></svg>,
|
||||
companies: <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="1.5" y="5" width="13" height="9.5" rx="1"/><path d="M5.5 5V3a1 1 0 011-1h3a1 1 0 011 1v2"/><line x1="8" y1="5" x2="8" y2="14.5"/><line x1="1.5" y1="9" x2="14.5" y2="9"/></svg>,
|
||||
users: <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><circle cx="6" cy="5" r="2.5"/><path d="M1 14c0-2.76 2.24-5 5-5s5 2.24 5 5"/><circle cx="12.5" cy="5.5" r="2"/><path d="M12.5 10c1.93 0 3.5 1.57 3.5 3.5"/></svg>,
|
||||
};
|
||||
|
||||
function NI({ icon }) {
|
||||
return <span className="nav-icon">{icon}</span>;
|
||||
}
|
||||
|
||||
function TeamNav({ onNav }) {
|
||||
const location = useLocation();
|
||||
|
||||
const primaryLinks = [
|
||||
{ to: '/dashboard', label: 'Dashboard' },
|
||||
{ to: '/projects', label: 'Projects' },
|
||||
{ to: '/requests', label: 'Requests' },
|
||||
{ to: '/file-sharing', label: 'File Sharing' },
|
||||
{ to: '/team/dashboard', label: 'Dashboard', icon: ICONS.dashboard },
|
||||
{ to: '/requests', label: 'Requests', icon: ICONS.requests },
|
||||
{ to: '/projects', label: 'Projects', icon: ICONS.projects },
|
||||
{ to: '/invoices', label: 'Finances', icon: ICONS.invoices },
|
||||
{ to: '/file-sharing', label: 'File Sharing', icon: ICONS.fileSharing },
|
||||
];
|
||||
|
||||
const utilityLinks = [
|
||||
{ to: '/meeting-notes', label: 'Meeting Notes' },
|
||||
{ to: '/invoices', label: 'Invoices & Expenses' },
|
||||
{ to: '/survey-maker', label: 'Survey Maker' },
|
||||
{ to: '/brand-book', label: 'Brand Book Maker' },
|
||||
{ to: '/converters', label: 'Image Converter' },
|
||||
{ to: '/fourge-passwords', label: 'Fourge Passwords' },
|
||||
{ to: '/survey-maker', label: 'Survey Maker', icon: ICONS.survey },
|
||||
{ to: '/brand-book', label: 'Brand Book Maker', icon: ICONS.brandBook },
|
||||
{ to: '/converters', label: 'Image Converter', icon: ICONS.converter },
|
||||
{ to: '/fourge-passwords', label: 'Fourge Passwords', icon: ICONS.passwords },
|
||||
];
|
||||
|
||||
const isCompaniesActive = location.pathname === '/company' && !location.search.includes('tab=users');
|
||||
@@ -26,51 +44,44 @@ function TeamNav({ onNav }) {
|
||||
|
||||
return (
|
||||
<div className="sidebar-section">
|
||||
{primaryLinks.map(({ to, label }) => (
|
||||
{primaryLinks.map(({ to, label, icon }) => (
|
||||
<NavLink key={to} to={to} onClick={onNav} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||
{label}
|
||||
<NI icon={icon} /><span className="nav-label">{label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
<div style={{ height: 1, margin: '10px 12px', background: 'var(--border)' }} />
|
||||
<div style={{ padding: '0 12px 8px', fontSize: 11, fontWeight: 700, letterSpacing: 0.8, textTransform: 'uppercase', color: 'var(--text-muted)' }}>
|
||||
Team Tools
|
||||
<div className="sidebar-tools-divider" style={{ height: 1, margin: '10px 12px', background: 'var(--border)' }} />
|
||||
<div className="sidebar-tools-label" style={{ padding: '0 12px 8px', fontSize: 11, fontWeight: 400, letterSpacing: 0.8, textTransform: 'uppercase', color: 'var(--text-muted)' }}>
|
||||
Tools
|
||||
</div>
|
||||
{utilityLinks.map(({ to, label }) => (
|
||||
{utilityLinks.map(({ to, label, icon }) => (
|
||||
<NavLink key={to} to={to} onClick={onNav} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||
{label}
|
||||
<NI icon={icon} /><span className="nav-label">{label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
<NavLink to="/company" onClick={onNav} className={() => `sidebar-link${isCompaniesActive ? ' active' : ''}`}>
|
||||
Companies
|
||||
<NI icon={ICONS.companies} /><span className="nav-label">Companies</span>
|
||||
</NavLink>
|
||||
<NavLink to="/company?tab=users" onClick={onNav} className={() => `sidebar-link${isUsersActive ? ' active' : ''}`}>
|
||||
Users
|
||||
<NI icon={ICONS.users} /><span className="nav-label">Users</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ClientNav({ onNav }) {
|
||||
const { currentUser } = useAuth();
|
||||
const companies = currentUser?.companies?.length
|
||||
? currentUser.companies
|
||||
: currentUser?.company ? [currentUser.company] : [];
|
||||
const companyTo = companies.length === 1 ? `/company/${companies[0].id}` : '/company';
|
||||
|
||||
const links = [
|
||||
{ to: '/dashboard', label: 'Dashboard' },
|
||||
{ to: '/projects', label: 'Projects' },
|
||||
{ to: '/requests', label: 'Requests' },
|
||||
{ to: '/file-sharing', label: 'File Sharing' },
|
||||
{ to: '/my-invoices', label: 'Invoices' },
|
||||
{ to: companyTo, label: 'Company' },
|
||||
{ to: '/dashboard', label: 'Dashboard', icon: ICONS.dashboard },
|
||||
{ to: '/requests', label: 'Requests', icon: ICONS.requests },
|
||||
{ to: '/projects', label: 'Projects', icon: ICONS.projects },
|
||||
{ to: '/file-sharing', label: 'File Sharing', icon: ICONS.fileSharing },
|
||||
{ to: '/my-invoices', label: 'Invoices', icon: ICONS.invoices },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="sidebar-section">
|
||||
{links.map(({ to, label }) => (
|
||||
{links.map(({ to, label, icon }) => (
|
||||
<NavLink key={label} to={to} onClick={onNav} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||
{label}
|
||||
<NI icon={icon} /><span className="nav-label">{label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
@@ -79,30 +90,30 @@ function ClientNav({ onNav }) {
|
||||
|
||||
function ExternalNav({ onNav }) {
|
||||
const links = [
|
||||
{ to: '/dashboard', label: 'Dashboard' },
|
||||
{ to: '/requests', label: 'Requests' },
|
||||
{ to: '/projects', label: 'Projects' },
|
||||
{ to: '/my-invoices-sub', label: 'Invoices' },
|
||||
{ to: '/file-sharing', label: 'File Sharing' },
|
||||
{ to: '/survey-maker', label: 'Survey Maker' },
|
||||
{ to: '/brand-book', label: 'Brand Book Maker' },
|
||||
{ to: '/converters', label: 'Image Converter' },
|
||||
{ to: '/dashboard', label: 'Dashboard', icon: ICONS.dashboard },
|
||||
{ to: '/requests', label: 'Requests', icon: ICONS.requests },
|
||||
{ to: '/projects', label: 'Projects', icon: ICONS.projects },
|
||||
{ to: '/my-invoices-sub', label: 'Invoices', icon: ICONS.invoices },
|
||||
{ to: '/file-sharing', label: 'File Sharing', icon: ICONS.fileSharing },
|
||||
{ to: '/survey-maker', label: 'Survey Maker', icon: ICONS.survey },
|
||||
{ to: '/brand-book', label: 'Brand Book Maker', icon: ICONS.brandBook },
|
||||
{ to: '/converters', label: 'Image Converter', icon: ICONS.converter },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="sidebar-section">
|
||||
{links.map(({ to, label }, index) => (
|
||||
{links.map(({ to, label, icon }, index) => (
|
||||
<div key={to}>
|
||||
{index === 4 && (
|
||||
<>
|
||||
<div style={{ height: 1, margin: '10px 12px', background: 'var(--border)' }} />
|
||||
<div style={{ padding: '0 12px 8px', fontSize: 11, fontWeight: 700, letterSpacing: 0.8, textTransform: 'uppercase', color: 'var(--text-muted)' }}>
|
||||
Team Tools
|
||||
<div className="sidebar-tools-divider" style={{ height: 1, margin: '10px 12px', background: 'var(--border)' }} />
|
||||
<div className="sidebar-tools-label" style={{ padding: '0 12px 8px', fontSize: 11, fontWeight: 400, letterSpacing: 0.8, textTransform: 'uppercase', color: 'var(--text-muted)' }}>
|
||||
Tools
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<NavLink to={to} onClick={onNav} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||
{label}
|
||||
<NI icon={icon} /><span className="nav-label">{label}</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
))}
|
||||
@@ -116,7 +127,22 @@ export default function Layout({ children }) {
|
||||
const location = useLocation();
|
||||
const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'dark');
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem('sidebarCollapsed') === 'true');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState({ projects: [], tasks: [] });
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [searchExpanded, setSearchExpanded] = useState(false);
|
||||
const searchInputRef = useRef(null);
|
||||
const [avatarOpen, setAvatarOpen] = useState(false);
|
||||
const avatarRef = useRef(null);
|
||||
|
||||
const hour = new Date().getHours();
|
||||
const timeOfDay = hour < 12 ? 'morning' : hour < 17 ? 'afternoon' : 'evening';
|
||||
const firstName = currentUser?.name?.split(' ')[0] || '';
|
||||
const isProfileRoute = location.pathname === '/profile' || location.pathname.startsWith('/profile/');
|
||||
const headerTitle = isProfileRoute ? 'Profile' : `Good ${timeOfDay}${firstName ? `, ${firstName}` : ''}`;
|
||||
const headerSubtitle = isProfileRoute
|
||||
? 'Account details and security settings.'
|
||||
: "Here's what's happening today.";
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
@@ -124,81 +150,146 @@ export default function Layout({ children }) {
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('sidebarCollapsed', String(sidebarCollapsed));
|
||||
}, [sidebarCollapsed]);
|
||||
if (!searchQuery.trim()) { setSearchResults({ projects: [], tasks: [] }); return; }
|
||||
const t = setTimeout(async () => {
|
||||
const { supabase } = await import('../lib/supabase');
|
||||
const [{ data: projects }, { data: tasks }] = await Promise.all([
|
||||
supabase.from('projects').select('id, name').ilike('name', `%${searchQuery}%`).limit(6),
|
||||
supabase.from('tasks').select('id, title').ilike('title', `%${searchQuery}%`).limit(6),
|
||||
]);
|
||||
setSearchResults({ projects: projects || [], tasks: tasks || [] });
|
||||
}, 280);
|
||||
return () => clearTimeout(t);
|
||||
}, [searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!avatarOpen) return;
|
||||
function handler(e) {
|
||||
if (avatarRef.current && !avatarRef.current.contains(e.target)) setAvatarOpen(false);
|
||||
}
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [avatarOpen]);
|
||||
|
||||
// Close menu on route change (derived-state pattern, no effect needed)
|
||||
const [lastPathname, setLastPathname] = useState(location.pathname);
|
||||
if (lastPathname !== location.pathname) {
|
||||
setLastPathname(location.pathname);
|
||||
setMenuOpen(false);
|
||||
setAvatarOpen(false);
|
||||
}
|
||||
|
||||
const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark');
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
navigate('/');
|
||||
};
|
||||
const handleLogout = async () => { await logout(); navigate('/'); };
|
||||
|
||||
const initials = currentUser?.name
|
||||
?.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
||||
const hasResults = searchResults.projects.length > 0 || searchResults.tasks.length > 0;
|
||||
|
||||
return (
|
||||
<div className={`app-layout${sidebarCollapsed ? ' sidebar-collapsed' : ''}`}>
|
||||
{/* Overlay */}
|
||||
<div className="app-layout">
|
||||
{menuOpen && <div className="sidebar-overlay" onClick={() => setMenuOpen(false)} />}
|
||||
|
||||
<aside className={`sidebar${menuOpen ? ' sidebar-open' : ''}`}>
|
||||
<div className="sidebar-logo">
|
||||
<img className="brand-logo brand-logo-sidebar" src="/fourge-logo.png" alt="Fourge Branding" />
|
||||
<button
|
||||
className="sidebar-pin-toggle"
|
||||
onClick={() => setSidebarCollapsed(current => !current)}
|
||||
title={sidebarCollapsed ? 'Pin sidebar open' : 'Collapse sidebar'}
|
||||
aria-label={sidebarCollapsed ? 'Pin sidebar open' : 'Collapse sidebar'}
|
||||
>
|
||||
{sidebarCollapsed ? '›' : '‹'}
|
||||
</button>
|
||||
<img className="brand-logo brand-logo-sidebar" src="/logonowordmark.png" alt="Fourge Branding" />
|
||||
</div>
|
||||
|
||||
{!sidebarCollapsed && (
|
||||
currentUser?.role === 'team'
|
||||
? <TeamNav onNav={() => setMenuOpen(false)} />
|
||||
: currentUser?.role === 'external'
|
||||
? <ExternalNav onNav={() => setMenuOpen(false)} />
|
||||
: <ClientNav onNav={() => setMenuOpen(false)} />
|
||||
)}
|
||||
{currentUser?.role === 'team'
|
||||
? <TeamNav onNav={() => setMenuOpen(false)} />
|
||||
: currentUser?.role === 'external'
|
||||
? <ExternalNav onNav={() => setMenuOpen(false)} />
|
||||
: <ClientNav onNav={() => setMenuOpen(false)} />
|
||||
}
|
||||
|
||||
<div className="sidebar-bottom">
|
||||
<NavLink to="/settings" onClick={() => setMenuOpen(false)} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||
<div className="sidebar-avatar" style={{ width: 28, height: 28, fontSize: 11, flexShrink: 0 }}>{initials}</div>
|
||||
<div className="sidebar-user-info">
|
||||
<div className="sidebar-user-name">{currentUser?.name || 'Set your name'}</div>
|
||||
<div className="sidebar-user-role">{currentUser?.role === 'external' ? 'Team' : currentUser?.role}</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
<div className="sidebar-bottom-actions">
|
||||
{!sidebarCollapsed && <button className="sidebar-link" style={{ flex: 1 }} onClick={handleLogout}>Sign Out</button>}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="sidebar-theme-toggle"
|
||||
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{theme === 'dark' ? '☀' : '☾'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div className="main-wrapper">
|
||||
{/* Mobile top bar inside main wrapper so it sits at the top */}
|
||||
<div className="mobile-topbar">
|
||||
<button className="hamburger" onClick={() => setMenuOpen(o => !o)} aria-label="Menu">
|
||||
<span /><span /><span />
|
||||
</button>
|
||||
<img className="brand-logo brand-logo-mobile" src="/fourge-logo.png" alt="Fourge Branding" />
|
||||
</div>
|
||||
|
||||
<main className="main-content">
|
||||
<div className="site-header">
|
||||
<div>
|
||||
<div className="site-header-greeting">{headerTitle}</div>
|
||||
<div className="site-header-sub">{headerSubtitle}</div>
|
||||
</div>
|
||||
<div className="site-header-right">
|
||||
<div className="site-header-search-wrap">
|
||||
{!searchExpanded ? (
|
||||
<button
|
||||
className="site-header-search-btn"
|
||||
onClick={() => { setSearchExpanded(true); setTimeout(() => searchInputRef.current?.focus(), 50); }}
|
||||
aria-label="Search"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><circle cx="6.5" cy="6.5" r="4"/><line x1="10" y1="10" x2="14" y2="14"/></svg>
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
className="site-header-search"
|
||||
type="text"
|
||||
placeholder="Search projects & tasks..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
onFocus={() => setSearchOpen(true)}
|
||||
onBlur={() => setTimeout(() => { setSearchOpen(false); setSearchExpanded(false); setSearchQuery(''); }, 150)}
|
||||
onKeyDown={e => { if (e.key === 'Escape') { setSearchOpen(false); setSearchExpanded(false); setSearchQuery(''); } }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{searchOpen && searchQuery.trim() && (
|
||||
<div className="site-header-dropdown">
|
||||
{!hasResults && <div className="site-header-dropdown-empty">No results for "{searchQuery}"</div>}
|
||||
{searchResults.projects.length > 0 && (
|
||||
<>
|
||||
<div className="site-header-dropdown-group">Projects</div>
|
||||
{searchResults.projects.map(p => (
|
||||
<div key={p.id} className="site-header-dropdown-item" onMouseDown={() => { navigate(`/projects/${p.id}`); setSearchQuery(''); setSearchOpen(false); setSearchExpanded(false); }}>
|
||||
<span className="nav-icon" style={{ opacity: 0.6 }}>{ICONS.projects}</span>
|
||||
{p.name}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{searchResults.tasks.length > 0 && (
|
||||
<>
|
||||
<div className="site-header-dropdown-group">Tasks</div>
|
||||
{searchResults.tasks.map(r => (
|
||||
<div key={r.id} className="site-header-dropdown-item" onMouseDown={() => { navigate(`/requests/${r.id}`); setSearchQuery(''); setSearchOpen(false); setSearchExpanded(false); }}>
|
||||
<span className="nav-icon" style={{ opacity: 0.6 }}>{ICONS.requests}</span>
|
||||
{r.title}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button onClick={toggleTheme} className="site-header-theme-toggle" title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}>
|
||||
{theme === 'dark'
|
||||
? <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="8" cy="8" r="3"/><line x1="8" y1="1" x2="8" y2="2.5"/><line x1="8" y1="13.5" x2="8" y2="15"/><line x1="1" y1="8" x2="2.5" y2="8"/><line x1="13.5" y1="8" x2="15" y2="8"/><line x1="3" y1="3" x2="4.1" y2="4.1"/><line x1="11.9" y1="11.9" x2="13" y2="13"/><line x1="13" y1="3" x2="11.9" y2="4.1"/><line x1="4.1" y1="11.9" x2="3" y2="13"/></svg>
|
||||
: <svg viewBox="0 0 16 16" fill="currentColor" stroke="none"><path d="M14 8.53A6 6 0 1 1 7.47 2 4.67 4.67 0 0 0 14 8.53Z"/></svg>
|
||||
}
|
||||
</button>
|
||||
|
||||
<div className="site-header-avatar-wrap" ref={avatarRef}>
|
||||
<button className="site-header-avatar-btn" onClick={() => setAvatarOpen(o => !o)} aria-label="Account menu">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4.4 3.6-7 8-7s8 2.6 8 7" fill="currentColor"/></svg>
|
||||
</button>
|
||||
{avatarOpen && (
|
||||
<div className="site-header-avatar-menu">
|
||||
<button className="site-header-avatar-item" onClick={() => { navigate('/profile'); setAvatarOpen(false); }}>Settings</button>
|
||||
<div className="site-header-avatar-divider" />
|
||||
<button className="site-header-avatar-item" onClick={handleLogout}>Sign Out</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user