All job seeker pages are already connected to real APIs:
- Jobs: /api/jobseeker/jobs (real company job postings)
- Applications: /api/jobseeker/applications (my applied jobs)
- Saved Jobs: Custom data storage for bookmarked jobs
- Apply: POST /api/jobseeker/jobs/{id}/apply
Dashboard shows real data from backend, not mock preview.
298 lines
10 KiB
TypeScript
298 lines
10 KiB
TypeScript
/**
|
|
* 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;
|