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 { Router } from '@solidjs/router';
|
||||
import { FileRoutes } from '@solidjs/start/router';
|
||||
import { ErrorBoundary, Suspense } from 'solid-js';
|
||||
import { AuthProvider } from '~/lib/auth';
|
||||
import './app.css';
|
||||
import { MetaProvider, Title } from "@solidjs/meta";
|
||||
import { Router } from "@solidjs/router";
|
||||
import { FileRoutes } from "@solidjs/start/router";
|
||||
import { ErrorBoundary, Suspense } from "solid-js";
|
||||
import { AuthProvider } from "~/lib/auth";
|
||||
import { AiChatWidget } from "~/components/AiChatWidget";
|
||||
import "./app.css";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
|
|
@ -14,16 +15,34 @@ export default function App() {
|
|||
<AuthProvider>
|
||||
<ErrorBoundary
|
||||
fallback={(err) => (
|
||||
<main style={{ padding: '24px', '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' }}>
|
||||
<main
|
||||
style={{
|
||||
padding: "24px",
|
||||
"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)}
|
||||
</pre>
|
||||
</main>
|
||||
)}
|
||||
>
|
||||
<Suspense>{props.children}</Suspense>
|
||||
<AiChatWidget />
|
||||
</ErrorBoundary>
|
||||
</AuthProvider>
|
||||
</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
|
||||
* (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 {
|
||||
User, Briefcase, LayoutDashboard, FolderOpen, MapPin, Star,
|
||||
CreditCard, Globe, ShieldCheck, HelpCircle, Settings,
|
||||
RefreshCw, LogOut, Bell, ChevronRight,
|
||||
} from 'lucide-solid';
|
||||
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,
|
||||
"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 }) {
|
||||
|
|
@ -42,9 +55,9 @@ function SidebarIcon(props: { label: string }) {
|
|||
}
|
||||
|
||||
function titleCase(value: string) {
|
||||
return String(value || '')
|
||||
return String(value || "")
|
||||
.toLowerCase()
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
|
|
@ -61,14 +74,14 @@ interface Props {
|
|||
|
||||
// ── Brand colours ─────────────────────────────────────────────────────────────
|
||||
|
||||
const ORANGE = '#FF5E13';
|
||||
const NAVY = '#0D0D2A';
|
||||
const ORANGE = "#FF5E13";
|
||||
const NAVY = "#0D0D2A";
|
||||
|
||||
// ── Component ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function DashboardShell(props: Props) {
|
||||
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();
|
||||
});
|
||||
|
||||
|
|
@ -77,18 +90,21 @@ export default function DashboardShell(props: Props) {
|
|||
// Fetch unread notification count
|
||||
const fetchUnreadCount = async () => {
|
||||
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;
|
||||
const res = await fetch('/api/me/notifications/unread-count', {
|
||||
const res = await fetch("/api/me/notifications/unread-count", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
credentials: 'include',
|
||||
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);
|
||||
console.error("Failed to fetch unread count:", e);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -100,68 +116,94 @@ export default function DashboardShell(props: Props) {
|
|||
});
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
'min-height': '100vh',
|
||||
background: '#F8FAFC',
|
||||
'font-family': "'Exo 2', sans-serif",
|
||||
}}>
|
||||
|
||||
<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',
|
||||
}}>
|
||||
<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 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 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' }}>
|
||||
<nav style={{ flex: "1", padding: "8px 8px" }}>
|
||||
<For each={props.sidebarItems}>
|
||||
{(item) => {
|
||||
const isActive = () => item.toLowerCase() === props.activeSidebar.toLowerCase();
|
||||
const isLogout = item.toLowerCase() === 'logout';
|
||||
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',
|
||||
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' }}>
|
||||
<span style={{ "flex-shrink": "0", color: isActive() ? ORANGE : "#9CA3AF" }}>
|
||||
<SidebarIcon label={item} />
|
||||
</span>
|
||||
{titleCase(item)}
|
||||
|
|
@ -172,61 +214,94 @@ export default function DashboardShell(props: Props) {
|
|||
</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'}
|
||||
<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' }}>
|
||||
<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 }}>
|
||||
<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>
|
||||
<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>
|
||||
<main style={{ flex: "1", padding: "24px", "overflow-y": "auto" }}>{props.children}</main>
|
||||
</div>
|
||||
<AiChatWidget />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -234,65 +309,65 @@ export default function DashboardShell(props: Props) {
|
|||
// ── 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)',
|
||||
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',
|
||||
height: "38px",
|
||||
"border-radius": "10px",
|
||||
border: "none",
|
||||
background: NAVY,
|
||||
color: '#fff',
|
||||
padding: '0 18px',
|
||||
'font-size': '13px',
|
||||
'font-weight': '700',
|
||||
cursor: 'pointer',
|
||||
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',
|
||||
height: "38px",
|
||||
"border-radius": "10px",
|
||||
border: "none",
|
||||
background: ORANGE,
|
||||
color: '#fff',
|
||||
padding: '0 18px',
|
||||
'font-size': '13px',
|
||||
'font-weight': '700',
|
||||
cursor: 'pointer',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
display: "block",
|
||||
"font-size": "12px",
|
||||
"font-weight": "600",
|
||||
color: "#374151",
|
||||
"margin-bottom": "6px",
|
||||
} as const;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue