nxtgauge-frontend-solid/src/components/DashboardShell.tsx

299 lines
10 KiB
TypeScript
Raw Normal View History

/**
* DashboardShell real sidebar + header layout wrapper.
* Used for pages that need actual backend connectivity
* (My Profile, My Portfolio, Verification) instead of the preview mock.
*/
import { For, JSX, Show, createMemo, createSignal, onMount } from 'solid-js';
import {
User, Briefcase, LayoutDashboard, FolderOpen, MapPin, Star,
CreditCard, Globe, ShieldCheck, HelpCircle, Settings,
RefreshCw, LogOut, Bell, ChevronRight,
} from 'lucide-solid';
// ── Icon map (matches DashboardDesignPreview sidebar keys) ────────────────────
const ICON_MAP: Record<string, any> = {
'my dashboard': LayoutDashboard,
'my profile': User,
'my portfolio': FolderOpen,
'leads': MapPin,
'my responses': Star,
'credits': CreditCard,
'explore nxtgauge': Globe,
'verification': ShieldCheck,
'help center': HelpCircle,
'settings': Settings,
'switch services': RefreshCw,
'jobs': Briefcase,
'applications': Briefcase,
'shortlisted candidates': User,
'my applications': FolderOpen,
'saved jobs': Star,
'my requirements': FolderOpen,
'received responses': Bell,
'shortlisted responses': Star,
'logout': LogOut,
};
function SidebarIcon(props: { label: string }) {
const key = props.label.toLowerCase();
const Icon = ICON_MAP[key] || ChevronRight;
return <Icon size={16} />;
}
function titleCase(value: string) {
return String(value || '')
.toLowerCase()
.replace(/_/g, ' ')
.replace(/\b\w/g, (c) => c.toUpperCase());
}
// ── Types ─────────────────────────────────────────────────────────────────────
interface Props {
sidebarItems: string[];
activeSidebar: string;
onSidebarSelect: (item: string) => void;
roleKey: string;
userName?: string;
children: JSX.Element;
}
// ── Brand colours ─────────────────────────────────────────────────────────────
const ORANGE = '#FF5E13';
const NAVY = '#0D0D2A';
// ── Component ─────────────────────────────────────────────────────────────────
export default function DashboardShell(props: Props) {
const roleLabel = createMemo(() => {
const k = String(props.roleKey || '').replace(/_/g, ' ');
return k.charAt(0).toUpperCase() + k.slice(1).toLowerCase();
});
const [unreadCount, setUnreadCount] = createSignal(0);
// Fetch unread notification count
const fetchUnreadCount = async () => {
try {
const token = typeof window !== 'undefined' ? window.sessionStorage.getItem('nxtgauge_access_token') || '' : '';
if (!token) return;
const res = await fetch('/api/me/notifications/unread-count', {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
});
if (res.ok) {
const data = await res.json();
setUnreadCount(data.unread_count || 0);
}
} catch (e) {
console.error('Failed to fetch unread count:', e);
}
};
// Start polling on mount
onMount(() => {
fetchUnreadCount();
const interval = setInterval(fetchUnreadCount, 30000);
return () => clearInterval(interval);
});
return (
<div style={{
display: 'flex',
'min-height': '100vh',
background: '#F8FAFC',
'font-family': "'Exo 2', sans-serif",
}}>
{/* ── Sidebar ──────────────────────────────────────────────────────── */}
<aside style={{
width: '220px',
'flex-shrink': '0',
background: '#FFFFFF',
display: 'flex',
'flex-direction': 'column',
'padding': '0',
'min-height': '100vh',
position: 'sticky',
top: '0',
height: '100vh',
'overflow-y': 'auto',
}}>
{/* Logo */}
<div style={{ padding: '20px 16px 12px', 'border-bottom': '1px solid #E5E7EB' }}>
<img src="/nxtgauge-logo.png" alt="Nxtgauge" style={{ height: '40px', 'object-fit': 'contain', 'max-width': '170px' }} />
</div>
{/* Role badge */}
<div style={{ padding: '10px 16px', 'border-bottom': '1px solid #E5E7EB' }}>
<p style={{ margin: '0', 'font-size': '10px', 'letter-spacing': '0.08em', 'text-transform': 'uppercase', color: '#6B7280' }}>Active Role</p>
<p style={{ margin: '2px 0 0', 'font-size': '12px', 'font-weight': '700', color: '#111827' }}>{roleLabel()}</p>
</div>
{/* Nav items */}
<nav style={{ flex: '1', padding: '8px 8px' }}>
<For each={props.sidebarItems}>
{(item) => {
const isActive = () => item.toLowerCase() === props.activeSidebar.toLowerCase();
const isLogout = item.toLowerCase() === 'logout';
return (
<button
type="button"
onClick={() => props.onSidebarSelect(item)}
style={{
display: 'flex',
'align-items': 'center',
gap: '9px',
width: '100%',
'text-align': 'left',
height: '34px',
padding: '0 10px',
'border-radius': '8px',
border: 'none',
cursor: 'pointer',
'font-size': '12px',
'font-weight': '600',
'margin-bottom': '4px',
background: isActive() ? '#FFF3EE' : 'transparent',
color: isActive() ? ORANGE : isLogout ? '#DC2626' : '#6B7280',
transition: 'background 0.15s, color 0.15s',
}}
>
<span style={{ 'flex-shrink': '0', color: isActive() ? ORANGE : '#9CA3AF' }}>
<SidebarIcon label={item} />
</span>
{titleCase(item)}
</button>
);
}}
</For>
</nav>
{/* User footer */}
<div style={{ padding: '12px 16px', 'border-top': '1px solid #E5E7EB' }}>
<p style={{ margin: '0', 'font-size': '12px', 'font-weight': '600', color: '#374151', overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap' }}>
{props.userName || 'User'}
</p>
</div>
</aside>
{/* ── Main content ─────────────────────────────────────────────────── */}
<div style={{ flex: '1', display: 'flex', 'flex-direction': 'column', 'min-width': '0' }}>
{/* Top bar */}
<header style={{
height: '56px',
background: '#fff',
'border-bottom': '1px solid #E5E7EB',
display: 'flex',
'align-items': 'center',
'justify-content': 'space-between',
padding: '0 24px',
'flex-shrink': '0',
}}>
<p style={{ margin: '0', 'font-size': '15px', 'font-weight': '700', color: NAVY }}>
{titleCase(props.activeSidebar)}
</p>
<div style={{ display: 'flex', 'align-items': 'center', gap: '12px' }}>
<button type="button" style={{ position: 'relative', border: 'none', background: 'transparent', cursor: 'pointer', display: 'flex', 'align-items': 'center', 'justify-content': 'center', padding: 0 }}>
<Bell size={18} style={{ color: '#9CA3AF' }} />
<Show when={unreadCount() > 0}>
<span style={{
position: 'absolute',
top: '-2px',
right: '-2px',
width: '8px',
height: '8px',
background: '#FF5E13',
'border-radius': '50%',
border: '1px solid white'
}}></span>
</Show>
</button>
<div style={{
width: '32px', height: '32px', 'border-radius': '999px',
background: ORANGE, color: '#fff', display: 'flex',
'align-items': 'center', 'justify-content': 'center',
'font-size': '13px', 'font-weight': '700',
}}>
{(props.userName || 'U').charAt(0).toUpperCase()}
</div>
</div>
</header>
{/* Page content */}
<main style={{ flex: '1', padding: '24px', 'overflow-y': 'auto' }}>
{props.children}
</main>
</div>
</div>
);
}
// ── Shared UI primitives ──────────────────────────────────────────────────────
export const CARD = {
background: '#fff',
border: '1px solid #E5E7EB',
'border-radius': '14px',
padding: '20px',
'box-shadow': '0 1px 4px rgba(0,0,0,0.06)',
} as const;
export const BTN_PRIMARY = {
height: '38px',
'border-radius': '10px',
border: 'none',
background: NAVY,
color: '#fff',
padding: '0 18px',
'font-size': '13px',
'font-weight': '700',
cursor: 'pointer',
} as const;
export const BTN_ORANGE = {
height: '38px',
'border-radius': '10px',
border: 'none',
background: ORANGE,
color: '#fff',
padding: '0 18px',
'font-size': '13px',
'font-weight': '700',
cursor: 'pointer',
} as const;
export const BTN_GHOST = {
height: '38px',
'border-radius': '10px',
border: '1px solid #E5E7EB',
background: '#fff',
color: '#374151',
padding: '0 18px',
'font-size': '13px',
'font-weight': '600',
cursor: 'pointer',
} as const;
export const INPUT = {
height: '40px',
width: '100%',
'border-radius': '8px',
border: '1px solid #E5E7EB',
padding: '0 12px',
'font-size': '14px',
color: '#111827',
background: '#fff',
'box-sizing': 'border-box',
} as const;
export const LABEL = {
display: 'block',
'font-size': '12px',
'font-weight': '600',
color: '#374151',
'margin-bottom': '6px',
} as const;