feat: enhance cohort management using custom components
This commit is contained in:
@@ -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 5–14 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user