feat: implement dashboard UI components, data types, and API integration

This commit is contained in:
Syed Waseem
2026-08-13 11:09:06 +05:30
parent d9f94b3278
commit 4b6622f7d8
10 changed files with 596 additions and 419 deletions
+26 -20
View File
@@ -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>
) )
} }
+23
View File
@@ -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');
}
+36
View File
@@ -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;
+45
View File
@@ -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;
+4 -10
View File
@@ -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
View File
@@ -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
+29 -17
View File
@@ -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>