Files
CurrenciCombo/src/components/portal/PortalLayout.tsx
nsatoshi b66ec0a78f
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
PR G: portal /transactions page + 12-state machine view (#11)
2026-04-22 17:18:52 +00:00

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>
);
}