feat: implement dashboard UI components, data types, and API integration
This commit is contained in:
+26
-20
@@ -1,28 +1,34 @@
|
|||||||
|
import { lazy, Suspense } from 'react'
|
||||||
import { Route, Routes, Navigate } from 'react-router-dom'
|
import { Route, Routes, Navigate } from 'react-router-dom'
|
||||||
import Layout from './layout/AppLayout'
|
import Layout from './layout/AppLayout'
|
||||||
import HomePage from './app/dashboard'
|
import CustomAppLoader from './components/custom/CustomAppLoader'
|
||||||
import CohortManage from './app/cohartManage'
|
|
||||||
import PolicyEngineList from './app/policyEngine/components/PolicyEngineList'
|
const HomePage = lazy(() => import('./app/dashboard'))
|
||||||
import AddPolicyEngine from './app/policyEngine/components/AddPolicyEngine'
|
const CohortManage = lazy(() => import('./app/cohartManage'))
|
||||||
import RecoveryIncidentsList from './app/recoveryIncidents/components/RecoveryIncidentsList'
|
const PolicyEngineList = lazy(() => import('./app/policyEngine/components/PolicyEngineList'))
|
||||||
import RecoveryIncidentTabs from './app/recoveryIncidents/tabs/index'
|
const AddPolicyEngine = lazy(() => import('./app/policyEngine/components/AddPolicyEngine'))
|
||||||
import AuditLogsList from './app/auditLogs/components/AuditLogsList'
|
const RecoveryIncidentsList = lazy(() => import('./app/recoveryIncidents/components/RecoveryIncidentsList'))
|
||||||
import ConfigurationPage from './app/configuration'
|
const RecoveryIncidentTabs = lazy(() => import('./app/recoveryIncidents/tabs/index'))
|
||||||
|
const AuditLogsList = lazy(() => import('./app/auditLogs/components/AuditLogsList'))
|
||||||
|
const ConfigurationPage = lazy(() => import('./app/configuration'))
|
||||||
|
|
||||||
function AppRoutes() {
|
function AppRoutes() {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Routes>
|
<Suspense fallback={<CustomAppLoader />}>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Routes>
|
||||||
<Route path="/cohorts" element={<CohortManage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/policy-engine" element={<PolicyEngineList />} />
|
<Route path="/cohorts" element={<CohortManage />} />
|
||||||
<Route path="/policy-engine/add" element={<AddPolicyEngine />} />
|
<Route path="/policy-engine" element={<PolicyEngineList />} />
|
||||||
<Route path="/policy-engine/edit/:id" element={<AddPolicyEngine />} />
|
<Route path="/policy-engine/add" element={<AddPolicyEngine />} />
|
||||||
<Route path="/action-builder" element={<Navigate to="/config?tab=action-builder" replace />} />
|
<Route path="/policy-engine/edit/:id" element={<AddPolicyEngine />} />
|
||||||
<Route path="/config" element={<ConfigurationPage />} />
|
<Route path="/action-builder" element={<Navigate to="/config?tab=action-builder" replace />} />
|
||||||
<Route path="/recovery" element={<RecoveryIncidentsList />} />
|
<Route path="/config" element={<ConfigurationPage />} />
|
||||||
<Route path="/recovery/:id" element={<RecoveryIncidentTabs />} />
|
<Route path="/recovery" element={<RecoveryIncidentsList />} />
|
||||||
<Route path="/audit-logs" element={<AuditLogsList />} />
|
<Route path="/recovery/:id" element={<RecoveryIncidentTabs />} />
|
||||||
</Routes>
|
<Route path="/audit-logs" element={<AuditLogsList />} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiClient } from '../api/ApiClient';
|
||||||
|
import type {
|
||||||
|
DashboardMetric,
|
||||||
|
ExposureChartItem,
|
||||||
|
DisruptionMixItem,
|
||||||
|
RecentIncident,
|
||||||
|
} from './DashboardTypes';
|
||||||
|
|
||||||
|
export function getDashboardMetrics(): Promise<DashboardMetric[]> {
|
||||||
|
return ApiClient.get<any, DashboardMetric[]>('/dashboard/metrics');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExposureData(): Promise<ExposureChartItem[]> {
|
||||||
|
return ApiClient.get<any, ExposureChartItem[]>('/dashboard/exposure');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDisruptionMixData(): Promise<DisruptionMixItem[]> {
|
||||||
|
return ApiClient.get<any, DisruptionMixItem[]>('/dashboard/disruption-mix');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRecentIncidents(): Promise<RecentIncident[]> {
|
||||||
|
return ApiClient.get<any, RecentIncident[]>('/dashboard/recent-incidents');
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export interface DashboardMetric {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
trend: string;
|
||||||
|
isPositive?: boolean;
|
||||||
|
sparklineData: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExposureChartItem {
|
||||||
|
category: string;
|
||||||
|
amount: number;
|
||||||
|
displayAmount: string;
|
||||||
|
color: string;
|
||||||
|
views?: string;
|
||||||
|
date?: string;
|
||||||
|
hasTooltip?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DisruptionMixItem {
|
||||||
|
label: string;
|
||||||
|
percentage: number;
|
||||||
|
value: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecentIncident {
|
||||||
|
id: string;
|
||||||
|
recoveryId: string;
|
||||||
|
passengerName: string;
|
||||||
|
pnr: string;
|
||||||
|
flightNumber: string;
|
||||||
|
category: string;
|
||||||
|
status: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import type { DisruptionMixItem } from "../DashboardTypes";
|
||||||
|
|
||||||
|
interface DisruptionMixChartProps {
|
||||||
|
data: DisruptionMixItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisruptionMixChart: React.FC<DisruptionMixChartProps> = ({ data }) => {
|
||||||
|
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Calculate SVG donut stroke offsets
|
||||||
|
const size = 180;
|
||||||
|
const strokeWidth = 24;
|
||||||
|
const radius = (size - strokeWidth) / 2;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
|
||||||
|
let accumulatedPercent = 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-[16px] border border-gray-100 p-6 shadow-[0_2px_4px_rgba(0,0,0,0.02)] flex flex-col justify-between h-full">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-[17px] font-bold text-gray-900 leading-snug">
|
||||||
|
Disruption Mix
|
||||||
|
</h2>
|
||||||
|
<p className="text-[12px] text-gray-400 font-normal mt-0.5">
|
||||||
|
Distribution of passenger recovery cases.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Donut Chart Visual */}
|
||||||
|
<div className="flex flex-col items-center justify-center my-6 relative">
|
||||||
|
<div className="relative w-[180px] h-[180px] flex items-center justify-center">
|
||||||
|
<svg width={size} height={size} className="transform -rotate-90">
|
||||||
|
{data.map((item, index) => {
|
||||||
|
const strokeDasharray = `${(item.percentage / 100) * circumference} ${circumference}`;
|
||||||
|
const strokeDashoffset = -((accumulatedPercent / 100) * circumference);
|
||||||
|
accumulatedPercent += item.percentage;
|
||||||
|
|
||||||
|
const isHovered = hoveredIndex === index;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<circle
|
||||||
|
key={item.label}
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
fill="transparent"
|
||||||
|
stroke={item.color}
|
||||||
|
strokeWidth={isHovered ? strokeWidth + 4 : strokeWidth}
|
||||||
|
strokeDasharray={strokeDasharray}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
className="transition-all duration-300 cursor-pointer"
|
||||||
|
onMouseEnter={() => setHoveredIndex(index)}
|
||||||
|
onMouseLeave={() => setHoveredIndex(null)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Donut Center Info */}
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
|
||||||
|
<span className="text-[22px] font-bold text-gray-900 leading-none">
|
||||||
|
{hoveredIndex !== null ? `${data[hoveredIndex].percentage}%` : "100%"}
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] font-medium text-gray-400 mt-1">
|
||||||
|
{hoveredIndex !== null ? data[hoveredIndex].label : "Total Cases"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend Grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 pt-2">
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={item.label}
|
||||||
|
className={`flex items-center justify-between p-2 rounded-lg transition-colors cursor-pointer ${
|
||||||
|
hoveredIndex === index ? "bg-gray-50" : ""
|
||||||
|
}`}
|
||||||
|
onMouseEnter={() => setHoveredIndex(index)}
|
||||||
|
onMouseLeave={() => setHoveredIndex(null)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="w-2.5 h-2.5 rounded-full shrink-0"
|
||||||
|
style={{ backgroundColor: item.color }}
|
||||||
|
/>
|
||||||
|
<span className="text-[12px] font-medium text-gray-700">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[12px] font-bold text-gray-900">
|
||||||
|
{item.percentage}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DisruptionMixChart;
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import type { ExposureChartItem } from "../DashboardTypes";
|
||||||
|
|
||||||
|
interface ExposureBarChartProps {
|
||||||
|
data: ExposureChartItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExposureBarChart: React.FC<ExposureBarChartProps> = ({ data }) => {
|
||||||
|
const [activeBar, setActiveBar] = useState<string | null>("Baggage");
|
||||||
|
|
||||||
|
const maxAmount = 60;
|
||||||
|
const yAxisTicks = ["$50", "$50", "$50", "$30", "$10"];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-[16px] border border-gray-100 p-6 shadow-[0_2px_4px_rgba(0,0,0,0.02)] flex flex-col justify-between h-full">
|
||||||
|
{/* Chart Title Header */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h2 className="text-[17px] font-bold text-gray-900 leading-snug">
|
||||||
|
Refund & Compensation Exposure
|
||||||
|
</h2>
|
||||||
|
<p className="text-[12px] text-gray-400 font-normal mt-0.5">
|
||||||
|
Real-time assessment log of refund and compensation cases.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bar Chart Body */}
|
||||||
|
<div className="relative w-full h-[280px] flex flex-col justify-end pt-8 pb-2">
|
||||||
|
{/* Horizontal Background Grid Lines */}
|
||||||
|
<div className="absolute inset-0 flex flex-col justify-between pointer-events-none pb-8 pt-8 pl-12 pr-4">
|
||||||
|
{yAxisTicks.map((_, idx) => (
|
||||||
|
<div key={idx} className="w-full border-b border-gray-100/80 h-0" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chart Content Area */}
|
||||||
|
<div className="flex w-full h-full relative z-10">
|
||||||
|
{/* Y-Axis Labels */}
|
||||||
|
<div className="flex flex-col justify-between pr-4 pb-8 text-[11px] font-medium text-gray-400 text-right w-12 select-none">
|
||||||
|
{yAxisTicks.map((tick, idx) => (
|
||||||
|
<span key={idx} className="leading-none">
|
||||||
|
{tick}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bars Grid */}
|
||||||
|
<div className="flex-1 grid grid-cols-4 items-end gap-6 px-4 pb-8 h-full">
|
||||||
|
{data.map((item) => {
|
||||||
|
const heightPercent = Math.min(100, Math.max(10, (item.amount / maxAmount) * 100));
|
||||||
|
const isSelected = activeBar === item.category || (item.hasTooltip && !activeBar);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.category}
|
||||||
|
className="relative flex flex-col items-center justify-end h-full group cursor-pointer"
|
||||||
|
onMouseEnter={() => setActiveBar(item.category)}
|
||||||
|
onMouseLeave={() => setActiveBar("Baggage")}
|
||||||
|
>
|
||||||
|
{/* Tooltip Bubble (Visible for selected bar e.g. Baggage) */}
|
||||||
|
{isSelected && (item.hasTooltip || activeBar === item.category) && (
|
||||||
|
<div className="absolute -top-14 z-20 flex flex-col items-center animate-fadeIn">
|
||||||
|
<div className="bg-white border border-gray-100 shadow-md rounded-[8px] px-3 py-1.5 text-center whitespace-nowrap min-w-[120px]">
|
||||||
|
<p className="text-[11px] font-bold text-gray-800 leading-tight">
|
||||||
|
{item.views || `${item.amount} cases`}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-gray-400 font-medium leading-tight mt-0.5">
|
||||||
|
{item.date || "Monday, April 22nd"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Tooltip Caret */}
|
||||||
|
<div className="w-2.5 h-2.5 bg-white border-r border-b border-gray-100 transform rotate-45 -mt-1.5 shadow-xs" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bar Element */}
|
||||||
|
<div
|
||||||
|
className="w-full max-w-[110px] rounded-t-[4px] transition-all duration-300 relative group-hover:brightness-105"
|
||||||
|
style={{
|
||||||
|
height: `${heightPercent}%`,
|
||||||
|
backgroundColor: item.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* X-Axis Category Labels */}
|
||||||
|
<div className="grid grid-cols-4 pl-12 pr-4 text-center">
|
||||||
|
{data.map((item) => (
|
||||||
|
<span
|
||||||
|
key={item.category}
|
||||||
|
className={`text-[12px] font-medium transition-colors ${
|
||||||
|
activeBar === item.category ? "text-gray-900 font-semibold" : "text-gray-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.category}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExposureBarChart;
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { RecentIncident } from "../DashboardTypes";
|
||||||
|
import { CustomTable, CustomStatus } from "../../../components/custom";
|
||||||
|
import type { Column } from "../../../components/custom/CustomTable";
|
||||||
|
|
||||||
|
interface RecentIncidentsTableProps {
|
||||||
|
incidents: RecentIncident[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RecentIncidentsTable: React.FC<RecentIncidentsTableProps> = ({ incidents }) => {
|
||||||
|
const columns: Column<RecentIncident>[] = [
|
||||||
|
{
|
||||||
|
header: "Recovery ID",
|
||||||
|
accessor: (row) => (
|
||||||
|
<span className="text-[13px] font-bold text-gray-900 font-mono">
|
||||||
|
{row.recoveryId}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Passenger / PNR",
|
||||||
|
accessor: (row) => (
|
||||||
|
<span className="text-[13px] font-bold text-gray-900">
|
||||||
|
{row.passengerName}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Flight",
|
||||||
|
accessor: (row) => (
|
||||||
|
<span className="text-[13px] font-bold text-gray-900">
|
||||||
|
{row.flightNumber}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Category",
|
||||||
|
accessor: (row) => (
|
||||||
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-[11px] font-semibold bg-[#EFEFEF] text-gray-600">
|
||||||
|
{row.category}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Status",
|
||||||
|
accessor: (row) => <CustomStatus status={row.status} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Value",
|
||||||
|
accessor: (row) => (
|
||||||
|
<span className="text-[13px] font-bold text-gray-900">
|
||||||
|
{row.value}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-[16px] border border-gray-100 p-6 shadow-[0_2px_4px_rgba(0,0,0,0.02)] flex flex-col gap-5">
|
||||||
|
{/* Table Header Section */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-[17px] font-bold text-gray-900 leading-snug">
|
||||||
|
Recent Recovery Incidents
|
||||||
|
</h2>
|
||||||
|
<p className="text-[12px] text-gray-400 font-normal mt-0.5">
|
||||||
|
Real-time assessment log of refund and compensation cases.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom Table Component */}
|
||||||
|
<CustomTable<RecentIncident>
|
||||||
|
columns={columns}
|
||||||
|
data={incidents}
|
||||||
|
itemName="incidents"
|
||||||
|
totalItems={incidents.length}
|
||||||
|
startIndex={incidents.length > 0 ? 1 : 0}
|
||||||
|
endIndex={incidents.length}
|
||||||
|
totalPages={1}
|
||||||
|
currentPage={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecentIncidentsTable;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { DashboardMetric } from "../DashboardTypes";
|
||||||
|
|
||||||
|
export const StatCard: React.FC<{ metric: DashboardMetric }> = ({ metric }) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-[14px] p-4 px-5 border border-gray-100/80 shadow-[0_2px_4px_rgba(0,0,0,0.02)] flex flex-col justify-between flex-1 min-w-[170px] hover:shadow-md transition-all duration-200">
|
||||||
|
<div>
|
||||||
|
<span className="text-[13px] font-medium text-gray-600 block">
|
||||||
|
{metric.title}
|
||||||
|
</span>
|
||||||
|
<span className="text-[26px] font-bold text-gray-900 leading-tight tracking-tight mt-1.5 block">
|
||||||
|
{metric.value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mt-3 pt-1">
|
||||||
|
<span className="text-[12px] font-bold text-[#1B9869] bg-[#E4FAE7]/60 px-2 py-0.5 rounded-full inline-flex items-center">
|
||||||
|
{metric.trend}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Sparkline curve */}
|
||||||
|
<div className="w-[60px] h-[26px] flex items-center justify-end">
|
||||||
|
<svg
|
||||||
|
width="60"
|
||||||
|
height="26"
|
||||||
|
viewBox="0 0 60 26"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="overflow-visible"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M 2 20 Q 12 22, 20 15 T 40 18 T 58 6"
|
||||||
|
stroke="#1B9869"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatCard;
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
function HomePage() {
|
export { StatCard } from "./StatCard";
|
||||||
return (
|
export { ExposureBarChart } from "./ExposureBarChart";
|
||||||
<section className="page home-page">
|
export { DisruptionMixChart } from "./DisruptionMixChart";
|
||||||
<h1>Home Page</h1>
|
export { RecentIncidentsTable } from "./RecentIncidentsTable";
|
||||||
<p>Welcome to the home page. Use the navigation links to switch pages.</p>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HomePage
|
|
||||||
|
|||||||
+138
-372
@@ -1,383 +1,149 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import CustomButton from "../../components/custom/CustomButton";
|
import { Skeleton } from "../../components/custom";
|
||||||
import CustomSearchableDropdown from "../../components/custom/CustomSearchableDropdown";
|
import {
|
||||||
import CustomDropdown from "../../components/custom/CustomDropdown";
|
StatCard,
|
||||||
import CustomMultiSelect from "../../components/custom/CustomMultiSelect";
|
ExposureBarChart,
|
||||||
import CustomModal from "../../components/custom/CustomModal";
|
DisruptionMixChart,
|
||||||
import CustomInput from "../../components/custom/CustomInput";
|
RecentIncidentsTable,
|
||||||
import CustomTextArea from "../../components/custom/CustomTextArea";
|
} from "./components";
|
||||||
import CustomStatus from "../../components/custom/CustomStatus";
|
import type {
|
||||||
import CustomCheckBox from "../../components/custom/CustomCheckBox";
|
DashboardMetric,
|
||||||
import CustomRadio from "../../components/custom/CustomRadio";
|
ExposureChartItem,
|
||||||
import CustomSwitch from "../../components/custom/CustomSwitch";
|
DisruptionMixItem,
|
||||||
import CustomTable, { type Column } from "../../components/custom/CustomTable";
|
RecentIncident,
|
||||||
import CustomActionMenu, { CustomActionItem } from "../../components/custom/CustomActionMenu";
|
} from "./DashboardTypes";
|
||||||
import { FileTextIcon, CaretRightIcon, CheckIcon, XIcon, CopyIcon, PencilSimpleIcon, TrashIcon, PlusIcon } from "@phosphor-icons/react";
|
import {
|
||||||
|
getDashboardMetrics,
|
||||||
|
getExposureData,
|
||||||
|
getDisruptionMixData,
|
||||||
|
getRecentIncidents,
|
||||||
|
} from "./DashboardApi";
|
||||||
|
|
||||||
function HomePage() {
|
export default function HomePage() {
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [metrics, setMetrics] = useState<DashboardMetric[]>([]);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [exposureData, setExposureData] = useState<ExposureChartItem[]>([]);
|
||||||
const [dropdownValue, setDropdownValue] = useState("active");
|
const [disruptionMix, setDisruptionMix] = useState<DisruptionMixItem[]>([]);
|
||||||
const [multiSelectValue, setMultiSelectValue] = useState<(string | number)[]>(["active"]);
|
const [recentIncidents, setRecentIncidents] = useState<RecentIncident[]>([]);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [isChecked, setIsChecked] = useState(true);
|
|
||||||
const [radioValue, setRadioValue] = useState("option1");
|
|
||||||
const [isSwitchOn, setIsSwitchOn] = useState(true);
|
|
||||||
|
|
||||||
const tableData = [
|
useEffect(() => {
|
||||||
{
|
async function loadDashboardData() {
|
||||||
name: "Strategic Accounts",
|
try {
|
||||||
description: "Key corporate account travelers.",
|
setLoading(true);
|
||||||
status: "active",
|
const [metricsRes, exposureRes, mixRes, incidentsRes] = await Promise.all([
|
||||||
lastModified: "4 Jun 2026, 4:09pm",
|
getDashboardMetrics(),
|
||||||
lastModifiedBy: "John Doe",
|
getExposureData(),
|
||||||
},
|
getDisruptionMixData(),
|
||||||
{
|
getRecentIncidents(),
|
||||||
name: "Families with Infants",
|
]);
|
||||||
description: "Passengers traveling with children < 2yrs.",
|
|
||||||
status: "active",
|
|
||||||
lastModified: "4 Jun 2026, 4:09pm",
|
|
||||||
lastModifiedBy: "John Doe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Inactive Users",
|
|
||||||
description: "Users who haven't booked in 12 months.",
|
|
||||||
status: "inactive",
|
|
||||||
lastModified: "3 Jun 2026, 2:15pm",
|
|
||||||
lastModifiedBy: "Jane Smith",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const tableColumns: Column<typeof tableData[0]>[] = [
|
setMetrics(metricsRes);
|
||||||
{
|
setExposureData(exposureRes);
|
||||||
header: "Cohort Name",
|
setDisruptionMix(mixRes);
|
||||||
accessor: (row) => <span className="font-bold text-gray-900">{row.name}</span>,
|
setRecentIncidents(incidentsRes);
|
||||||
},
|
} catch (error) {
|
||||||
{
|
console.error("Error loading dashboard data:", error);
|
||||||
header: "Description",
|
} finally {
|
||||||
accessor: "description",
|
setLoading(false);
|
||||||
},
|
}
|
||||||
{
|
}
|
||||||
header: "Status",
|
|
||||||
accessor: (row) => <CustomStatus status={row.status} />,
|
loadDashboardData();
|
||||||
},
|
}, []);
|
||||||
{
|
|
||||||
header: "Last Modified",
|
if (loading) {
|
||||||
accessor: "lastModified",
|
return (
|
||||||
sortable: true,
|
<div>
|
||||||
},
|
{/* Top Stat Cards Skeleton */}
|
||||||
{
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||||
header: "Last Modified By",
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
accessor: "lastModifiedBy",
|
<div key={i} className="bg-white rounded-[14px] p-5 border border-gray-100/80 flex flex-col justify-between h-[100px]">
|
||||||
filterable: true,
|
<div className="space-y-2">
|
||||||
},
|
<Skeleton width="60%" height={16} />
|
||||||
{
|
<Skeleton width="40%" height={28} />
|
||||||
header: "Action",
|
</div>
|
||||||
accessor: () => (
|
<div className="flex items-center justify-between mt-2">
|
||||||
<div className="flex justify-center w-full">
|
<Skeleton width={45} height={18} />
|
||||||
<CustomActionMenu>
|
<Skeleton width={50} height={20} />
|
||||||
<CustomActionItem variant="success" icon={<CheckIcon size={16} />}>Activate</CustomActionItem>
|
</div>
|
||||||
<CustomActionItem icon={<XIcon size={16} />}>Deactivate</CustomActionItem>
|
</div>
|
||||||
<CustomActionItem icon={<CopyIcon size={16} />}>Duplicate</CustomActionItem>
|
))}
|
||||||
<CustomActionItem icon={<PencilSimpleIcon size={16} />}>Edit</CustomActionItem>
|
|
||||||
<CustomActionItem variant="danger" icon={<TrashIcon size={16} />}>Delete</CustomActionItem>
|
|
||||||
</CustomActionMenu>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
|
||||||
className: "text-center w-24",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const options = [
|
{/* Middle Section Skeleton */}
|
||||||
{ label: "Draft", value: "draft" },
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-stretch">
|
||||||
{ label: "Active", value: "active" },
|
<div className="lg:col-span-8 bg-white rounded-[16px] border border-gray-100 p-6 flex flex-col justify-between h-[360px]">
|
||||||
{ label: "Inactive", value: "inactive" },
|
<div className="space-y-2">
|
||||||
];
|
<Skeleton width="40%" height={22} />
|
||||||
|
<Skeleton width="60%" height={14} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end gap-6 h-[240px] pt-8">
|
||||||
|
<Skeleton className="flex-1" height="60%" />
|
||||||
|
<Skeleton className="flex-1" height="90%" />
|
||||||
|
<Skeleton className="flex-1" height="40%" />
|
||||||
|
<Skeleton className="flex-1" height="85%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-4 bg-white rounded-[16px] border border-gray-100 p-6 flex flex-col justify-between h-[360px]">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton width="50%" height={22} />
|
||||||
|
<Skeleton width="70%" height={14} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center my-4">
|
||||||
|
<Skeleton variant="circular" width={160} height={160} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} height={24} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Table Skeleton */}
|
||||||
|
<div className="bg-white rounded-[16px] border border-gray-100 p-6 space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton width="30%" height={22} />
|
||||||
|
<Skeleton width="50%" height={14} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3 pt-2">
|
||||||
|
<Skeleton height={40} />
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<Skeleton key={i} height={48} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 max-w-5xl mx-auto flex flex-col gap-8">
|
<div className="p-4 md:p-6 lg:p-4 space-y-6">
|
||||||
|
{/* 1. Top Stat Cards Row */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||||
|
{metrics.map((metric) => (
|
||||||
|
<StatCard key={metric.id} metric={metric} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2. Middle Section: Refund Exposure + Disruption Mix */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-stretch">
|
||||||
|
{/* Refund & Compensation Exposure (approx 7 cols) */}
|
||||||
|
<div className="lg:col-span-8 flex flex-col">
|
||||||
|
<ExposureBarChart data={exposureData} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Disruption Mix (approx 4 cols) */}
|
||||||
|
<div className="lg:col-span-4 flex flex-col">
|
||||||
|
<DisruptionMixChart data={disruptionMix} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 3. Bottom Section: Recent Recovery Incidents */}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold mb-2 text-gray-900">Dashboard (UI Test Page)</h1>
|
<RecentIncidentsTable incidents={recentIncidents} />
|
||||||
<p className="text-gray-500">Test all button variants and the custom components here.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
|
||||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Table Component</h2>
|
|
||||||
|
|
||||||
<CustomTable
|
|
||||||
columns={tableColumns}
|
|
||||||
data={tableData}
|
|
||||||
searchValue={searchValue}
|
|
||||||
onSearchChange={setSearchValue}
|
|
||||||
searchPlaceholder="Search cohorts..."
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={3}
|
|
||||||
totalItems={12}
|
|
||||||
startIndex={1}
|
|
||||||
endIndex={3}
|
|
||||||
onPageChange={setCurrentPage}
|
|
||||||
itemName="cohorts"
|
|
||||||
rightHeaderActions={
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="w-40">
|
|
||||||
<CustomDropdown
|
|
||||||
options={options}
|
|
||||||
value="active"
|
|
||||||
placeholder="Choose Status"
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<CustomButton variant="primary" size="md" leftIcon={<PlusIcon size={16} />}>
|
|
||||||
Create Cohort
|
|
||||||
</CustomButton>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
|
||||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Modal Components</h2>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4 items-center">
|
|
||||||
<CustomButton variant="primary" onClick={() => setIsModalOpen(true)}>
|
|
||||||
Open Test Modal
|
|
||||||
</CustomButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
|
||||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Status Pills & Toggles</h2>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex flex-wrap gap-4 items-center">
|
|
||||||
<span className="text-sm font-semibold w-24">Status Pills:</span>
|
|
||||||
<CustomStatus status="active" />
|
|
||||||
<CustomStatus status="inactive" />
|
|
||||||
<CustomStatus status="pending" />
|
|
||||||
<CustomStatus status="shipped" />
|
|
||||||
<CustomStatus status="neutral" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4 items-center">
|
|
||||||
<span className="text-sm font-semibold w-24">Checkboxes:</span>
|
|
||||||
<CustomCheckBox checked={isChecked} onChange={() => setIsChecked(!isChecked)} label="Checked State" />
|
|
||||||
<CustomCheckBox checked={false} onChange={() => {}} label="Unchecked State" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4 items-center">
|
|
||||||
<span className="text-sm font-semibold w-24">Radios:</span>
|
|
||||||
<CustomRadio checked={radioValue === "option1"} onChange={() => setRadioValue("option1")} label="Option 1" />
|
|
||||||
<CustomRadio checked={radioValue === "option2"} onChange={() => setRadioValue("option2")} label="Option 2" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4 items-center">
|
|
||||||
<span className="text-sm font-semibold w-24">Switches:</span>
|
|
||||||
<CustomSwitch checked={isSwitchOn} onChange={() => setIsSwitchOn(!isSwitchOn)} label="Toggle Feature" />
|
|
||||||
<CustomSwitch checked={false} onChange={() => {}} label="Off Toggle" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
|
||||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Button Variants</h2>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4 items-center">
|
|
||||||
<CustomButton variant="primary">Primary</CustomButton>
|
|
||||||
<CustomButton variant="secondary">Secondary</CustomButton>
|
|
||||||
<CustomButton variant="outlined">Outlined</CustomButton>
|
|
||||||
<CustomButton variant="text">Text Button</CustomButton>
|
|
||||||
<CustomButton variant="link">Link Button</CustomButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4 items-center mt-2">
|
|
||||||
<CustomButton variant="primary" size="sm">Small</CustomButton>
|
|
||||||
<CustomButton variant="primary" size="md">Medium</CustomButton>
|
|
||||||
<CustomButton variant="primary" size="lg">Large</CustomButton>
|
|
||||||
<CustomButton variant="primary" loading>Loading</CustomButton>
|
|
||||||
<CustomButton variant="primary" disabled>Disabled</CustomButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
|
||||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Input & TextArea Variants</h2>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-6 items-end">
|
|
||||||
<div className="w-48">
|
|
||||||
<CustomInput
|
|
||||||
label="Small (sm)"
|
|
||||||
size="sm"
|
|
||||||
placeholder="Enter text..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-56">
|
|
||||||
<CustomInput
|
|
||||||
label="Medium (md)"
|
|
||||||
size="md"
|
|
||||||
placeholder="Enter text..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-64">
|
|
||||||
<CustomInput
|
|
||||||
label="Large (lg)"
|
|
||||||
size="lg"
|
|
||||||
placeholder="Enter text..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-6 items-end mt-4">
|
|
||||||
<div className="w-48">
|
|
||||||
<CustomTextArea
|
|
||||||
label="Small (sm)"
|
|
||||||
size="sm"
|
|
||||||
placeholder="Enter text..."
|
|
||||||
rows={2}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-56">
|
|
||||||
<CustomTextArea
|
|
||||||
label="Medium (md)"
|
|
||||||
size="md"
|
|
||||||
placeholder="Enter text..."
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-64">
|
|
||||||
<CustomTextArea
|
|
||||||
label="Large (lg)"
|
|
||||||
size="lg"
|
|
||||||
placeholder="Enter text..."
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
|
||||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Standard Dropdown Variants</h2>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-6 items-end">
|
|
||||||
<div className="w-48">
|
|
||||||
<CustomDropdown
|
|
||||||
label="Small (sm)"
|
|
||||||
size="sm"
|
|
||||||
options={options}
|
|
||||||
value={dropdownValue}
|
|
||||||
onChange={setDropdownValue}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-56">
|
|
||||||
<CustomDropdown
|
|
||||||
label="Medium (md)"
|
|
||||||
size="md"
|
|
||||||
options={options}
|
|
||||||
value={dropdownValue}
|
|
||||||
onChange={setDropdownValue}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6">
|
|
||||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Searchable Dropdown Variants</h2>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-6 items-end">
|
|
||||||
<div className="w-48">
|
|
||||||
<CustomSearchableDropdown
|
|
||||||
label="Small (sm)"
|
|
||||||
size="sm"
|
|
||||||
options={options}
|
|
||||||
value={dropdownValue}
|
|
||||||
onChange={setDropdownValue}
|
|
||||||
placeholder="Small size..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-56">
|
|
||||||
<CustomSearchableDropdown
|
|
||||||
label="Medium (md)"
|
|
||||||
size="md"
|
|
||||||
options={options}
|
|
||||||
value={dropdownValue}
|
|
||||||
onChange={setDropdownValue}
|
|
||||||
placeholder="Medium size..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-64">
|
|
||||||
<CustomSearchableDropdown
|
|
||||||
label="Large (lg)"
|
|
||||||
size="lg"
|
|
||||||
options={options}
|
|
||||||
value={dropdownValue}
|
|
||||||
onChange={setDropdownValue}
|
|
||||||
placeholder="Large size..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-8 rounded-[20px] border border-gray-100 shadow-sm flex flex-col gap-6 mb-32">
|
|
||||||
<h2 className="text-[15px] font-bold text-gray-900 border-b border-gray-100 pb-3">Multi-Select Dropdown</h2>
|
|
||||||
|
|
||||||
<div className="w-[400px]">
|
|
||||||
<CustomMultiSelect
|
|
||||||
label="Select Multiple Statuses"
|
|
||||||
options={options}
|
|
||||||
value={multiSelectValue}
|
|
||||||
onChange={setMultiSelectValue}
|
|
||||||
placeholder="Select statuses..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CustomModal
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
onClose={() => setIsModalOpen(false)}
|
|
||||||
title="New Dynamic Cohorts"
|
|
||||||
description="Configure targeting criteria for high-precision recovery."
|
|
||||||
icon={<FileTextIcon size={24} />}
|
|
||||||
primaryAction={{
|
|
||||||
label: "Next Step",
|
|
||||||
onClick: () => setIsModalOpen(false),
|
|
||||||
icon: <CaretRightIcon size={18} />
|
|
||||||
}}
|
|
||||||
secondaryAction={{
|
|
||||||
label: "Cancel",
|
|
||||||
onClick: () => setIsModalOpen(false)
|
|
||||||
}}
|
|
||||||
size="md"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-6 h-64">
|
|
||||||
<div className="flex justify-between border-b border-gray-100 pb-2">
|
|
||||||
<div>
|
|
||||||
<span className="text-[10px] font-bold text-[#1B9869] tracking-wider uppercase">1 - Information</span>
|
|
||||||
<h3 className="text-sm font-bold text-[#1B9869]">Cohort Identity</h3>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<span className="text-[10px] font-bold text-gray-400 tracking-wider uppercase">2 - Targeting</span>
|
|
||||||
<h3 className="text-sm font-bold text-gray-400">Targeting Criteria</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="w-full">
|
|
||||||
<label className="text-xs font-bold text-gray-700 mb-1 block">Cohort Name</label>
|
|
||||||
<input type="text" placeholder="Enter" className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#1B9869]" />
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<CustomDropdown
|
|
||||||
label="Status"
|
|
||||||
options={[{label: "Selected Option", value: "selected"}]}
|
|
||||||
value="selected"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<label className="text-xs font-bold text-gray-700 mb-1 block">Description</label>
|
|
||||||
<textarea placeholder="Enter" className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm h-24 focus:outline-none focus:border-[#1B9869]"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CustomModal>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HomePage
|
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { UsersIcon, BriefcaseIcon, BuildingsIcon } from "@phosphor-icons/react";
|
import {
|
||||||
|
AirplaneTiltIcon,
|
||||||
|
ShieldCheckIcon,
|
||||||
|
ArrowsClockwiseIcon,
|
||||||
|
UsersFourIcon,
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
|
|
||||||
const CustomAppLoader = () => {
|
const CustomAppLoader = () => {
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
|
||||||
// HRM related icons
|
// AeroResolve domain icons
|
||||||
const icons = [
|
const icons = [
|
||||||
{ component: UsersIcon, key: "users" },
|
{ component: AirplaneTiltIcon, key: "flight", label: "Flight Operations" },
|
||||||
{ component: BriefcaseIcon, key: "briefcase" },
|
{ component: ArrowsClockwiseIcon, key: "recovery", label: "Recovery Incidents" },
|
||||||
{ component: BuildingsIcon, key: "building" },
|
{ component: ShieldCheckIcon, key: "policy", label: "Policy Engine" },
|
||||||
|
{ component: UsersFourIcon, key: "cohorts", label: "Passenger Cohorts" },
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setCurrentIndex((prev) => (prev + 1) % icons.length);
|
setCurrentIndex((prev) => (prev + 1) % icons.length);
|
||||||
}, 800); // Slower, smoother transition
|
}, 700);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [icons.length]);
|
}, [icons.length]);
|
||||||
@@ -22,28 +28,34 @@ const CustomAppLoader = () => {
|
|||||||
const ActiveIcon = icons[currentIndex].component;
|
const ActiveIcon = icons[currentIndex].component;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center space-y-6 h-screen w-full ">
|
<div className="flex flex-col items-center justify-center space-y-6 min-h-[400px] h-screen w-full bg-[#F8FAFC] font-sans">
|
||||||
<div className="relative flex items-center justify-center">
|
<div className="relative flex items-center justify-center">
|
||||||
|
{/* Outer Glowing Pulsing Aura */}
|
||||||
|
<div className="absolute w-24 h-24 bg-[#1B9869]/10 rounded-full animate-ping pointer-events-none" />
|
||||||
|
|
||||||
{/* Outer Spinning Ring */}
|
{/* Outer Spinning Ring */}
|
||||||
<div className="w-20 h-20 border-4 border-gray-200 border-t-[#0B3B6A] rounded-full animate-spin"></div>
|
<div className="w-20 h-20 border-4 border-gray-200/80 border-t-[#1B9869] border-r-[#1B9869]/40 rounded-full animate-spin" />
|
||||||
|
|
||||||
{/* Icon Container */}
|
{/* Icon Container */}
|
||||||
<div className="absolute flex items-center justify-center">
|
<div className="absolute flex items-center justify-center w-12 h-12 bg-white rounded-full shadow-sm border border-gray-100">
|
||||||
<ActiveIcon
|
<ActiveIcon
|
||||||
key={icons[currentIndex].key}
|
key={icons[currentIndex].key}
|
||||||
size={32}
|
size={28}
|
||||||
className="text-[#0B3B6A] transition-all duration-500 opacity-100"
|
className="text-[#1B9869] transition-all duration-300 transform scale-100"
|
||||||
strokeWidth={1.5}
|
weight="bold"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="flex flex-col items-center gap-1.5 text-center">
|
||||||
<h3 className="text-[#0B3B6A] font-semibold text-lg tracking-wide">
|
<h3 className="text-gray-900 font-bold text-xl tracking-tight flex items-center gap-2">
|
||||||
HRM System
|
Aero<span className="text-[#1B9869]">Resolve</span>
|
||||||
</h3>
|
</h3>
|
||||||
<span className="text-gray-500 text-sm animate-pulse">
|
<span className="text-gray-500 text-xs font-semibold uppercase tracking-wider">
|
||||||
Loading resources...
|
Passenger Recovery & Resolution Platform
|
||||||
|
</span>
|
||||||
|
<span className="text-[13px] font-medium text-[#1B9869] animate-pulse mt-2">
|
||||||
|
Loading {icons[currentIndex].label}...
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user