feat: add floating AI chat widget to all pages
- Add AiChatWidget component with floating button in bottom-right - Routes to /api/ai/chat/message for chat, ticket, form, cover letter intents - Quick action buttons for common tasks - Appears on all public pages via app.tsx root layout - Appears on all dashboard pages via DashboardShell
This commit is contained in:
parent
6f550458e7
commit
fe81ce54c7
3 changed files with 602 additions and 175 deletions
39
src/app.tsx
39
src/app.tsx
|
|
@ -1,9 +1,10 @@
|
||||||
import { MetaProvider, Title } from '@solidjs/meta';
|
import { MetaProvider, Title } from "@solidjs/meta";
|
||||||
import { Router } from '@solidjs/router';
|
import { Router } from "@solidjs/router";
|
||||||
import { FileRoutes } from '@solidjs/start/router';
|
import { FileRoutes } from "@solidjs/start/router";
|
||||||
import { ErrorBoundary, Suspense } from 'solid-js';
|
import { ErrorBoundary, Suspense } from "solid-js";
|
||||||
import { AuthProvider } from '~/lib/auth';
|
import { AuthProvider } from "~/lib/auth";
|
||||||
import './app.css';
|
import { AiChatWidget } from "~/components/AiChatWidget";
|
||||||
|
import "./app.css";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -14,16 +15,34 @@ export default function App() {
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
fallback={(err) => (
|
fallback={(err) => (
|
||||||
<main style={{ padding: '24px', 'font-family': 'Inter, system-ui, sans-serif', color: '#111827', background: '#fff' }}>
|
<main
|
||||||
<h1 style={{ margin: 0, 'font-size': '20px' }}>Frontend Error</h1>
|
style={{
|
||||||
<p style={{ 'margin-top': '8px' }}>A runtime error occurred while rendering this page.</p>
|
padding: "24px",
|
||||||
<pre style={{ 'margin-top': '12px', padding: '12px', background: '#f3f4f6', 'border-radius': '8px', 'white-space': 'pre-wrap' }}>
|
"font-family": "Inter, system-ui, sans-serif",
|
||||||
|
color: "#111827",
|
||||||
|
background: "#fff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1 style={{ margin: 0, "font-size": "20px" }}>Frontend Error</h1>
|
||||||
|
<p style={{ "margin-top": "8px" }}>
|
||||||
|
A runtime error occurred while rendering this page.
|
||||||
|
</p>
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
"margin-top": "12px",
|
||||||
|
padding: "12px",
|
||||||
|
background: "#f3f4f6",
|
||||||
|
"border-radius": "8px",
|
||||||
|
"white-space": "pre-wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{String((err as any)?.message || err)}
|
{String((err as any)?.message || err)}
|
||||||
</pre>
|
</pre>
|
||||||
</main>
|
</main>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Suspense>{props.children}</Suspense>
|
<Suspense>{props.children}</Suspense>
|
||||||
|
<AiChatWidget />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</MetaProvider>
|
</MetaProvider>
|
||||||
|
|
|
||||||
333
src/components/AiChatWidget.tsx
Normal file
333
src/components/AiChatWidget.tsx
Normal file
|
|
@ -0,0 +1,333 @@
|
||||||
|
import { createSignal, Show, For, onMount } from "solid-js";
|
||||||
|
import { MessageCircle, X, Send, Bot, User, Loader } from "lucide-solid";
|
||||||
|
|
||||||
|
const API = "/api/gateway";
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
role: "user" | "assistant";
|
||||||
|
content: string;
|
||||||
|
intent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatResponse {
|
||||||
|
message: string;
|
||||||
|
conversation_id: string;
|
||||||
|
intent: string;
|
||||||
|
confidence: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AiChatWidget() {
|
||||||
|
const [isOpen, setIsOpen] = createSignal(false);
|
||||||
|
const [messages, setMessages] = createSignal<ChatMessage[]>([
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content:
|
||||||
|
"Hi! I'm your AI assistant. I can help you create support tickets, fill out forms, generate job descriptions, or write cover letters. What can I help you with?",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const [input, setInput] = createSignal("");
|
||||||
|
const [isLoading, setIsLoading] = createSignal(false);
|
||||||
|
const [conversationId, setConversationId] = createSignal("");
|
||||||
|
|
||||||
|
const toggleChat = () => setIsOpen((v) => !v);
|
||||||
|
|
||||||
|
const sendMessage = async () => {
|
||||||
|
const text = input().trim();
|
||||||
|
if (!text || isLoading()) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
const userMessage: ChatMessage = { role: "user", content: text };
|
||||||
|
setMessages((prev) => [...prev, userMessage]);
|
||||||
|
setInput("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}/api/ai/chat/message`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: text,
|
||||||
|
conversation_id: conversationId() || undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error("AI request failed");
|
||||||
|
|
||||||
|
const data: ChatResponse = await res.json();
|
||||||
|
if (data.conversation_id && !conversationId()) {
|
||||||
|
setConversationId(data.conversation_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const assistantMessage: ChatMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: data.message,
|
||||||
|
intent: data.intent,
|
||||||
|
};
|
||||||
|
setMessages((prev) => [...prev, assistantMessage]);
|
||||||
|
} catch (err) {
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content:
|
||||||
|
"I'm having trouble connecting right now. Please try again or contact support@nxtgauge.com.",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Floating button */}
|
||||||
|
<button
|
||||||
|
onClick={toggleChat}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "24px",
|
||||||
|
right: "24px",
|
||||||
|
width: "56px",
|
||||||
|
height: "56px",
|
||||||
|
"border-radius": "50%",
|
||||||
|
background: "#FF5E13",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-content": "center",
|
||||||
|
"box-shadow": "0 4px 16px rgba(255, 90, 19, 0.35)",
|
||||||
|
"z-index": "9999",
|
||||||
|
transition: "transform 0.2s",
|
||||||
|
}}
|
||||||
|
title="AI Assistant"
|
||||||
|
>
|
||||||
|
<Show when={isOpen()} fallback={<MessageCircle size={24} color="#fff" />}>
|
||||||
|
<X size={24} color="#fff" />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Chat window */}
|
||||||
|
<Show when={isOpen()}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "96px",
|
||||||
|
right: "24px",
|
||||||
|
width: "380px",
|
||||||
|
height: "520px",
|
||||||
|
background: "#fff",
|
||||||
|
"border-radius": "16px",
|
||||||
|
"box-shadow": "0 8px 40px rgba(0,0,0,0.15)",
|
||||||
|
display: "flex",
|
||||||
|
"flex-direction": "column",
|
||||||
|
"z-index": "9998",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #FF5E13 0%, #E5470F 100%)",
|
||||||
|
padding: "16px 20px",
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-content": "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", "align-items": "center", gap: "10px" }}>
|
||||||
|
<Bot size={22} color="#fff" />
|
||||||
|
<div>
|
||||||
|
<p style={{ margin: 0, color: "#fff", "font-weight": "700", "font-size": "15px" }}>
|
||||||
|
AI Assistant
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: 0, color: "rgba(255,255,255,0.8)", "font-size": "11px" }}>
|
||||||
|
Powered by gemma3
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={toggleChat}
|
||||||
|
style={{
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X size={20} color="#fff" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick actions */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "10px 16px",
|
||||||
|
"border-bottom": "1px solid #E5E7EB",
|
||||||
|
display: "flex",
|
||||||
|
gap: "8px",
|
||||||
|
"flex-wrap": "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{["Create Ticket", "Job Description", "Cover Letter", "Fill Form"].map((label) => (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setInput(`${label.toLowerCase()}: `);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: "4px 10px",
|
||||||
|
"border-radius": "20px",
|
||||||
|
border: "1px solid #E5E7EB",
|
||||||
|
background: "#F9FAFB",
|
||||||
|
"font-size": "11px",
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "#374151",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
overflow: "auto",
|
||||||
|
padding: "16px",
|
||||||
|
display: "flex",
|
||||||
|
"flex-direction": "column",
|
||||||
|
gap: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={messages()}>
|
||||||
|
{(msg) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "flex-start",
|
||||||
|
gap: "8px",
|
||||||
|
"flex-direction": msg.role === "user" ? "row-reverse" : "row",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "28px",
|
||||||
|
height: "28px",
|
||||||
|
"border-radius": "50%",
|
||||||
|
background: msg.role === "user" ? "#FF5E13" : "#E5E7EB",
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-content": "center",
|
||||||
|
"flex-shrink": 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show when={msg.role === "user"} fallback={<Bot size={14} color="#6B7280" />}>
|
||||||
|
<User size={14} color="#fff" />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
"max-width": "75%",
|
||||||
|
padding: "10px 14px",
|
||||||
|
"border-radius": "14px",
|
||||||
|
background: msg.role === "user" ? "#FF5E13" : "#F3F4F6",
|
||||||
|
color: msg.role === "user" ? "#fff" : "#111827",
|
||||||
|
"font-size": "13px",
|
||||||
|
"line-height": "1.5",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p style={{ margin: 0, "white-space": "pre-wrap" }}>{msg.content}</p>
|
||||||
|
<Show when={msg.intent && msg.role === "assistant"}>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "4px 0 0",
|
||||||
|
"font-size": "10px",
|
||||||
|
color: "#9CA3AF",
|
||||||
|
"font-style": "italic",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Intent: {msg.intent}
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<Show when={isLoading()}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
gap: "8px",
|
||||||
|
color: "#9CA3AF",
|
||||||
|
"font-size": "13px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Loader size={14} style={{ animation: "spin 1s linear infinite" }} />
|
||||||
|
Thinking...
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "12px 16px",
|
||||||
|
"border-top": "1px solid #E5E7EB",
|
||||||
|
display: "flex",
|
||||||
|
gap: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={input()}
|
||||||
|
onInput={(e) => setInput(e.currentTarget.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Ask me anything..."
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
height: "40px",
|
||||||
|
"border-radius": "20px",
|
||||||
|
border: "1px solid #E5E7EB",
|
||||||
|
padding: "0 16px",
|
||||||
|
"font-size": "13px",
|
||||||
|
outline: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={sendMessage}
|
||||||
|
disabled={isLoading() || !input().trim()}
|
||||||
|
style={{
|
||||||
|
width: "40px",
|
||||||
|
height: "40px",
|
||||||
|
"border-radius": "50%",
|
||||||
|
background: isLoading() ? "#E5E7EB" : "#FF5E13",
|
||||||
|
border: "none",
|
||||||
|
cursor: isLoading() ? "default" : "pointer",
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-content": "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Send size={16} color="#fff" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,36 +3,49 @@
|
||||||
* Used for pages that need actual backend connectivity
|
* Used for pages that need actual backend connectivity
|
||||||
* (My Profile, My Portfolio, Verification) instead of the preview mock.
|
* (My Profile, My Portfolio, Verification) instead of the preview mock.
|
||||||
*/
|
*/
|
||||||
import { For, JSX, Show, createMemo, createSignal, onMount } from 'solid-js';
|
import { For, JSX, Show, createMemo, createSignal, onMount } from "solid-js";
|
||||||
|
import { AiChatWidget } from "./AiChatWidget";
|
||||||
import {
|
import {
|
||||||
User, Briefcase, LayoutDashboard, FolderOpen, MapPin, Star,
|
User,
|
||||||
CreditCard, Globe, ShieldCheck, HelpCircle, Settings,
|
Briefcase,
|
||||||
RefreshCw, LogOut, Bell, ChevronRight,
|
LayoutDashboard,
|
||||||
} from 'lucide-solid';
|
FolderOpen,
|
||||||
|
MapPin,
|
||||||
|
Star,
|
||||||
|
CreditCard,
|
||||||
|
Globe,
|
||||||
|
ShieldCheck,
|
||||||
|
HelpCircle,
|
||||||
|
Settings,
|
||||||
|
RefreshCw,
|
||||||
|
LogOut,
|
||||||
|
Bell,
|
||||||
|
ChevronRight,
|
||||||
|
} from "lucide-solid";
|
||||||
|
|
||||||
// ── Icon map (matches DashboardDesignPreview sidebar keys) ────────────────────
|
// ── Icon map (matches DashboardDesignPreview sidebar keys) ────────────────────
|
||||||
|
|
||||||
const ICON_MAP: Record<string, any> = {
|
const ICON_MAP: Record<string, any> = {
|
||||||
'my dashboard': LayoutDashboard,
|
"my dashboard": LayoutDashboard,
|
||||||
'my profile': User,
|
"my profile": User,
|
||||||
'my portfolio': FolderOpen,
|
"my portfolio": FolderOpen,
|
||||||
'leads': MapPin,
|
leads: MapPin,
|
||||||
'my responses': Star,
|
"my responses": Star,
|
||||||
'credits': CreditCard,
|
credits: CreditCard,
|
||||||
'explore nxtgauge': Globe,
|
"explore nxtgauge": Globe,
|
||||||
'verification': ShieldCheck,
|
verification: ShieldCheck,
|
||||||
'help center': HelpCircle,
|
"help center": HelpCircle,
|
||||||
'settings': Settings,
|
settings: Settings,
|
||||||
'switch services': RefreshCw,
|
"switch services": RefreshCw,
|
||||||
'jobs': Briefcase,
|
jobs: Briefcase,
|
||||||
'applications': Briefcase,
|
applications: Briefcase,
|
||||||
'shortlisted candidates': User,
|
"shortlisted candidates": User,
|
||||||
'my applications': FolderOpen,
|
"my applications": FolderOpen,
|
||||||
'saved jobs': Star,
|
"saved jobs": Star,
|
||||||
'my requirements': FolderOpen,
|
"my requirements": FolderOpen,
|
||||||
'received responses': Bell,
|
"received responses": Bell,
|
||||||
'shortlisted responses': Star,
|
"shortlisted responses": Star,
|
||||||
'logout': LogOut,
|
logout: LogOut,
|
||||||
};
|
};
|
||||||
|
|
||||||
function SidebarIcon(props: { label: string }) {
|
function SidebarIcon(props: { label: string }) {
|
||||||
|
|
@ -42,9 +55,9 @@ function SidebarIcon(props: { label: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function titleCase(value: string) {
|
function titleCase(value: string) {
|
||||||
return String(value || '')
|
return String(value || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/_/g, ' ')
|
.replace(/_/g, " ")
|
||||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,14 +74,14 @@ interface Props {
|
||||||
|
|
||||||
// ── Brand colours ─────────────────────────────────────────────────────────────
|
// ── Brand colours ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const ORANGE = '#FF5E13';
|
const ORANGE = "#FF5E13";
|
||||||
const NAVY = '#0D0D2A';
|
const NAVY = "#0D0D2A";
|
||||||
|
|
||||||
// ── Component ─────────────────────────────────────────────────────────────────
|
// ── Component ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function DashboardShell(props: Props) {
|
export default function DashboardShell(props: Props) {
|
||||||
const roleLabel = createMemo(() => {
|
const roleLabel = createMemo(() => {
|
||||||
const k = String(props.roleKey || '').replace(/_/g, ' ');
|
const k = String(props.roleKey || "").replace(/_/g, " ");
|
||||||
return k.charAt(0).toUpperCase() + k.slice(1).toLowerCase();
|
return k.charAt(0).toUpperCase() + k.slice(1).toLowerCase();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -77,18 +90,21 @@ export default function DashboardShell(props: Props) {
|
||||||
// Fetch unread notification count
|
// Fetch unread notification count
|
||||||
const fetchUnreadCount = async () => {
|
const fetchUnreadCount = async () => {
|
||||||
try {
|
try {
|
||||||
const token = typeof window !== 'undefined' ? window.sessionStorage.getItem('nxtgauge_access_token') || '' : '';
|
const token =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? window.sessionStorage.getItem("nxtgauge_access_token") || ""
|
||||||
|
: "";
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
const res = await fetch('/api/me/notifications/unread-count', {
|
const res = await fetch("/api/me/notifications/unread-count", {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
credentials: 'include',
|
credentials: "include",
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setUnreadCount(data.unread_count || 0);
|
setUnreadCount(data.unread_count || 0);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to fetch unread count:', e);
|
console.error("Failed to fetch unread count:", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -100,68 +116,94 @@ export default function DashboardShell(props: Props) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
'min-height': '100vh',
|
display: "flex",
|
||||||
background: '#F8FAFC',
|
"min-height": "100vh",
|
||||||
'font-family': "'Exo 2', sans-serif",
|
background: "#F8FAFC",
|
||||||
}}>
|
"font-family": "'Exo 2', sans-serif",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* ── Sidebar ──────────────────────────────────────────────────────── */}
|
{/* ── Sidebar ──────────────────────────────────────────────────────── */}
|
||||||
<aside style={{
|
<aside
|
||||||
width: '220px',
|
style={{
|
||||||
'flex-shrink': '0',
|
width: "220px",
|
||||||
background: '#FFFFFF',
|
"flex-shrink": "0",
|
||||||
display: 'flex',
|
background: "#FFFFFF",
|
||||||
'flex-direction': 'column',
|
display: "flex",
|
||||||
'padding': '0',
|
"flex-direction": "column",
|
||||||
'min-height': '100vh',
|
padding: "0",
|
||||||
position: 'sticky',
|
"min-height": "100vh",
|
||||||
top: '0',
|
position: "sticky",
|
||||||
height: '100vh',
|
top: "0",
|
||||||
'overflow-y': 'auto',
|
height: "100vh",
|
||||||
}}>
|
"overflow-y": "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div style={{ padding: '20px 16px 12px', 'border-bottom': '1px solid #E5E7EB' }}>
|
<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' }} />
|
<img
|
||||||
|
src="/nxtgauge-logo.png"
|
||||||
|
alt="Nxtgauge"
|
||||||
|
style={{ height: "40px", "object-fit": "contain", "max-width": "170px" }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Role badge */}
|
{/* Role badge */}
|
||||||
<div style={{ padding: '10px 16px', 'border-bottom': '1px solid #E5E7EB' }}>
|
<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
|
||||||
<p style={{ margin: '2px 0 0', 'font-size': '12px', 'font-weight': '700', color: '#111827' }}>{roleLabel()}</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>
|
</div>
|
||||||
|
|
||||||
{/* Nav items */}
|
{/* Nav items */}
|
||||||
<nav style={{ flex: '1', padding: '8px 8px' }}>
|
<nav style={{ flex: "1", padding: "8px 8px" }}>
|
||||||
<For each={props.sidebarItems}>
|
<For each={props.sidebarItems}>
|
||||||
{(item) => {
|
{(item) => {
|
||||||
const isActive = () => item.toLowerCase() === props.activeSidebar.toLowerCase();
|
const isActive = () => item.toLowerCase() === props.activeSidebar.toLowerCase();
|
||||||
const isLogout = item.toLowerCase() === 'logout';
|
const isLogout = item.toLowerCase() === "logout";
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => props.onSidebarSelect(item)}
|
onClick={() => props.onSidebarSelect(item)}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
'align-items': 'center',
|
"align-items": "center",
|
||||||
gap: '9px',
|
gap: "9px",
|
||||||
width: '100%',
|
width: "100%",
|
||||||
'text-align': 'left',
|
"text-align": "left",
|
||||||
height: '34px',
|
height: "34px",
|
||||||
padding: '0 10px',
|
padding: "0 10px",
|
||||||
'border-radius': '8px',
|
"border-radius": "8px",
|
||||||
border: 'none',
|
border: "none",
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
'font-size': '12px',
|
"font-size": "12px",
|
||||||
'font-weight': '600',
|
"font-weight": "600",
|
||||||
'margin-bottom': '4px',
|
"margin-bottom": "4px",
|
||||||
background: isActive() ? '#FFF3EE' : 'transparent',
|
background: isActive() ? "#FFF3EE" : "transparent",
|
||||||
color: isActive() ? ORANGE : isLogout ? '#DC2626' : '#6B7280',
|
color: isActive() ? ORANGE : isLogout ? "#DC2626" : "#6B7280",
|
||||||
transition: 'background 0.15s, color 0.15s',
|
transition: "background 0.15s, color 0.15s",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ 'flex-shrink': '0', color: isActive() ? ORANGE : '#9CA3AF' }}>
|
<span style={{ "flex-shrink": "0", color: isActive() ? ORANGE : "#9CA3AF" }}>
|
||||||
<SidebarIcon label={item} />
|
<SidebarIcon label={item} />
|
||||||
</span>
|
</span>
|
||||||
{titleCase(item)}
|
{titleCase(item)}
|
||||||
|
|
@ -172,61 +214,94 @@ export default function DashboardShell(props: Props) {
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* User footer */}
|
{/* User footer */}
|
||||||
<div style={{ padding: '12px 16px', 'border-top': '1px solid #E5E7EB' }}>
|
<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' }}>
|
<p
|
||||||
{props.userName || 'User'}
|
style={{
|
||||||
|
margin: "0",
|
||||||
|
"font-size": "12px",
|
||||||
|
"font-weight": "600",
|
||||||
|
color: "#374151",
|
||||||
|
overflow: "hidden",
|
||||||
|
"text-overflow": "ellipsis",
|
||||||
|
"white-space": "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.userName || "User"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* ── Main content ─────────────────────────────────────────────────── */}
|
{/* ── Main content ─────────────────────────────────────────────────── */}
|
||||||
<div style={{ flex: '1', display: 'flex', 'flex-direction': 'column', 'min-width': '0' }}>
|
<div style={{ flex: "1", display: "flex", "flex-direction": "column", "min-width": "0" }}>
|
||||||
{/* Top bar */}
|
{/* Top bar */}
|
||||||
<header style={{
|
<header
|
||||||
height: '56px',
|
style={{
|
||||||
background: '#fff',
|
height: "56px",
|
||||||
'border-bottom': '1px solid #E5E7EB',
|
background: "#fff",
|
||||||
display: 'flex',
|
"border-bottom": "1px solid #E5E7EB",
|
||||||
'align-items': 'center',
|
display: "flex",
|
||||||
'justify-content': 'space-between',
|
"align-items": "center",
|
||||||
padding: '0 24px',
|
"justify-content": "space-between",
|
||||||
'flex-shrink': '0',
|
padding: "0 24px",
|
||||||
}}>
|
"flex-shrink": "0",
|
||||||
<p style={{ margin: '0', 'font-size': '15px', 'font-weight': '700', color: NAVY }}>
|
}}
|
||||||
|
>
|
||||||
|
<p style={{ margin: "0", "font-size": "15px", "font-weight": "700", color: NAVY }}>
|
||||||
{titleCase(props.activeSidebar)}
|
{titleCase(props.activeSidebar)}
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', 'align-items': 'center', gap: '12px' }}>
|
<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 }}>
|
<button
|
||||||
<Bell size={18} style={{ color: '#9CA3AF' }} />
|
type="button"
|
||||||
<Show when={unreadCount() > 0}>
|
style={{
|
||||||
<span style={{
|
position: "relative",
|
||||||
position: 'absolute',
|
border: "none",
|
||||||
top: '-2px',
|
background: "transparent",
|
||||||
right: '-2px',
|
cursor: "pointer",
|
||||||
width: '8px',
|
display: "flex",
|
||||||
height: '8px',
|
"align-items": "center",
|
||||||
background: '#FF5E13',
|
"justify-content": "center",
|
||||||
'border-radius': '50%',
|
padding: 0,
|
||||||
border: '1px solid white'
|
}}
|
||||||
}}></span>
|
>
|
||||||
</Show>
|
<Bell size={18} style={{ color: "#9CA3AF" }} />
|
||||||
</button>
|
<Show when={unreadCount() > 0}>
|
||||||
<div style={{
|
<span
|
||||||
width: '32px', height: '32px', 'border-radius': '999px',
|
style={{
|
||||||
background: ORANGE, color: '#fff', display: 'flex',
|
position: "absolute",
|
||||||
'align-items': 'center', 'justify-content': 'center',
|
top: "-2px",
|
||||||
'font-size': '13px', 'font-weight': '700',
|
right: "-2px",
|
||||||
}}>
|
width: "8px",
|
||||||
{(props.userName || 'U').charAt(0).toUpperCase()}
|
height: "8px",
|
||||||
</div>
|
background: "#FF5E13",
|
||||||
</div>
|
"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>
|
</header>
|
||||||
|
|
||||||
{/* Page content */}
|
{/* Page content */}
|
||||||
<main style={{ flex: '1', padding: '24px', 'overflow-y': 'auto' }}>
|
<main style={{ flex: "1", padding: "24px", "overflow-y": "auto" }}>{props.children}</main>
|
||||||
{props.children}
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
|
<AiChatWidget />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -234,65 +309,65 @@ export default function DashboardShell(props: Props) {
|
||||||
// ── Shared UI primitives ──────────────────────────────────────────────────────
|
// ── Shared UI primitives ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const CARD = {
|
export const CARD = {
|
||||||
background: '#fff',
|
background: "#fff",
|
||||||
border: '1px solid #E5E7EB',
|
border: "1px solid #E5E7EB",
|
||||||
'border-radius': '14px',
|
"border-radius": "14px",
|
||||||
padding: '20px',
|
padding: "20px",
|
||||||
'box-shadow': '0 1px 4px rgba(0,0,0,0.06)',
|
"box-shadow": "0 1px 4px rgba(0,0,0,0.06)",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const BTN_PRIMARY = {
|
export const BTN_PRIMARY = {
|
||||||
height: '38px',
|
height: "38px",
|
||||||
'border-radius': '10px',
|
"border-radius": "10px",
|
||||||
border: 'none',
|
border: "none",
|
||||||
background: NAVY,
|
background: NAVY,
|
||||||
color: '#fff',
|
color: "#fff",
|
||||||
padding: '0 18px',
|
padding: "0 18px",
|
||||||
'font-size': '13px',
|
"font-size": "13px",
|
||||||
'font-weight': '700',
|
"font-weight": "700",
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const BTN_ORANGE = {
|
export const BTN_ORANGE = {
|
||||||
height: '38px',
|
height: "38px",
|
||||||
'border-radius': '10px',
|
"border-radius": "10px",
|
||||||
border: 'none',
|
border: "none",
|
||||||
background: ORANGE,
|
background: ORANGE,
|
||||||
color: '#fff',
|
color: "#fff",
|
||||||
padding: '0 18px',
|
padding: "0 18px",
|
||||||
'font-size': '13px',
|
"font-size": "13px",
|
||||||
'font-weight': '700',
|
"font-weight": "700",
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const BTN_GHOST = {
|
export const BTN_GHOST = {
|
||||||
height: '38px',
|
height: "38px",
|
||||||
'border-radius': '10px',
|
"border-radius": "10px",
|
||||||
border: '1px solid #E5E7EB',
|
border: "1px solid #E5E7EB",
|
||||||
background: '#fff',
|
background: "#fff",
|
||||||
color: '#374151',
|
color: "#374151",
|
||||||
padding: '0 18px',
|
padding: "0 18px",
|
||||||
'font-size': '13px',
|
"font-size": "13px",
|
||||||
'font-weight': '600',
|
"font-weight": "600",
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const INPUT = {
|
export const INPUT = {
|
||||||
height: '40px',
|
height: "40px",
|
||||||
width: '100%',
|
width: "100%",
|
||||||
'border-radius': '8px',
|
"border-radius": "8px",
|
||||||
border: '1px solid #E5E7EB',
|
border: "1px solid #E5E7EB",
|
||||||
padding: '0 12px',
|
padding: "0 12px",
|
||||||
'font-size': '14px',
|
"font-size": "14px",
|
||||||
color: '#111827',
|
color: "#111827",
|
||||||
background: '#fff',
|
background: "#fff",
|
||||||
'box-sizing': 'border-box',
|
"box-sizing": "border-box",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const LABEL = {
|
export const LABEL = {
|
||||||
display: 'block',
|
display: "block",
|
||||||
'font-size': '12px',
|
"font-size": "12px",
|
||||||
'font-weight': '600',
|
"font-weight": "600",
|
||||||
color: '#374151',
|
color: "#374151",
|
||||||
'margin-bottom': '6px',
|
"margin-bottom": "6px",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue