primus-saas-react 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/styles.css +568 -0
- package/package.json +15 -9
- package/DEMO.md +0 -68
- package/INTEGRATION.md +0 -702
- package/build_log.txt +0 -0
- package/postcss.config.js +0 -6
- package/src/components/ai/AICopilot.tsx +0 -88
- package/src/components/auth/PrimusLogin.tsx +0 -298
- package/src/components/auth/UserProfile.tsx +0 -26
- package/src/components/banking/accounts/AccountDashboard.tsx +0 -67
- package/src/components/banking/cards/CreditCardVisual.tsx +0 -67
- package/src/components/banking/credit/CreditScoreCard.tsx +0 -80
- package/src/components/banking/kyc/KYCVerification.tsx +0 -76
- package/src/components/banking/loans/LoanCalculator.tsx +0 -106
- package/src/components/banking/transactions/TransactionHistory.tsx +0 -74
- package/src/components/crud/PrimusDataTable.tsx +0 -220
- package/src/components/crud/PrimusModal.tsx +0 -68
- package/src/components/dashboard/PrimusDashboard.tsx +0 -145
- package/src/components/documents/DocumentViewer.tsx +0 -107
- package/src/components/featureflags/FeatureFlagToggle.tsx +0 -64
- package/src/components/insurance/agents/AgentDirectory.tsx +0 -72
- package/src/components/insurance/claims/ClaimStatusTracker.tsx +0 -78
- package/src/components/insurance/fraud/FraudDetectionDashboard.tsx +0 -68
- package/src/components/insurance/policies/PolicyCard.tsx +0 -77
- package/src/components/insurance/premium/PremiumCalculator.tsx +0 -104
- package/src/components/insurance/quotes/QuoteComparison.tsx +0 -75
- package/src/components/layout/PrimusHeader.tsx +0 -75
- package/src/components/layout/PrimusLayout.tsx +0 -47
- package/src/components/layout/PrimusSidebar.tsx +0 -102
- package/src/components/logging/LogViewer.tsx +0 -90
- package/src/components/notifications/NotificationFeed.tsx +0 -106
- package/src/components/notifications/PrimusNotificationCenter.tsx +0 -282
- package/src/components/payments/CheckoutForm.tsx +0 -167
- package/src/components/security/SecurityDashboard.tsx +0 -83
- package/src/components/shared/Button.tsx +0 -36
- package/src/components/shared/Input.tsx +0 -36
- package/src/components/storage/FileUploader.tsx +0 -79
- package/src/context/PrimusProvider.tsx +0 -156
- package/src/context/PrimusThemeProvider.tsx +0 -160
- package/src/hooks/useNotifications.ts +0 -58
- package/src/hooks/usePrimusAuth.ts +0 -3
- package/src/hooks/useRealtimeNotifications.ts +0 -114
- package/src/index.ts +0 -42
- package/tailwind.config.js +0 -18
- package/tsconfig.json +0 -28
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
interface KYCStatus {
|
|
4
|
-
userId: string;
|
|
5
|
-
status: string;
|
|
6
|
-
verificationDate: string;
|
|
7
|
-
documents: Array<{ type: string; status: string; uploadDate: string }>;
|
|
8
|
-
riskLevel: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const KYCVerification: React.FC<{ userId: string; apiUrl?: string }> = ({
|
|
12
|
-
userId,
|
|
13
|
-
apiUrl = 'http://localhost:5221'
|
|
14
|
-
}) => {
|
|
15
|
-
const [status, setStatus] = useState<KYCStatus | null>(null);
|
|
16
|
-
const [loading, setLoading] = useState(true);
|
|
17
|
-
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
fetchStatus();
|
|
20
|
-
}, [userId]);
|
|
21
|
-
|
|
22
|
-
const fetchStatus = async () => {
|
|
23
|
-
try {
|
|
24
|
-
const response = await fetch(`${apiUrl}/api/banking/kyc/status/${userId}`);
|
|
25
|
-
if (response.ok) {
|
|
26
|
-
const data = await response.json();
|
|
27
|
-
setStatus(data);
|
|
28
|
-
}
|
|
29
|
-
} catch (error) {
|
|
30
|
-
console.error('Failed to fetch KYC status:', error);
|
|
31
|
-
} finally {
|
|
32
|
-
setLoading(false);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
if (loading) return <div className="p-4">Loading KYC status...</div>;
|
|
37
|
-
if (!status) return <div className="p-4">No KYC data found</div>;
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
41
|
-
<h3 className="text-lg font-medium text-gray-900 mb-4">KYC Verification Status</h3>
|
|
42
|
-
|
|
43
|
-
<div className="mb-6">
|
|
44
|
-
<div className="flex items-center gap-2 mb-2">
|
|
45
|
-
<span className="text-sm text-gray-600">Status:</span>
|
|
46
|
-
<span className={`px-3 py-1 rounded-full text-sm font-medium ${status.status === 'verified' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
|
|
47
|
-
}`}>
|
|
48
|
-
{status.status}
|
|
49
|
-
</span>
|
|
50
|
-
</div>
|
|
51
|
-
<div className="flex items-center gap-2">
|
|
52
|
-
<span className="text-sm text-gray-600">Risk Level:</span>
|
|
53
|
-
<span className={`px-3 py-1 rounded-full text-sm font-medium ${status.riskLevel === 'low' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
|
54
|
-
}`}>
|
|
55
|
-
{status.riskLevel}
|
|
56
|
-
</span>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<div>
|
|
61
|
-
<h4 className="font-medium text-gray-900 mb-3">Documents</h4>
|
|
62
|
-
<div className="space-y-2">
|
|
63
|
-
{status.documents.map((doc, index) => (
|
|
64
|
-
<div key={index} className="flex justify-between items-center p-3 bg-gray-50 rounded">
|
|
65
|
-
<span className="text-sm text-gray-900">{doc.type}</span>
|
|
66
|
-
<span className={`px-2 py-1 rounded text-xs font-medium ${doc.status === 'approved' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
|
|
67
|
-
}`}>
|
|
68
|
-
{doc.status}
|
|
69
|
-
</span>
|
|
70
|
-
</div>
|
|
71
|
-
))}
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
);
|
|
76
|
-
};
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
|
|
3
|
-
export const LoanCalculator: React.FC<{ apiUrl?: string }> = ({
|
|
4
|
-
apiUrl = 'http://localhost:5221'
|
|
5
|
-
}) => {
|
|
6
|
-
const [amount, setAmount] = useState(25000);
|
|
7
|
-
const [rate, setRate] = useState(5.5);
|
|
8
|
-
const [term, setTerm] = useState(60);
|
|
9
|
-
const [result, setResult] = useState<any>(null);
|
|
10
|
-
const [loading, setLoading] = useState(false);
|
|
11
|
-
|
|
12
|
-
const calculate = async () => {
|
|
13
|
-
setLoading(true);
|
|
14
|
-
try {
|
|
15
|
-
const response = await fetch(`${apiUrl}/api/banking/loans/calculate`, {
|
|
16
|
-
method: 'POST',
|
|
17
|
-
headers: { 'Content-Type': 'application/json' },
|
|
18
|
-
body: JSON.stringify({ amount, interestRate: rate, term })
|
|
19
|
-
});
|
|
20
|
-
if (response.ok) {
|
|
21
|
-
const data = await response.json();
|
|
22
|
-
setResult(data);
|
|
23
|
-
}
|
|
24
|
-
} catch (error) {
|
|
25
|
-
console.error('Failed to calculate loan:', error);
|
|
26
|
-
} finally {
|
|
27
|
-
setLoading(false);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
33
|
-
<h3 className="text-lg font-medium text-gray-900 mb-4">Loan Calculator</h3>
|
|
34
|
-
|
|
35
|
-
<div className="space-y-4 mb-6">
|
|
36
|
-
<div>
|
|
37
|
-
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
38
|
-
Loan Amount: ${amount.toLocaleString()}
|
|
39
|
-
</label>
|
|
40
|
-
<input
|
|
41
|
-
type="range"
|
|
42
|
-
min="1000"
|
|
43
|
-
max="100000"
|
|
44
|
-
step="1000"
|
|
45
|
-
value={amount}
|
|
46
|
-
onChange={(e) => setAmount(Number(e.target.value))}
|
|
47
|
-
className="w-full"
|
|
48
|
-
/>
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
<div>
|
|
52
|
-
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
53
|
-
Interest Rate: {rate}%
|
|
54
|
-
</label>
|
|
55
|
-
<input
|
|
56
|
-
type="range"
|
|
57
|
-
min="1"
|
|
58
|
-
max="15"
|
|
59
|
-
step="0.1"
|
|
60
|
-
value={rate}
|
|
61
|
-
onChange={(e) => setRate(Number(e.target.value))}
|
|
62
|
-
className="w-full"
|
|
63
|
-
/>
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
<div>
|
|
67
|
-
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
68
|
-
Term: {term} months
|
|
69
|
-
</label>
|
|
70
|
-
<input
|
|
71
|
-
type="range"
|
|
72
|
-
min="12"
|
|
73
|
-
max="360"
|
|
74
|
-
step="12"
|
|
75
|
-
value={term}
|
|
76
|
-
onChange={(e) => setTerm(Number(e.target.value))}
|
|
77
|
-
className="w-full"
|
|
78
|
-
/>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
<button
|
|
83
|
-
onClick={calculate}
|
|
84
|
-
disabled={loading}
|
|
85
|
-
className="w-full bg-primus-600 text-white py-2 px-4 rounded hover:bg-primus-700 disabled:opacity-50"
|
|
86
|
-
>
|
|
87
|
-
{loading ? 'Calculating...' : 'Calculate'}
|
|
88
|
-
</button>
|
|
89
|
-
|
|
90
|
-
{result && (
|
|
91
|
-
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
|
|
92
|
-
<div className="grid grid-cols-2 gap-4">
|
|
93
|
-
<div>
|
|
94
|
-
<p className="text-sm text-gray-600">Monthly Payment</p>
|
|
95
|
-
<p className="text-2xl font-bold text-gray-900">${result.monthlyPayment}</p>
|
|
96
|
-
</div>
|
|
97
|
-
<div>
|
|
98
|
-
<p className="text-sm text-gray-600">Total Interest</p>
|
|
99
|
-
<p className="text-2xl font-bold text-gray-900">${result.totalInterest}</p>
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
|
-
)}
|
|
104
|
-
</div>
|
|
105
|
-
);
|
|
106
|
-
};
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
ArrowDownLeftIcon,
|
|
4
|
-
ArrowUpRightIcon,
|
|
5
|
-
ShoppingBagIcon
|
|
6
|
-
// BanknotesIcon
|
|
7
|
-
} from '@heroicons/react/24/outline';
|
|
8
|
-
import { clsx } from "clsx";
|
|
9
|
-
|
|
10
|
-
export interface Transaction {
|
|
11
|
-
id: string;
|
|
12
|
-
description: string;
|
|
13
|
-
amount: number;
|
|
14
|
-
date: string;
|
|
15
|
-
type: 'credit' | 'debit';
|
|
16
|
-
category?: 'shopping' | 'transfer' | 'food' | 'other';
|
|
17
|
-
status?: 'pending' | 'completed' | 'failed';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface TransactionHistoryProps {
|
|
21
|
-
transactions: Transaction[];
|
|
22
|
-
onTransactionClick?: (id: string) => void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const TransactionHistory: React.FC<TransactionHistoryProps> = ({
|
|
26
|
-
transactions,
|
|
27
|
-
onTransactionClick
|
|
28
|
-
}) => {
|
|
29
|
-
const getIcon = (t: Transaction) => {
|
|
30
|
-
if (t.type === 'credit') return <ArrowDownLeftIcon className="h-5 w-5 text-green-600" />;
|
|
31
|
-
if (t.category === 'shopping') return <ShoppingBagIcon className="h-5 w-5 text-blue-600" />;
|
|
32
|
-
return <ArrowUpRightIcon className="h-5 w-5 text-red-600" />; // simplistic default
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<div className="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
|
|
37
|
-
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center">
|
|
38
|
-
<h3 className="font-semibold text-gray-900">Recent Transactions</h3>
|
|
39
|
-
<button className="text-sm text-primus-600 hover:text-primus-700 font-medium">View All</button>
|
|
40
|
-
</div>
|
|
41
|
-
<div className="divide-y divide-gray-100">
|
|
42
|
-
{transactions.map((t) => (
|
|
43
|
-
<div
|
|
44
|
-
key={t.id}
|
|
45
|
-
onClick={() => onTransactionClick?.(t.id)}
|
|
46
|
-
className="px-6 py-4 flex items-center justify-between hover:bg-gray-50 cursor-pointer transition-colors"
|
|
47
|
-
>
|
|
48
|
-
<div className="flex items-center gap-4">
|
|
49
|
-
<div className={clsx("h-10 w-10 rounded-full flex items-center justify-center",
|
|
50
|
-
t.type === 'credit' ? "bg-green-100" : "bg-gray-100"
|
|
51
|
-
)}>
|
|
52
|
-
{getIcon(t)}
|
|
53
|
-
</div>
|
|
54
|
-
<div>
|
|
55
|
-
<p className="text-sm font-medium text-gray-900">{t.description}</p>
|
|
56
|
-
<p className="text-xs text-gray-500">{t.date}</p>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
<div className="text-right">
|
|
60
|
-
<p className={clsx(
|
|
61
|
-
"text-sm font-semibold",
|
|
62
|
-
t.type === 'credit' ? "text-green-600" : "text-gray-900"
|
|
63
|
-
)}>
|
|
64
|
-
{t.type === 'credit' ? '+' : '-'}
|
|
65
|
-
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(t.amount)}
|
|
66
|
-
</p>
|
|
67
|
-
<p className="text-xs text-gray-400 capitalize">{t.status || 'completed'}</p>
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
))}
|
|
71
|
-
</div>
|
|
72
|
-
</div >
|
|
73
|
-
);
|
|
74
|
-
};
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import React, { useState, useMemo } from 'react';
|
|
2
|
-
|
|
3
|
-
export interface Column<T> {
|
|
4
|
-
key: keyof T | string;
|
|
5
|
-
header: string;
|
|
6
|
-
render?: (value: any, row: T) => React.ReactNode;
|
|
7
|
-
sortable?: boolean;
|
|
8
|
-
width?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface PrimusDataTableProps<T> {
|
|
12
|
-
data: T[];
|
|
13
|
-
columns: Column<T>[];
|
|
14
|
-
/** Row key field */
|
|
15
|
-
rowKey: keyof T;
|
|
16
|
-
/** Loading state */
|
|
17
|
-
loading?: boolean;
|
|
18
|
-
/** Enable row selection */
|
|
19
|
-
selectable?: boolean;
|
|
20
|
-
/** Selected row keys */
|
|
21
|
-
selectedKeys?: (string | number)[];
|
|
22
|
-
/** On selection change */
|
|
23
|
-
onSelectionChange?: (keys: (string | number)[]) => void;
|
|
24
|
-
/** On row click */
|
|
25
|
-
onRowClick?: (row: T) => void;
|
|
26
|
-
/** Actions column renderer */
|
|
27
|
-
actions?: (row: T) => React.ReactNode;
|
|
28
|
-
/** Empty state message */
|
|
29
|
-
emptyMessage?: string;
|
|
30
|
-
/** Search placeholder */
|
|
31
|
-
searchPlaceholder?: string;
|
|
32
|
-
/** Enable search */
|
|
33
|
-
searchable?: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function PrimusDataTable<T extends Record<string, any>>({
|
|
37
|
-
data,
|
|
38
|
-
columns,
|
|
39
|
-
rowKey,
|
|
40
|
-
loading = false,
|
|
41
|
-
selectable = false,
|
|
42
|
-
selectedKeys = [],
|
|
43
|
-
onSelectionChange,
|
|
44
|
-
onRowClick,
|
|
45
|
-
actions,
|
|
46
|
-
emptyMessage = 'No data available',
|
|
47
|
-
searchPlaceholder = 'Filter...',
|
|
48
|
-
searchable = true,
|
|
49
|
-
}: PrimusDataTableProps<T>) {
|
|
50
|
-
const [search, setSearch] = useState('');
|
|
51
|
-
const [sortKey, setSortKey] = useState<string | null>(null);
|
|
52
|
-
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
|
|
53
|
-
|
|
54
|
-
const filteredData = useMemo(() => {
|
|
55
|
-
let result = [...data];
|
|
56
|
-
|
|
57
|
-
// Search filter
|
|
58
|
-
if (search) {
|
|
59
|
-
const lowerSearch = search.toLowerCase();
|
|
60
|
-
result = result.filter(row =>
|
|
61
|
-
Object.values(row).some(val =>
|
|
62
|
-
String(val).toLowerCase().includes(lowerSearch)
|
|
63
|
-
)
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Sort
|
|
68
|
-
if (sortKey) {
|
|
69
|
-
result.sort((a, b) => {
|
|
70
|
-
const aVal = a[sortKey];
|
|
71
|
-
const bVal = b[sortKey];
|
|
72
|
-
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
73
|
-
return sortDir === 'asc' ? cmp : -cmp;
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return result;
|
|
78
|
-
}, [data, search, sortKey, sortDir]);
|
|
79
|
-
|
|
80
|
-
const handleSort = (key: string) => {
|
|
81
|
-
if (sortKey === key) {
|
|
82
|
-
setSortDir(sortDir === 'asc' ? 'desc' : 'asc');
|
|
83
|
-
} else {
|
|
84
|
-
setSortKey(key);
|
|
85
|
-
setSortDir('asc');
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const toggleSelect = (key: string | number) => {
|
|
90
|
-
const newKeys = selectedKeys.includes(key)
|
|
91
|
-
? selectedKeys.filter(k => k !== key)
|
|
92
|
-
: [...selectedKeys, key];
|
|
93
|
-
onSelectionChange?.(newKeys);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const toggleSelectAll = () => {
|
|
97
|
-
if (selectedKeys.length === filteredData.length) {
|
|
98
|
-
onSelectionChange?.([]);
|
|
99
|
-
} else {
|
|
100
|
-
onSelectionChange?.(filteredData.map(row => row[rowKey]));
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<div className="flex flex-col space-y-4">
|
|
106
|
-
{/* Toolbar */}
|
|
107
|
-
{searchable && (
|
|
108
|
-
<div className="flex items-center justify-between px-1">
|
|
109
|
-
<div className="relative group">
|
|
110
|
-
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
111
|
-
<svg className="h-4 w-4 text-gray-500 group-focus-within:text-purple-400 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
112
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
113
|
-
</svg>
|
|
114
|
-
</div>
|
|
115
|
-
<input
|
|
116
|
-
type="text"
|
|
117
|
-
placeholder={searchPlaceholder}
|
|
118
|
-
value={search}
|
|
119
|
-
onChange={e => setSearch(e.target.value)}
|
|
120
|
-
className="w-64 pl-10 pr-4 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-purple-500/50 focus:bg-white/10 transition-all hover:bg-white/10"
|
|
121
|
-
/>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
)}
|
|
125
|
-
|
|
126
|
-
{/* Table Container */}
|
|
127
|
-
<div className={`overflow-hidden rounded-xl border border-white/5 bg-slate-900/40 backdrop-blur-md shadow-xl ring-1 ring-white/5 ${loading ? 'opacity-80' : ''}`}>
|
|
128
|
-
<div className="overflow-x-auto">
|
|
129
|
-
<table className="w-full text-left border-collapse">
|
|
130
|
-
<thead className="bg-white/5 border-b border-white/5 backdrop-blur-md sticky top-0 z-20">
|
|
131
|
-
<tr>
|
|
132
|
-
{selectable && (
|
|
133
|
-
<th className="w-12 px-6 py-4">
|
|
134
|
-
<input
|
|
135
|
-
type="checkbox"
|
|
136
|
-
checked={selectedKeys.length === filteredData.length && filteredData.length > 0}
|
|
137
|
-
onChange={toggleSelectAll}
|
|
138
|
-
className="rounded border-gray-600 bg-gray-800 text-purple-600 focus:ring-offset-gray-900 focus:ring-purple-500 transition-colors cursor-pointer"
|
|
139
|
-
/>
|
|
140
|
-
</th>
|
|
141
|
-
)}
|
|
142
|
-
{columns.map(col => (
|
|
143
|
-
<th
|
|
144
|
-
key={String(col.key)}
|
|
145
|
-
className={`px-6 py-4 text-xs font-semibold text-gray-400 uppercase tracking-wider ${col.sortable ? 'cursor-pointer hover:text-white group' : ''}`}
|
|
146
|
-
style={{ width: col.width }}
|
|
147
|
-
onClick={() => col.sortable && handleSort(String(col.key))}
|
|
148
|
-
>
|
|
149
|
-
<div className="flex items-center gap-2">
|
|
150
|
-
{col.header}
|
|
151
|
-
{col.sortable && (
|
|
152
|
-
<span className={`transition-opacity duration-200 ${sortKey === col.key ? 'opacity-100 text-purple-400' : 'opacity-0 group-hover:opacity-50'}`}>
|
|
153
|
-
{sortDir === 'asc' ? '↑' : '↓'}
|
|
154
|
-
</span>
|
|
155
|
-
)}
|
|
156
|
-
</div>
|
|
157
|
-
</th>
|
|
158
|
-
))}
|
|
159
|
-
{actions && <th className="w-24 px-6 py-4 text-right text-xs font-semibold text-gray-400 uppercase tracking-wider"></th>}
|
|
160
|
-
</tr>
|
|
161
|
-
</thead>
|
|
162
|
-
<tbody className="divide-y divide-white/5">
|
|
163
|
-
{loading ? (
|
|
164
|
-
<tr>
|
|
165
|
-
<td colSpan={columns.length + (selectable ? 1 : 0) + (actions ? 1 : 0)} className="px-6 py-24 text-center text-gray-500">
|
|
166
|
-
<div className="flex flex-col items-center justify-center gap-3">
|
|
167
|
-
<div className="w-6 h-6 border-2 border-purple-500/50 border-t-purple-500 rounded-full animate-spin" />
|
|
168
|
-
<span className="text-sm font-medium">Loading data...</span>
|
|
169
|
-
</div>
|
|
170
|
-
</td>
|
|
171
|
-
</tr>
|
|
172
|
-
) : filteredData.length === 0 ? (
|
|
173
|
-
<tr>
|
|
174
|
-
<td colSpan={columns.length + (selectable ? 1 : 0) + (actions ? 1 : 0)} className="px-6 py-24 text-center text-gray-500">
|
|
175
|
-
<p className="text-sm">{emptyMessage}</p>
|
|
176
|
-
</td>
|
|
177
|
-
</tr>
|
|
178
|
-
) : (
|
|
179
|
-
filteredData.map(row => (
|
|
180
|
-
<tr
|
|
181
|
-
key={String(row[rowKey])}
|
|
182
|
-
onClick={() => onRowClick?.(row)}
|
|
183
|
-
className={`
|
|
184
|
-
group transition-colors duration-150
|
|
185
|
-
${onRowClick ? 'cursor-pointer hover:bg-white/5' : ''}
|
|
186
|
-
${selectedKeys.includes(row[rowKey]) ? 'bg-purple-500/10 hover:bg-purple-500/20' : ''}
|
|
187
|
-
`}
|
|
188
|
-
>
|
|
189
|
-
{selectable && (
|
|
190
|
-
<td className="px-6 py-4" onClick={e => e.stopPropagation()}>
|
|
191
|
-
<input
|
|
192
|
-
type="checkbox"
|
|
193
|
-
checked={selectedKeys.includes(row[rowKey])}
|
|
194
|
-
onChange={() => toggleSelect(row[rowKey])}
|
|
195
|
-
className="rounded border-gray-600 bg-gray-800 text-purple-600 focus:ring-offset-gray-900 focus:ring-purple-500 cursor-pointer"
|
|
196
|
-
/>
|
|
197
|
-
</td>
|
|
198
|
-
)}
|
|
199
|
-
{columns.map(col => (
|
|
200
|
-
<td key={String(col.key)} className="px-6 py-4 text-sm text-gray-300 group-hover:text-white transition-colors">
|
|
201
|
-
{col.render ? col.render(row[col.key as keyof T], row) : String(row[col.key as keyof T] ?? '')}
|
|
202
|
-
</td>
|
|
203
|
-
))}
|
|
204
|
-
{actions && (
|
|
205
|
-
<td className="px-6 py-4 text-right" onClick={e => e.stopPropagation()}>
|
|
206
|
-
<div className="opacity-0 group-hover:opacity-100 transition-opacity transform translate-x-2 group-hover:translate-x-0">
|
|
207
|
-
{actions(row)}
|
|
208
|
-
</div>
|
|
209
|
-
</td>
|
|
210
|
-
)}
|
|
211
|
-
</tr>
|
|
212
|
-
))
|
|
213
|
-
)}
|
|
214
|
-
</tbody>
|
|
215
|
-
</table>
|
|
216
|
-
</div>
|
|
217
|
-
</div>
|
|
218
|
-
</div>
|
|
219
|
-
);
|
|
220
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
export interface PrimusModalProps {
|
|
4
|
-
open: boolean;
|
|
5
|
-
onClose: () => void;
|
|
6
|
-
title?: string;
|
|
7
|
-
children: React.ReactNode;
|
|
8
|
-
footer?: React.ReactNode;
|
|
9
|
-
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const PrimusModal: React.FC<PrimusModalProps> = ({
|
|
13
|
-
open,
|
|
14
|
-
onClose,
|
|
15
|
-
title,
|
|
16
|
-
children,
|
|
17
|
-
footer,
|
|
18
|
-
size = 'md',
|
|
19
|
-
}) => {
|
|
20
|
-
if (!open) return null;
|
|
21
|
-
|
|
22
|
-
const sizeClasses = {
|
|
23
|
-
sm: 'max-w-sm',
|
|
24
|
-
md: 'max-w-md',
|
|
25
|
-
lg: 'max-w-lg',
|
|
26
|
-
xl: 'max-w-xl',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
31
|
-
{/* Backdrop */}
|
|
32
|
-
<div
|
|
33
|
-
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
34
|
-
onClick={onClose}
|
|
35
|
-
/>
|
|
36
|
-
|
|
37
|
-
{/* Modal */}
|
|
38
|
-
<div className={`relative w-full ${sizeClasses[size]} mx-4 bg-gray-800 rounded-xl border border-gray-700 shadow-2xl`}>
|
|
39
|
-
{/* Header */}
|
|
40
|
-
{title && (
|
|
41
|
-
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-700">
|
|
42
|
-
<h2 className="text-lg font-semibold text-white">{title}</h2>
|
|
43
|
-
<button
|
|
44
|
-
onClick={onClose}
|
|
45
|
-
className="text-gray-400 hover:text-white transition-colors"
|
|
46
|
-
>
|
|
47
|
-
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
48
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
49
|
-
</svg>
|
|
50
|
-
</button>
|
|
51
|
-
</div>
|
|
52
|
-
)}
|
|
53
|
-
|
|
54
|
-
{/* Content */}
|
|
55
|
-
<div className="p-6">
|
|
56
|
-
{children}
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
{/* Footer */}
|
|
60
|
-
{footer && (
|
|
61
|
-
<div className="flex justify-end gap-3 px-6 py-4 border-t border-gray-700 bg-gray-800/50">
|
|
62
|
-
{footer}
|
|
63
|
-
</div>
|
|
64
|
-
)}
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
);
|
|
68
|
-
};
|