feat: enhance cohort management using custom components

This commit is contained in:
amee
2026-07-08 05:22:24 +05:30
parent 04a6607904
commit 924aede2ca
8 changed files with 436 additions and 182 deletions
@@ -10,6 +10,9 @@ export interface CohartResponse {
description: string;
membersCount: number;
createdAt: string;
status: 'active' | 'inactive' | 'draft' | 'published';
updatedAt: string;
lastModifiedBy: string;
}
export interface CreateCohartPayload {
@@ -20,4 +23,5 @@ export interface CreateCohartPayload {
export interface UpdateCohartPayload {
name?: string;
description?: string;
status?: 'active' | 'inactive' | 'draft' | 'published';
}
@@ -0,0 +1,407 @@
import { useMemo, useEffect, useState } from 'react';
import { Plus, Trash2, Search, ListFilter, Check, X, Copy, Pencil } from 'lucide-react';
import {
CustomTable,
CustomInput,
CustomDropdown,
CustomButton,
CustomStatus,
CustomActionMenu,
CustomActionItem,
CustomConfirmationModal,
CustomAlertBanner,
Skeleton,
} from '../../../components/custom';
import type { Column } from '../../../components/custom/CustomTable';
import { listCoharts, deleteCohart, updateCohart } from '../CohartManageApi';
import type { CohartResponse } from '../CohartManageTypes';
const PAGE_SIZE = 5;
const MOCK_COHORTS: CohartResponse[] = [
{
id: '1',
name: 'Strategic Accounts',
description: 'Key corporate account travelers.',
membersCount: 245,
createdAt: '2026-06-04T16:09:00Z',
status: 'active',
updatedAt: '2026-06-04T16:09:00Z',
lastModifiedBy: 'John Doe',
},
{
id: '2',
name: 'Families with Infants',
description: 'Passengers traveling with children < 2yrs.',
membersCount: 182,
createdAt: '2026-06-04T16:09:00Z',
status: 'active',
updatedAt: '2026-06-04T16:09:00Z',
lastModifiedBy: 'John Doe',
},
{
id: '3',
name: 'Strategic Accounts',
description: 'Key corporate account travelers.',
membersCount: 310,
createdAt: '2026-06-04T16:09:00Z',
status: 'active',
updatedAt: '2026-06-04T16:09:00Z',
lastModifiedBy: 'John Doe',
},
{
id: '4',
name: 'Premium Frequent Flyers',
description: 'Passengers with 10+ flights in the last 12 months.',
membersCount: 98,
createdAt: '2026-06-03T10:30:00Z',
status: 'inactive',
updatedAt: '2026-06-03T10:30:00Z',
lastModifiedBy: 'Jane Smith',
},
{
id: '5',
name: 'Elite Status Members',
description: 'Gold and Platinum tier loyalty programme members.',
membersCount: 512,
createdAt: '2026-06-02T08:45:00Z',
status: 'draft',
updatedAt: '2026-06-02T08:45:00Z',
lastModifiedBy: 'Jane Smith',
},
{
id: '6',
name: 'Medical Assistance',
description: 'Passengers requiring wheelchair or medical support.',
membersCount: 73,
createdAt: '2026-05-30T11:20:00Z',
status: 'published',
updatedAt: '2026-05-30T11:20:00Z',
lastModifiedBy: 'Mark Lee',
},
{
id: '7',
name: 'Unaccompanied Minors',
description: 'Children aged 514 traveling without a guardian.',
membersCount: 57,
createdAt: '2026-05-28T14:00:00Z',
status: 'active',
updatedAt: '2026-05-28T14:00:00Z',
lastModifiedBy: 'Mark Lee',
},
];
const STATUS_OPTIONS = [
{ label: 'Choose Status', value: '' },
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
];
function formatDate(iso: string): string {
if (!iso) return '—';
return new Date(iso)
.toLocaleString('en-GB', {
day: 'numeric',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
})
.replace(',', '');
}
function headerLabel(text: string) {
return (
<span className="text-[12px] font-semibold text-[#6C766D] tracking-[0px]">{text}</span>
);
}
function cellText(text: string) {
return (
<span className="text-[12px] font-medium text-[#676767] leading-[15px] tracking-[0px]">{text}</span>
);
}
type SortKey = 'updatedAt' | 'lastModifiedBy';
interface SortHeaderProps {
label: string;
colKey: SortKey;
sortKey: SortKey | null;
onSort: (key: SortKey) => void;
}
function SortHeader({ label, colKey, sortKey, onSort }: SortHeaderProps) {
return (
<button
type="button"
onClick={() => onSort(colKey)}
className="flex items-center gap-1.5 group text-[12px] font-semibold text-[#6C766D] tracking-[0px]"
>
{label}
<ListFilter
size={13}
className={`transition-colors ${
sortKey === colKey ? 'text-gray-900' : 'text-[#6C766D] group-hover:text-gray-700'
}`}
/>
</button>
);
}
export default function CohortList() {
const [allCohorts, setAllCohorts] = useState<CohartResponse[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [sortKey, setSortKey] = useState<SortKey | null>(null);
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
const [currentPage, setCurrentPage] = useState(1);
const [deleteTarget, setDeleteTarget] = useState<CohartResponse | null>(null);
const [deactivateTarget, setDeactivateTarget] = useState<CohartResponse | null>(null);
const [deleteLoading, setDeleteLoading] = useState(false);
const [statusLoading, setStatusLoading] = useState(false);
useEffect(() => {
listCoharts()
.then(setAllCohorts)
.catch(() => setAllCohorts(MOCK_COHORTS))
.finally(() => setLoading(false));
}, []);
const handleSort = (key: SortKey) => {
if (sortKey === key) {
setSortDir(d => (d === 'asc' ? 'desc' : 'asc'));
} else {
setSortKey(key);
setSortDir('asc');
}
};
const handleSearchChange = (val: string) => {
setSearch(val);
setCurrentPage(1);
};
const handleStatusFilter = (val: string) => {
setStatusFilter(val);
setCurrentPage(1);
};
const handleDelete = async () => {
if (!deleteTarget) return;
setDeleteLoading(true);
try {
await deleteCohart(deleteTarget.id);
setAllCohorts(prev => prev.filter(c => c.id !== deleteTarget.id));
setDeleteTarget(null);
} catch {
setError('Failed to delete cohort. Please try again.');
} finally {
setDeleteLoading(false);
}
};
const handleToggleStatus = async () => {
if (!deactivateTarget) return;
setStatusLoading(true);
try {
const newStatus = deactivateTarget.status === 'active' ? 'inactive' : 'active';
const updated = await updateCohart(deactivateTarget.id, { status: newStatus });
setAllCohorts(prev => prev.map(c => (c.id === deactivateTarget.id ? updated : c)));
setDeactivateTarget(null);
} catch {
setError('Failed to update cohort status. Please try again.');
} finally {
setStatusLoading(false);
}
};
const filtered = useMemo(
() =>
allCohorts
.filter(c => c.name.toLowerCase().includes(search.toLowerCase()))
.filter(c => (statusFilter ? c.status === statusFilter : true)),
[allCohorts, search, statusFilter]
);
const sorted = useMemo(() => {
if (!sortKey) return filtered;
return [...filtered].sort((a, b) => {
const aVal = sortKey === 'updatedAt'
? new Date(a.updatedAt).getTime()
: a.lastModifiedBy.toLowerCase();
const bVal = sortKey === 'updatedAt'
? new Date(b.updatedAt).getTime()
: b.lastModifiedBy.toLowerCase();
if (aVal < bVal) return sortDir === 'asc' ? -1 : 1;
if (aVal > bVal) return sortDir === 'asc' ? 1 : -1;
return 0;
});
}, [filtered, sortKey, sortDir]);
const totalItems = sorted.length;
const totalPages = Math.max(1, Math.ceil(totalItems / PAGE_SIZE));
const startIndex = totalItems > 0 ? (currentPage - 1) * PAGE_SIZE + 1 : 0;
const endIndex = Math.min(currentPage * PAGE_SIZE, totalItems);
const paged = sorted.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
const columns: Column<CohartResponse>[] = [
{
header: headerLabel('Cohort Name'),
accessor: row => (
<span className="text-[14px] font-semibold text-[#0F172B] leading-[18px] tracking-[0px]">
{row.name}
</span>
),
},
{
header: headerLabel('Description'),
accessor: row => cellText(row.description),
},
{
header: headerLabel('Status'),
accessor: row => <CustomStatus status={row.status} />,
},
{
header: <SortHeader label="Last Modified" colKey="updatedAt" sortKey={sortKey} onSort={handleSort} />,
accessor: row => cellText(formatDate(row.updatedAt)),
},
{
header: <SortHeader label="Last Modified By" colKey="lastModifiedBy" sortKey={sortKey} onSort={handleSort} />,
accessor: row => cellText(row.lastModifiedBy),
},
{
header: headerLabel('Action'),
className: 'text-right',
accessor: row => (
<CustomActionMenu>
<CustomActionItem icon={<Check size={15} />} variant="success" onClick={() => setDeactivateTarget(row)}>
Activate
</CustomActionItem>
<CustomActionItem icon={<X size={15} />} onClick={() => setDeactivateTarget(row)}>
Deactivate
</CustomActionItem>
<CustomActionItem icon={<Copy size={15} />} onClick={() => {}}>
Duplicate
</CustomActionItem>
<CustomActionItem icon={<Pencil size={15} />} onClick={() => {}}>
Edit
</CustomActionItem>
<CustomActionItem icon={<Trash2 size={15} />} variant="danger" onClick={() => setDeleteTarget(row)}>
Delete
</CustomActionItem>
</CustomActionMenu>
),
},
];
if (loading) {
return (
<div className="w-full flex flex-col bg-white rounded-[20px] shadow-sm border border-gray-100 overflow-hidden">
<div className="flex items-center justify-between p-4 border-b border-gray-100">
<Skeleton width={320} height={36} />
<div className="flex items-center gap-3">
<Skeleton width={140} height={36} />
<Skeleton width={148} height={36} />
</div>
</div>
<div className="p-6 flex flex-col gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} height={52} />
))}
</div>
</div>
);
}
return (
<>
{error && (
<CustomAlertBanner
message={error}
type="error"
onClose={() => setError(null)}
/>
)}
<CustomTable<CohartResponse>
columns={columns}
data={paged}
leftHeaderActions={
<div className="w-[380px]">
<CustomInput
placeholder="Search cohorts..."
value={search}
onChange={e => handleSearchChange(e.target.value)}
leftIcon={<Search size={16} />}
className="!bg-[#F3F6F5] !rounded-[10px] !h-[38px] !border !border-[#E5E7EB]"
containerClassName="!gap-0"
/>
</div>
}
rightHeaderActions={
<>
<div className="w-44">
<CustomDropdown
options={STATUS_OPTIONS}
value={statusFilter}
onChange={handleStatusFilter}
placeholder="Choose Status"
size="md"
/>
</div>
<CustomButton
variant="primary"
size="md"
leftIcon={<Plus size={16} />}
className="!rounded-[10px] !gap-[10px] !h-[40px]"
onClick={() => {}}
>
Create Cohort
</CustomButton>
</>
}
currentPage={currentPage}
totalPages={totalPages}
totalItems={totalItems}
startIndex={startIndex}
endIndex={endIndex}
onPageChange={setCurrentPage}
itemName="cohorts"
/>
<CustomConfirmationModal
isOpen={!!deleteTarget}
onClose={() => setDeleteTarget(null)}
onConfirm={handleDelete}
title="Delete Cohort"
description={`"${deleteTarget?.name}" will be permanently removed and cannot be recovered.`}
confirmText="Delete"
cancelText="Cancel"
variant="danger"
isLoading={deleteLoading}
/>
<CustomConfirmationModal
isOpen={!!deactivateTarget}
onClose={() => setDeactivateTarget(null)}
onConfirm={handleToggleStatus}
title={deactivateTarget?.status === 'active' ? 'Deactivate Cohort' : 'Activate Cohort'}
description={
deactivateTarget?.status === 'active'
? `"${deactivateTarget?.name}" will be deactivated and removed from active targeting.`
: `"${deactivateTarget?.name}" will be reactivated and available for targeting.`
}
confirmText={deactivateTarget?.status === 'active' ? 'Deactivate' : 'Activate'}
cancelText="Cancel"
variant="warning"
isLoading={statusLoading}
/>
</>
);
}
+2 -161
View File
@@ -1,164 +1,5 @@
import { useState } from 'react';
import { Search, Plus, ChevronLeft, ChevronRight, MoreHorizontal, Eye, Edit, Trash2 } from 'lucide-react';
import AddCohart from './components/AddCohart';
const INITIAL_COHORTS = [
{
id: 1,
name: 'Strategic Accounts',
description: 'Key corporate account travelers.',
status: 'Active',
lastModified: '4 Jun 2026, 4:09pm',
modifiedBy: 'John Doe',
},
{
id: 2,
name: 'Families with Infants',
description: 'Passengers traveling with children < 2yrs.',
status: 'Active',
lastModified: '4 Jun 2026, 4:09pm',
modifiedBy: 'John Doe',
},
{
id: 3,
name: 'Strategic Accounts',
description: 'Key corporate account travelers.',
status: 'Active',
lastModified: '4 Jun 2026, 4:09pm',
modifiedBy: 'John Doe',
},
];
import CohortList from './components/cohartList';
export default function CohortManage() {
const [cohorts, setCohorts] = useState(INITIAL_COHORTS);
const [isAddOpen, setIsAddOpen] = useState(false);
const [actionMenuOpen, setActionMenuOpen] = useState<number | null>(null);
const handleAddCohort = (newCohort: any) => {
setCohorts([newCohort, ...cohorts]);
};
const handleDelete = (id: number) => {
setCohorts(cohorts.filter(c => c.id !== id));
setActionMenuOpen(null);
};
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-100 flex flex-col min-h-[600px]">
{/* Top Controls */}
<div className="p-4 border-b border-gray-100 flex items-center justify-between gap-4">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="Search cohorts..."
className="w-full pl-10 pr-4 py-2 bg-gray-50 border-none rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500/20"
/>
</div>
<div className="flex items-center gap-4">
<select className="px-4 py-2 bg-white border border-gray-200 rounded-lg text-sm text-gray-600 focus:outline-none focus:border-gray-300">
<option>Choose Status</option>
<option>Active</option>
<option>Inactive</option>
</select>
<button
onClick={() => setIsAddOpen(true)}
className="flex items-center gap-2 px-4 py-2 bg-[#1B9869] hover:bg-[#157a54] text-white text-sm font-medium rounded-lg transition-colors"
>
<Plus size={18} />
Create Cohort
</button>
</div>
</div>
{/* Table */}
<div className="flex-1 overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-gray-100">
<th className="px-6 py-4 text-xs font-semibold text-gray-500 w-[20%]">Cohort Name</th>
<th className="px-6 py-4 text-xs font-semibold text-gray-500 w-[30%]">Description</th>
<th className="px-6 py-4 text-xs font-semibold text-gray-500 w-[15%]">Status</th>
<th className="px-6 py-4 text-xs font-semibold text-gray-500 w-[15%]">Last Modified</th>
<th className="px-6 py-4 text-xs font-semibold text-gray-500 w-[15%]">Last Modified By</th>
<th className="px-6 py-4 text-xs font-semibold text-gray-500 text-center w-[5%]">Action</th>
</tr>
</thead>
<tbody>
{cohorts.map((cohort) => (
<tr key={cohort.id} className="border-b border-gray-50 hover:bg-gray-50/50 transition-colors">
<td className="px-6 py-4 text-sm font-bold text-gray-900">{cohort.name}</td>
<td className="px-6 py-4 text-sm text-gray-500">{cohort.description}</td>
<td className="px-6 py-4">
<div className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-green-50 text-green-700 text-xs font-medium border border-green-100">
<div className="w-1.5 h-1.5 rounded-full bg-green-500" />
{cohort.status}
</div>
</td>
<td className="px-6 py-4 text-sm text-gray-500">{cohort.lastModified}</td>
<td className="px-6 py-4 text-sm text-gray-500">{cohort.modifiedBy}</td>
<td className="px-6 py-4 text-center relative">
<button
onClick={() => setActionMenuOpen(actionMenuOpen === cohort.id ? null : cohort.id)}
className="text-gray-400 hover:text-gray-600 transition-colors p-2 rounded-lg hover:bg-gray-100"
>
<MoreHorizontal size={18} />
</button>
{actionMenuOpen === cohort.id && (
<div className="absolute right-8 top-10 w-48 bg-white border border-gray-100 shadow-xl rounded-xl z-10 py-2 overflow-hidden">
<button className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors text-left font-medium">
<Eye size={16} className="text-[#3B82F6]" />
View Audience
</button>
<button className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors text-left font-medium">
<Edit size={16} className="text-yellow-500" />
Edit Targeting
</button>
<button
onClick={() => handleDelete(cohort.id)}
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 transition-colors text-left font-medium"
>
<Trash2 size={16} />
Delete Permanently
</button>
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="p-4 border-t border-gray-100 flex items-center justify-between bg-gray-50/50 rounded-b-xl">
<span className="text-sm text-gray-500">Showing 1 to 5 of 128 orders</span>
<div className="flex items-center gap-1">
<button className="w-8 h-8 flex items-center justify-center rounded border border-gray-200 text-gray-500 hover:bg-gray-100 transition-colors bg-white">
<ChevronLeft size={16} />
</button>
<button className="w-8 h-8 flex items-center justify-center rounded border border-[#1B9869] bg-[#1B9869] text-white text-sm font-medium transition-colors">
1
</button>
<button className="w-8 h-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors bg-white text-sm">
2
</button>
<button className="w-8 h-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors bg-white text-sm">
3
</button>
<button className="w-8 h-8 flex items-center justify-center rounded border border-gray-200 text-gray-500 hover:bg-gray-100 transition-colors bg-white">
<ChevronRight size={16} />
</button>
</div>
</div>
<AddCohart
isOpen={isAddOpen}
onClose={() => setIsAddOpen(false)}
onAdd={handleAddCohort}
/>
</div>
);
return <CohortList />;
}
+3 -3
View File
@@ -83,7 +83,7 @@ export const CustomActionMenu: React.FC<CustomActionMenuProps> = ({
e.stopPropagation();
setIsOpen(!isOpen);
}}
className={`flex h-8 w-8 items-center justify-center rounded-full transition-colors focus:outline-none ${isOpen ? 'bg-gray-100 text-gray-900' : 'text-gray-400 hover:bg-gray-100 hover:text-gray-900'}`}
className={`flex h-8 w-8 items-center justify-center rounded-full transition-colors focus:outline-none ${isOpen ? 'bg-gray-100 text-black' : 'text-black hover:bg-gray-100'}`}
aria-label="Actions"
>
<MoreVertical size={18} />
@@ -127,12 +127,12 @@ export const CustomActionItem: React.FC<CustomActionItemProps> = ({
const variantClasses = {
default: "text-gray-700 hover:bg-gray-50 hover:text-gray-900",
success: "text-[#1B9869] bg-[#EBF7F2] hover:brightness-95",
danger: "text-red-600 hover:bg-red-50 hover:text-red-700",
danger: "text-red-600 bg-red-50 hover:brightness-95",
};
return (
<div
className={`group flex items-center gap-2.5 w-full cursor-pointer px-3 py-2 text-[13px] font-medium transition-all rounded-md ${variantClasses[variant]}`}
className={`flex items-center gap-2.5 w-full cursor-pointer px-3 py-2 text-[13px] font-medium transition-all rounded-md ${variantClasses[variant]}`}
onClick={(e) => {
e.stopPropagation();
onClick?.();
+7 -5
View File
@@ -67,7 +67,9 @@ const CustomDropdown = React.forwardRef<HTMLDivElement, CustomDropdownProps>(
setIsOpen(false);
};
const selectedOption = options.find((opt) => String(opt.value) === String(value));
const selectedOption = value !== '' && value !== null && value !== undefined
? options.find((opt) => String(opt.value) === String(value))
: undefined;
return (
<div className="w-full flex flex-col gap-1.5" ref={ref}>
@@ -83,7 +85,7 @@ const CustomDropdown = React.forwardRef<HTMLDivElement, CustomDropdownProps>(
onClick={() => !disabled && setIsOpen(!isOpen)}
className={`
w-full rounded-lg
bg-white text-gray-900
bg-white
border ${error ? 'border-red-500' : 'border-gray-300'}
${sizeClasses[size]}
px-3
@@ -103,11 +105,11 @@ const CustomDropdown = React.forwardRef<HTMLDivElement, CustomDropdownProps>(
</span>
)}
<div className="flex-1 truncate text-left">
<div className="flex-1 truncate text-left text-[14px] font-medium tracking-[0.25px] leading-[15px]">
{selectedOption ? (
<span>{selectedOption.label}</span>
<span style={{ color: '#6C766D' }}>{selectedOption.label}</span>
) : (
<span className="text-gray-500">{placeholder}</span>
<span style={{ color: '#6C766D' }}>{placeholder}</span>
)}
</div>
+3 -3
View File
@@ -34,12 +34,12 @@ const CustomStatus: React.FC<CustomStatusProps> = ({
const finalVariant = variant || getVariantFromStatus(status);
const styles = {
success: { bg: "bg-[#EBF7F2]", text: "text-[#1B9869]", dot: "bg-[#1B9869]" },
success: { bg: "bg-[#E4FAE7]", text: "text-[#258B33]", dot: "bg-[#258B33]" },
error: { bg: "bg-rose-50", text: "text-rose-600", dot: "bg-rose-600" },
warning: { bg: "bg-amber-50", text: "text-amber-600", dot: "bg-amber-600" },
info: { bg: "bg-sky-50", text: "text-sky-600", dot: "bg-sky-600" },
neutral: { bg: "bg-slate-100", text: "text-slate-600", dot: "bg-slate-600" },
brand: { bg: "bg-[#EBF7F2]", text: "text-[#1B9869]", dot: "bg-[#1B9869]" },
brand: { bg: "bg-[#E4FAE7]", text: "text-[#258B33]", dot: "bg-[#258B33]" },
};
const currentStyle = styles[finalVariant];
@@ -55,7 +55,7 @@ const CustomStatus: React.FC<CustomStatusProps> = ({
aria-disabled={!isClickable}
className={`
inline-flex items-center justify-center gap-2 px-3 py-1.5 rounded-full
text-[13px] font-semibold antialiased transition-all duration-300
text-[11px] font-semibold leading-[16.5px] tracking-[0px] antialiased transition-all duration-300
${currentStyle.bg} ${currentStyle.text}
${isClickable ? "hover:brightness-95 active:scale-95 cursor-pointer" : "cursor-default pointer-events-none"}
${className}
+8 -8
View File
@@ -1,5 +1,5 @@
import React from "react";
import { Search, ChevronLeft, ChevronRight, MoreVertical, Filter, ArrowDownUp } from "lucide-react";
import { Search, ChevronLeft, ChevronRight, Filter, ArrowDownUp } from "lucide-react";
import CustomInput from "./CustomInput";
export interface Column<T> {
@@ -96,7 +96,7 @@ export function CustomTable<T>({
<div className="w-full overflow-x-auto">
<table className="w-full text-left border-collapse min-w-[800px]">
<thead>
<tr className="bg-gray-50/80 border-b border-gray-100">
<tr className="bg-[#F3F6F5] border-b border-[#F9FAFB]">
{columns.map((col, index) => (
<th
key={index}
@@ -140,7 +140,7 @@ export function CustomTable<T>({
</div>
{/* Pagination Footer */}
<div className="flex items-center justify-between p-4 bg-gray-50/30 border-t border-gray-100">
<div className="flex items-center justify-between py-4 px-6 bg-[#F3F6F5] border-t border-[#E4E9F2]">
<div className="text-[13px] font-medium text-gray-500">
Showing {totalItems > 0 ? startIndex : 0} to {endIndex} of {totalItems} {itemName}
</div>
@@ -149,7 +149,7 @@ export function CustomTable<T>({
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
className="w-8 h-8 flex items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-500 hover:bg-gray-50 hover:text-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="w-8 h-8 flex items-center justify-center rounded-lg border border-[#9FACA1] bg-white text-[#9FACA1] hover:bg-gray-50 hover:text-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft size={16} />
</button>
@@ -160,9 +160,9 @@ export function CustomTable<T>({
key={page}
onClick={() => handlePageChange(page)}
className={`w-8 h-8 flex items-center justify-center rounded-lg text-sm font-semibold transition-colors
${currentPage === page
? "bg-[#1B9869] text-white border border-[#1B9869]"
: "bg-white text-gray-600 border border-gray-200 hover:bg-gray-50 hover:text-gray-900"
${currentPage === page
? "bg-gradient-to-b from-[#1B9869] to-[#14704E] text-white"
: "bg-white text-[#9FACA1] border border-[#9FACA1] hover:bg-gray-50 hover:text-gray-900"
}
`}
>
@@ -174,7 +174,7 @@ export function CustomTable<T>({
<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="w-8 h-8 flex items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-500 hover:bg-gray-50 hover:text-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className="w-8 h-8 flex items-center justify-center rounded-lg border border-[#9FACA1] bg-white text-[#9FACA1] hover:bg-gray-50 hover:text-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ChevronRight size={16} />
</button>
+2 -2
View File
@@ -1,8 +1,8 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Albert+Sans:wght@300;400;500;600;700&display=swap');
@import "tailwindcss";
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-sans: "Albert Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
@layer base {