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

396 lines
12 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 } from "solid-js";
import { AiChatWidget } from "./AiChatWidget";
import NotificationBell from "./NotificationBell";
import {
User,
Briefcase,
LayoutDashboard,
FolderOpen,
MapPin,
Star,
CreditCard,
Globe,
ShieldCheck,
HelpCircle,
Settings,
RefreshCw,
LogOut,
Bell,
ChevronRight,
Lock,
} from "lucide-solid";
const ICON_MAP: Record<string, any> = {
2026-04-26 23:58:43 +02:00
my_dashboard: LayoutDashboard,
my_profile: User,
my_portfolio: FolderOpen,
portfolio: FolderOpen,
leads: MapPin,
2026-04-26 23:58:43 +02:00
my_responses: Star,
responses: Star,
credits: CreditCard,
2026-04-26 23:58:43 +02:00
explore_nxtgauge: Globe,
explore: Globe,
verification: ShieldCheck,
2026-04-26 23:58:43 +02:00
verification_status: ShieldCheck,
help_center: HelpCircle,
help: HelpCircle,
support: HelpCircle,
settings: Settings,
2026-04-26 23:58:43 +02:00
switch_services: RefreshCw,
switch_service: RefreshCw,
logout: LogOut,
jobs: Briefcase,
2026-04-26 23:58:43 +02:00
job_postings: Briefcase,
applications: Briefcase,
2026-04-26 23:58:43 +02:00
my_applications: FolderOpen,
shortlisted_candidates: User,
my_requirements: FolderOpen,
requirements: FolderOpen,
received_responses: Bell,
shortlisted_responses: Star,
saved_jobs: Star,
};
2026-04-26 23:58:43 +02:00
function normalizeSidebarIconKey(value: string): string {
return String(value || "")
.trim()
.toLowerCase()
.replace(/\s+/g, "_")
.replace(/-/g, "_");
}
function SidebarIcon(props: { label: string }) {
2026-04-26 23:58:43 +02:00
const key = normalizeSidebarIconKey(props.label);
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;
isAdmin?: boolean;
/** Shows a checkmark on the Verification nav item once the profile is approved. */
verificationApproved?: boolean;
children: JSX.Element;
}
// ── Brand colours ─────────────────────────────────────────────────────────────
2026-04-26 23:58:43 +02:00
export const ORANGE = "#FF5E13";
export const NAVY = "#0D0D2A";
const DARK_INK = "#03004E";
// ── 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();
});
return (
<div
style={{
display: "flex",
2026-08-12 13:38:38 +02:00
// height:100vh (not min-height) keeps the layout bounded so the sidebar
// stays sticky and <main> scrolls independently — FE-004 fix.
height: "100vh",
overflow: "hidden",
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)}
<Show when={props.verificationApproved && item.toLowerCase() === "verification"}>
<span style={{ "margin-left": "auto", color: "#10B981", "font-weight": "700" }}></span>
</Show>
</button>
);
}}
</For>
</nav>
{/* Admin section (only for admins) */}
<Show when={props.isAdmin}>
<div style={{ padding: "8px", "border-top": "1px solid #E5E7EB", "margin-top": "auto" }}>
<p style={{ margin: "4px 8px", "font-size": "10px", "letter-spacing": "0.08em", "text-transform": "uppercase", color: "#9CA3AF" }}>
Admin Tools
</p>
<a
href="/admin/ai-credits"
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-top": "4px",
background: "transparent",
color: "#6B7280",
"text-decoration": "none",
}}
>
<span style={{ "flex-shrink": "0", color: "#9CA3AF" }}>
<Lock size={16} />
</span>
AI Credits Admin
</a>
</div>
</Show>
{/* 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 ─────────────────────────────────────────────────── */}
2026-08-12 13:38:38 +02:00
<div style={{ flex: "1", display: "flex", "flex-direction": "column", "min-width": "0", overflow: "hidden" }}>
{/* 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": "18px", "font-weight": "700", color: NAVY }}>
{titleCase(props.activeSidebar)}
</p>
<div style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<NotificationBell />
<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>
<AiChatWidget />
</div>
);
}
// ── Shared UI primitives ──────────────────────────────────────────────────────
export const CARD = {
background: "#fff",
border: "1px solid #E5E7EB",
"border-radius": "16px",
padding: "14px",
"box-shadow": "0 1px 4px rgba(0,0,0,0.06)",
} as const;
export const BTN_PRIMARY = {
height: "34px",
"border-radius": "8px",
border: "none",
background: NAVY,
color: "#fff",
padding: "0 14px",
"font-size": "12px",
"font-weight": "700",
cursor: "pointer",
} as const;
export const BTN_ORANGE = {
height: "34px",
"border-radius": "8px",
border: "none",
background: ORANGE,
color: "#fff",
padding: "0 14px",
"font-size": "12px",
"font-weight": "700",
cursor: "pointer",
} as const;
export const BTN_GHOST = {
height: "34px",
"border-radius": "8px",
border: "1px solid #E5E7EB",
background: "#fff",
color: "#374151",
padding: "0 14px",
"font-size": "12px",
"font-weight": "700",
cursor: "pointer",
} as const;
export const INPUT = {
height: "36px",
width: "100%",
"border-radius": "8px",
border: "1px solid #E5E7EB",
padding: "0 10px",
"font-size": "12px",
color: "#111827",
background: "#fff",
"box-sizing": "border-box",
outline: "none",
} as const;
export const LABEL = {
display: "block",
"font-size": "11px",
"font-weight": "700",
color: "#6B7280",
"margin-bottom": "6px",
"letter-spacing": "0.01em",
"text-transform": "none",
} as const;
2026-04-26 23:58:43 +02:00
export const PKG_CARD = {
WHITE: "#ffffff",
COIN_BG: "#FFFBF8",
COIN_AVATAR_BG: "linear-gradient(135deg, #FEF3C7 0%, #FDE68A 100%)",
BEST_VALUE_BG: `linear-gradient(135deg, ${ORANGE} 0%, #FF8A5C 100%)`,
BORDER_DEFAULT: "#E5E7EB",
TEXT_SECONDARY: "#6B7280",
TEXT_PRIMARY: "#111827",
TEXT_ACCENT: ORANGE,
TEXT_MUTED: "#9CA3AF",
TEXT_SUCCESS: "#16A34A",
SHADOW_DEFAULT: "0 1px 3px rgba(0, 0, 0, 0.08)",
SHADOW_ACCENT: "0 4px 20px rgba(255, 94, 19, 0.15)",
} as const;