Some checks failed
CI / Frontend Lint (push) Failing after 6s
CI / Frontend Type Check (push) Failing after 6s
CI / Frontend Build (push) Failing after 6s
CI / Frontend E2E Tests (push) Failing after 9s
CI / Orchestrator Build (push) Failing after 6s
CI / Contracts Compile (push) Failing after 6s
CI / Contracts Test (push) Failing after 6s
Security Scan / Dependency Vulnerability Scan (push) Failing after 4s
Security Scan / OWASP ZAP Scan (push) Failing after 4s
177 lines
7.6 KiB
TypeScript
177 lines
7.6 KiB
TypeScript
import { useState } from 'react';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
import {
|
|
LayoutDashboard, Zap, Building2, Landmark, FileText, Shield, CheckSquare,
|
|
Settings, LogOut, ChevronLeft, ChevronRight, Bell, User, Copy,
|
|
ExternalLink, ChevronDown, GitBranch
|
|
} from 'lucide-react';
|
|
|
|
const navItems = [
|
|
{ id: 'dashboard', label: 'Overview', icon: LayoutDashboard, path: '/dashboard' },
|
|
{ id: 'transaction-builder', label: 'Transaction Builder', icon: Zap, path: '/transaction-builder' },
|
|
{ id: 'transactions', label: 'Transactions', icon: GitBranch, path: '/transactions' },
|
|
{ id: 'accounts', label: 'Accounts', icon: Building2, path: '/accounts' },
|
|
{ id: 'treasury', label: 'Treasury', icon: Landmark, path: '/treasury' },
|
|
{ id: 'reporting', label: 'Reporting', icon: FileText, path: '/reporting' },
|
|
{ id: 'compliance', label: 'Compliance & Risk', icon: Shield, path: '/compliance' },
|
|
{ id: 'settlements', label: 'Settlements', icon: CheckSquare, path: '/settlements' },
|
|
];
|
|
|
|
interface PortalLayoutProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export default function PortalLayout({ children }: PortalLayoutProps) {
|
|
const { user, wallet, disconnect } = useAuth();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
const [showUserMenu, setShowUserMenu] = useState(false);
|
|
const [showNotifications, setShowNotifications] = useState(false);
|
|
|
|
const currentPath = location.pathname;
|
|
|
|
const copyAddress = () => {
|
|
if (wallet?.address) {
|
|
navigator.clipboard.writeText(wallet.address);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="portal-layout">
|
|
<div className="portal-topbar">
|
|
<div className="portal-topbar-left">
|
|
<div className="portal-logo" onClick={() => navigate('/dashboard')}>
|
|
<Building2 size={22} color="#3b82f6" />
|
|
{!collapsed && (
|
|
<div className="portal-logo-text">
|
|
<span className="portal-logo-name">Solace Bank Group</span>
|
|
<span className="portal-logo-plc">PLC</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="portal-topbar-center">
|
|
<div className="portal-env-badge">
|
|
<span className="env-dot" />
|
|
Production
|
|
</div>
|
|
</div>
|
|
|
|
<div className="portal-topbar-right">
|
|
<div className="portal-notif-wrapper">
|
|
<button className="portal-icon-btn" onClick={() => { setShowNotifications(!showNotifications); setShowUserMenu(false); }}>
|
|
<Bell size={18} />
|
|
<span className="portal-notif-badge">3</span>
|
|
</button>
|
|
{showNotifications && (
|
|
<div className="portal-dropdown notifications-dropdown">
|
|
<div className="portal-dropdown-header">Notifications</div>
|
|
<div className="portal-dropdown-item warning">
|
|
<span className="dropdown-dot warning" />
|
|
<div>
|
|
<div className="dropdown-title">AML Alert</div>
|
|
<div className="dropdown-desc">Unusual pattern on ACC-001</div>
|
|
</div>
|
|
</div>
|
|
<div className="portal-dropdown-item info">
|
|
<span className="dropdown-dot info" />
|
|
<div>
|
|
<div className="dropdown-title">Settlement Confirmed</div>
|
|
<div className="dropdown-desc">TX-2024-0847 settled</div>
|
|
</div>
|
|
</div>
|
|
<div className="portal-dropdown-item">
|
|
<span className="dropdown-dot success" />
|
|
<div>
|
|
<div className="dropdown-title">Report Ready</div>
|
|
<div className="dropdown-desc">Q4 IFRS Balance Sheet</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="portal-user-wrapper">
|
|
<button className="portal-user-btn" onClick={() => { setShowUserMenu(!showUserMenu); setShowNotifications(false); }}>
|
|
<div className="portal-avatar">
|
|
<User size={14} />
|
|
</div>
|
|
<div className="portal-user-info">
|
|
<span className="portal-user-name">{user?.displayName || 'User'}</span>
|
|
<span className="portal-user-role">{user?.role?.replace('_', ' ') || 'Admin'}</span>
|
|
</div>
|
|
<ChevronDown size={12} />
|
|
</button>
|
|
{showUserMenu && (
|
|
<div className="portal-dropdown user-dropdown">
|
|
<div className="portal-dropdown-header">Account</div>
|
|
<div className="portal-dropdown-section">
|
|
<div className="portal-wallet-addr">
|
|
<span className="mono">{wallet?.address ? `${wallet.address.slice(0, 8)}...${wallet.address.slice(-6)}` : '—'}</span>
|
|
<button className="copy-btn" onClick={copyAddress} title="Copy address"><Copy size={12} /></button>
|
|
</div>
|
|
<div className="portal-wallet-bal">
|
|
<span>{wallet?.balance ? `${parseFloat(wallet.balance).toFixed(4)} ETH` : '—'}</span>
|
|
<span className="chain-badge">Chain {wallet?.chainId || 1}</span>
|
|
</div>
|
|
</div>
|
|
<div className="portal-dropdown-divider" />
|
|
<button className="portal-dropdown-action" onClick={() => navigate('/settings')}>
|
|
<Settings size={14} /> Settings
|
|
</button>
|
|
<button className="portal-dropdown-action" onClick={() => window.open('https://etherscan.io', '_blank')}>
|
|
<ExternalLink size={14} /> View on Explorer
|
|
</button>
|
|
<div className="portal-dropdown-divider" />
|
|
<button className="portal-dropdown-action danger" onClick={disconnect}>
|
|
<LogOut size={14} /> Disconnect Wallet
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="portal-body">
|
|
<nav className={`portal-sidebar ${collapsed ? 'collapsed' : ''}`}>
|
|
<div className="portal-nav-items">
|
|
{navItems.map(item => {
|
|
const Icon = item.icon;
|
|
const isActive = currentPath === item.path || (item.path !== '/dashboard' && currentPath.startsWith(item.path));
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
className={`portal-nav-item ${isActive ? 'active' : ''}`}
|
|
onClick={() => navigate(item.path)}
|
|
title={collapsed ? item.label : undefined}
|
|
>
|
|
<Icon size={18} />
|
|
{!collapsed && <span>{item.label}</span>}
|
|
{isActive && <div className="nav-active-indicator" />}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="portal-nav-footer">
|
|
<button className="portal-nav-item" onClick={() => navigate('/settings')} title={collapsed ? 'Settings' : undefined}>
|
|
<Settings size={18} />
|
|
{!collapsed && <span>Settings</span>}
|
|
</button>
|
|
<button className="portal-collapse-btn" onClick={() => setCollapsed(!collapsed)}>
|
|
{collapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
|
|
</button>
|
|
</div>
|
|
</nav>
|
|
|
|
<main className="portal-content">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|