urmi-ai/frontend/src/components/AppShell.tsx
2026-10-02 11:22:40 +05:30

146 lines
6.1 KiB
TypeScript

import { useState } from 'react'
import {
Activity, Bell, BookOpen, Bot, Boxes, ChevronDown, CircleDollarSign,
Code2, CreditCard, Cpu, FileCheck2, Gauge, KeyRound, LayoutDashboard,
LogOut, Menu, MessageSquareText, Network, Phone, Search, Settings, ShieldCheck,
SlidersHorizontal, TestTube2, Users, Webhook, X, Zap,
} from 'lucide-react'
import { NavLink, Outlet, useNavigate } from 'react-router-dom'
import { useAuth } from '../features/auth/auth-context'
import { ComplianceProvider } from '../features/compliance/ComplianceProvider'
import { useCompliance } from '../features/compliance/compliance-context'
import { COMPLIANCE_ONLY_PATHS } from '../features/compliance/compliance-context'
const groups = [
{ label: 'Compliance', links: [
{ label: 'KYC', path: '/console/kyc', icon: ShieldCheck },
{ label: 'DLT', path: '/console/dlt', icon: FileCheck2 },
{ label: 'Message settings', path: '/console/message-settings', icon: SlidersHorizontal },
]},
{ label: 'Workspace', links: [
{ label: 'Overview', path: '/console', icon: LayoutDashboard },
{ label: 'Activity', path: '/console/activity', icon: Activity },
]},
{ label: 'Agent', links: [
{ label: 'UrmiAI', path: '/console/agent/urmiai', icon: Bot },
{ label: 'UrmIOT', path: '/console/agent/urmiot', icon: Cpu },
{ label: 'UrmiOTP', path: '/console/agent/urmiotp', icon: KeyRound },
]},
{ label: 'Communicate', links: [
{ label: 'Messaging', path: '/console/messaging', icon: MessageSquareText },
{ label: 'Campaigns', path: '/console/campaigns', icon: Zap },
{ label: 'Templates', path: '/console/templates', icon: BookOpen },
]},
{ label: 'Numbers', links: [
{ label: 'Voice', path: '/console/voice', icon: Phone },
{ label: 'Buy Numbers', path: '/console/numbers/buy', icon: Search },
{ label: 'My Numbers', path: '/console/numbers', icon: Network, end: true },
]},
{ label: 'Develop', links: [
{ label: 'Webhooks & Sockets', path: '/console/webhooks', icon: Webhook },
{ label: 'API Keys', path: '/console/api-keys', icon: KeyRound },
{ label: 'API Explorer', path: '/console/api-explorer', icon: Code2 },
]},
{ label: 'Test & observe', links: [
{ label: 'Testing Center', path: '/console/testing', icon: TestTube2 },
{ label: 'Logs', path: '/console/logs', icon: Boxes },
{ label: 'Analytics', path: '/console/analytics', icon: Gauge },
]},
{ label: 'Manage', links: [
{ label: 'Billing', path: '/console/billing', icon: CreditCard },
{ label: 'Team', path: '/console/team', icon: Users },
{ label: 'Settings', path: '/console/settings', icon: Settings },
]},
]
function AppShellContent() {
const [open, setOpen] = useState(false)
const [accountOpen, setAccountOpen] = useState(false)
const { user, logout } = useAuth()
const { status } = useCompliance()
const navigate = useNavigate()
const unlocked = status?.console_unlocked ?? false
const visibleGroups = unlocked
? groups
: [{
label: 'Compliance',
links: groups[0].links.filter((link) => COMPLIANCE_ONLY_PATHS.includes(link.path)),
}]
async function signOut() {
await logout()
navigate('/')
}
return (
<div className="app-shell">
<aside className={`sidebar ${open ? 'is-open' : ''}`}>
<div className="brand">
<NavLink to={unlocked ? '/console' : '/console/dlt'} className="brand-mark" onClick={() => setOpen(false)}>
<img src="/urmi-logo.png" alt="Urmi" />
</NavLink>
<button className="icon-button sidebar-close" onClick={() => setOpen(false)}><X size={18} /></button>
</div>
<nav>
{visibleGroups.map((group) => (
<div className="nav-group" key={group.label}>
<p>{group.label}</p>
{group.links.map(({ label, path, icon: Icon, end }) => (
<NavLink key={path} to={path} end={end ?? path === '/console'} onClick={() => setOpen(false)}>
<Icon size={17} strokeWidth={1.8} />
<span>{label}</span>
</NavLink>
))}
</div>
))}
</nav>
{unlocked && (
<div className="sidebar-plan">
<div><CircleDollarSign size={17} /><span>Growth plan</span></div>
<strong>₹24,820.00</strong>
<small>Available balance</small>
<button>Add funds</button>
</div>
)}
</aside>
{open && <button className="sidebar-scrim" aria-label="Close navigation" onClick={() => setOpen(false)} />}
<div className="workspace">
<header className="topbar">
<button className="icon-button menu-button" onClick={() => setOpen(true)}><Menu size={20} /></button>
<button className="selector">Urmi Technologies <ChevronDown size={14} /></button>
<button className="selector hide-mobile">Production API <ChevronDown size={14} /></button>
<span className="environment"><i /> Production</span>
<label className="global-search">
<Search size={16} />
<input aria-label="Global search" placeholder="Search numbers, calls, messages..." />
<kbd>⌘ K</kbd>
</label>
<span className="system-status"><i /> Operational</span>
<button className="icon-button notification-button"><Bell size={19} /><b>3</b></button>
<div className="account-menu">
<button className="avatar" onClick={() => setAccountOpen(!accountOpen)}>
{user?.avatar_url ? <img src={user.avatar_url} alt="" /> : user?.name.slice(0, 2).toUpperCase()}
</button>
{accountOpen && <div className="account-popover">
<div><strong>{user?.name}</strong><span>{user?.email}</span></div>
<NavLink to="/console/account" onClick={() => setAccountOpen(false)}><Settings size={15} /> Sign-in & security</NavLink>
<button onClick={signOut}><LogOut size={15} /> Log out</button>
</div>}
</div>
</header>
<main><Outlet /></main>
</div>
</div>
)
}
export function AppShell() {
return (
<ComplianceProvider>
<AppShellContent />
</ComplianceProvider>
)
}