Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ph-ee-operations-web-react</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/fav.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ph-ee-operations-web-react</title>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

</html>
Binary file added public/fav.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
148 changes: 84 additions & 64 deletions src/components/shared/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom'
import { useState } from 'react'
import { Bell, HelpCircle, Search, User } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import {
ArrowLeftRight,
BarChart2,
Bell,
ClipboardList,
HelpCircle,
LayoutDashboard,
Landmark,
Search,
Settings,
Shield,
SlidersHorizontal,
Ticket,
User,
Users,
} from 'lucide-react'
import { APP_NAME, DEFAULT_TENANT, TENANTS } from '@/config/constants'

const navItems = [
{ label: 'Dashboard', path: '/' },
{ label: 'Settings', path: '/settings' },
interface NavItem {
label: string
path: string
icon: LucideIcon
exact?: boolean
}

const navItems: NavItem[] = [
{ label: 'Dashboard', path: '/', icon: LayoutDashboard, exact: true },
{ label: 'Payment Hub', path: '/payment-hub', icon: Landmark },
{ label: 'Vouchers', path: '/vouchers', icon: Ticket },
{ label: 'Account Mapper', path: '/account-mapper',icon: ArrowLeftRight },
{ label: 'Visualizations', path: '/reporting', icon: BarChart2 },
{ label: 'G2P Payment Config',path: '/g2p-config', icon: SlidersHorizontal },
{ label: 'Users', path: '/rbac', icon: Users },
{ label: 'Roles & Permissions',path: '/rbac', icon: Shield },
{ label: 'Audit Trails', path: '/audit', icon: ClipboardList },
{ label: 'Settings', path: '/settings', icon: Settings },
]

export default function AppLayout() {
Expand All @@ -20,48 +51,54 @@ export default function AppLayout() {
localStorage.setItem('tenant', value)
}

function isActive(item: NavItem) {
return item.exact
? location.pathname === item.path
: location.pathname.startsWith(item.path)
}

return (
<div className="flex h-screen bg-[#F5F7FA]">
{/* Sidebar */}
<aside
style={{ width: 240, backgroundColor: '#1565C0' }}
className="flex flex-col text-white shrink-0"
>
{/* Logo */}
<div className="px-4 py-3 border-b border-white/10 flex items-center justify-center">
<div className="bg-white rounded px-2 py-1">
<img
src="/payment-hub-ee.png"
alt={APP_NAME}
className="h-10 w-auto object-contain"
/>
</div>
<aside className="w-60 flex flex-col bg-white border-r border-gray-200 shrink-0">
{/* Logo area */}
<div className="px-4 py-3 border-b border-gray-100 flex items-center justify-center">
<img
src="/payment-hub-ee.png"
alt={APP_NAME}
className="h-10 w-auto object-contain"
/>
</div>

{/* Nav items */}
<nav className="flex-1 py-2">
{navItems.map((item) => (
<Link
key={item.path}
to={item.path}
className={[
'flex items-center px-6 py-3 text-sm font-medium transition-colors',
location.pathname === item.path
? 'bg-white/20 text-white'
: 'text-white/80 hover:bg-white/10 hover:text-white',
].join(' ')}
>
{item.label}
</Link>
))}
{/* Nav */}
<nav className="flex-1 py-3 overflow-y-auto">
{navItems.map((item) => {
const active = isActive(item)
const Icon = item.icon
return (
<Link
key={`${item.label}-${item.path}`}
to={item.path}
className={[
'flex items-center gap-3 px-4 py-2.5 text-sm font-medium transition-colors relative',
active
? 'bg-blue-50 text-[#1565C0] border-l-4 border-[#1565C0]'
: 'text-gray-700 hover:bg-gray-50 border-l-4 border-transparent',
].join(' ')}
>
<Icon size={16} className={active ? 'text-[#1565C0]' : 'text-gray-400'} />
{item.label}
</Link>
)
})}
</nav>

{/* Back button */}
<div className="px-6 py-4 border-t border-white/10">
<div className="px-5 py-4 border-t border-gray-100">
<button
type="button"
onClick={() => navigate(-1)}
className="text-sm text-white/70 hover:text-white"
className="text-sm text-gray-400 hover:text-gray-600 transition-colors"
>
← Back
</button>
Expand All @@ -70,61 +107,44 @@ export default function AppLayout() {

{/* Right panel */}
<div className="flex-1 flex flex-col overflow-hidden min-w-0">
{/* Top header */}
<header className="h-16 bg-white border-b border-gray-200 flex items-center justify-end px-6 gap-3 shrink-0">
{/* Language */}
<select aria-label="Language" className="text-sm border border-gray-200 rounded px-2 py-1 text-gray-600 bg-white">
{/* Header */}
<header className="h-14 bg-white border-b border-gray-200 flex items-center justify-end px-6 gap-3 shrink-0">
<select
aria-label="Language"
className="text-sm border border-gray-200 rounded px-2 py-1 text-gray-600 bg-white"
>
<option value="en">EN</option>
<option value="fr">FR</option>
</select>

{/* Tenant switcher */}
<select
aria-label="Tenant"
className="text-sm border border-gray-200 rounded px-2 py-1 text-gray-600 bg-white"
value={tenant}
onChange={(e) => handleTenantChange(e.target.value)}
>
{TENANTS.map((t) => (
<option key={t} value={t}>
{t}
</option>
<option key={t} value={t}>{t}</option>
))}
</select>

<button
type="button"
aria-label="Notifications"
className="p-1 text-gray-400 hover:text-gray-600"
>
<button type="button" aria-label="Notifications" className="p-1 text-gray-400 hover:text-gray-600">
<Bell size={18} />
</button>
<button
type="button"
aria-label="Search"
className="p-1 text-gray-400 hover:text-gray-600"
>
<button type="button" aria-label="Search" className="p-1 text-gray-400 hover:text-gray-600">
<Search size={18} />
</button>
<button
type="button"
aria-label="Help"
className="p-1 text-gray-400 hover:text-gray-600"
>
<button type="button" aria-label="Help" className="p-1 text-gray-400 hover:text-gray-600">
<HelpCircle size={18} />
</button>

{/* User avatar */}
<div
className="w-8 h-8 rounded-full flex items-center justify-center"
style={{ backgroundColor: '#1565C0' }}
>
<div className="w-8 h-8 rounded-full bg-[#1565C0] flex items-center justify-center">
<User size={15} className="text-white" />
</div>
</header>

{/* Main content */}
<main className="flex-1 overflow-auto p-6">
<main className="flex-1 overflow-auto p-6 bg-[#F5F7FA]">
<Outlet />
</main>
</div>
Expand Down
105 changes: 105 additions & 0 deletions src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import * as React from "react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"

const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogClose = DialogPrimitive.Close

function DialogPortal(props: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal {...props} />
}

function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
)
}

function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
className={cn(
"fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-2xl bg-white shadow-xl outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
)
}

function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("flex flex-col gap-1.5 px-6 pt-6 pb-4", className)}
{...props}
/>
)
}

function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("flex justify-end gap-2 px-6 pb-6 pt-2", className)}
{...props}
/>
)
}

function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
className={cn("text-base font-semibold text-foreground", className)}
{...props}
/>
)
}

function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}

export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
}
4 changes: 4 additions & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import G2PConfig from '@/pages/G2PConfig'
import Settings from '@/pages/Settings'
import RBACConfig from '@/pages/RBACConfig'
import Reporting from '@/pages/Reporting'
import Visualizations from '@/pages/Visualizations'
import NotFound from '@/pages/NotFound'

const queryClient = new QueryClient()

Expand All @@ -32,8 +34,10 @@ const router = createBrowserRouter([
{ path: 'settings', element: <Settings /> },
{ path: 'rbac', element: <RBACConfig /> },
{ path: 'reporting', element: <Reporting /> },
{ path: 'visualizations', element: <Visualizations /> },
],
},
{ path: '*', element: <NotFound /> },
])

createRoot(document.getElementById('root')!).render(
Expand Down
Loading
Loading