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,282 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import { Bell, Send, X, Check, AlertTriangle, Info, AlertCircle } from 'lucide-react';
|
|
3
|
-
import { usePrimusTheme, PrimusTheme, themeColors } from '../../context/PrimusThemeProvider';
|
|
4
|
-
|
|
5
|
-
interface Notification {
|
|
6
|
-
id: string;
|
|
7
|
-
title: string;
|
|
8
|
-
message: string;
|
|
9
|
-
type: 'info' | 'success' | 'warning' | 'error';
|
|
10
|
-
timestamp: string;
|
|
11
|
-
read: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const API_URL = 'http://localhost:5222';
|
|
15
|
-
|
|
16
|
-
interface PrimusNotificationCenterProps {
|
|
17
|
-
/** Override the theme from context (optional) */
|
|
18
|
-
theme?: PrimusTheme;
|
|
19
|
-
/** Custom API URL (optional) */
|
|
20
|
-
apiUrl?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* PrimusNotificationCenter - Enterprise Notification Component
|
|
25
|
-
*
|
|
26
|
-
* Features:
|
|
27
|
-
* - Automatic theme inheritance from PrimusThemeProvider
|
|
28
|
-
* - Bell icon with animated badge
|
|
29
|
-
* - Admin panel for sending notifications
|
|
30
|
-
* - Real-time polling (3 second intervals)
|
|
31
|
-
* - Type-specific icons and colors
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```tsx
|
|
35
|
-
* // With theme provider (recommended)
|
|
36
|
-
* <PrimusThemeProvider defaultTheme="dark">
|
|
37
|
-
* <PrimusNotificationCenter />
|
|
38
|
-
* </PrimusThemeProvider>
|
|
39
|
-
*
|
|
40
|
-
* // With manual override
|
|
41
|
-
* <PrimusNotificationCenter theme="light" />
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
export function PrimusNotificationCenter({ theme: themeOverride, apiUrl = API_URL }: PrimusNotificationCenterProps) {
|
|
45
|
-
const themeContext = usePrimusTheme();
|
|
46
|
-
const theme = themeOverride || themeContext.theme;
|
|
47
|
-
const t = themeColors[theme];
|
|
48
|
-
|
|
49
|
-
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
50
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
51
|
-
const [showSendForm, setShowSendForm] = useState(false);
|
|
52
|
-
const [unreadCount, setUnreadCount] = useState(0);
|
|
53
|
-
const [newTitle, setNewTitle] = useState('');
|
|
54
|
-
const [newMessage, setNewMessage] = useState('');
|
|
55
|
-
const [newType, setNewType] = useState<'info' | 'success' | 'warning' | 'error'>('info');
|
|
56
|
-
const [sending, setSending] = useState(false);
|
|
57
|
-
|
|
58
|
-
const fetchNotifications = useCallback(async () => {
|
|
59
|
-
try {
|
|
60
|
-
const response = await fetch(`${apiUrl}/api/notifications`);
|
|
61
|
-
if (response.ok) {
|
|
62
|
-
const data = await response.json();
|
|
63
|
-
setNotifications(data);
|
|
64
|
-
setUnreadCount(data.filter((n: Notification) => !n.read).length);
|
|
65
|
-
}
|
|
66
|
-
} catch (error) {
|
|
67
|
-
// Silent fail - backend may not be connected
|
|
68
|
-
}
|
|
69
|
-
}, [apiUrl]);
|
|
70
|
-
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
fetchNotifications();
|
|
73
|
-
const interval = setInterval(fetchNotifications, 3000);
|
|
74
|
-
return () => clearInterval(interval);
|
|
75
|
-
}, [fetchNotifications]);
|
|
76
|
-
|
|
77
|
-
const sendNotification = async () => {
|
|
78
|
-
if (!newTitle.trim() || !newMessage.trim()) return;
|
|
79
|
-
setSending(true);
|
|
80
|
-
try {
|
|
81
|
-
const response = await fetch(`${apiUrl}/api/notifications`, {
|
|
82
|
-
method: 'POST',
|
|
83
|
-
headers: { 'Content-Type': 'application/json' },
|
|
84
|
-
body: JSON.stringify({ title: newTitle, message: newMessage, type: newType })
|
|
85
|
-
});
|
|
86
|
-
if (response.ok) {
|
|
87
|
-
setNewTitle('');
|
|
88
|
-
setNewMessage('');
|
|
89
|
-
setNewType('info');
|
|
90
|
-
setShowSendForm(false);
|
|
91
|
-
await fetchNotifications();
|
|
92
|
-
}
|
|
93
|
-
} catch (error) {
|
|
94
|
-
console.error('Failed to send notification');
|
|
95
|
-
}
|
|
96
|
-
setSending(false);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const markAsRead = async (id: string) => {
|
|
100
|
-
try {
|
|
101
|
-
await fetch(`${apiUrl}/api/notifications/${id}/read`, { method: 'PUT' });
|
|
102
|
-
setNotifications(prev => prev.map(n => n.id === id ? { ...n, read: true } : n));
|
|
103
|
-
setUnreadCount(prev => Math.max(0, prev - 1));
|
|
104
|
-
} catch {
|
|
105
|
-
// Silent fail
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const getTypeIcon = (type: string) => {
|
|
110
|
-
const iconClass = "h-4 w-4";
|
|
111
|
-
switch (type) {
|
|
112
|
-
case 'success': return <Check className={`${iconClass} ${t.success}`} />;
|
|
113
|
-
case 'warning': return <AlertTriangle className={`${iconClass} ${t.warning}`} />;
|
|
114
|
-
case 'error': return <AlertCircle className={`${iconClass} ${t.error}`} />;
|
|
115
|
-
default: return <Info className={`${iconClass} ${t.info}`} />;
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const getTypeBg = (type: string) => {
|
|
120
|
-
if (theme === 'light') {
|
|
121
|
-
switch (type) {
|
|
122
|
-
case 'success': return 'bg-emerald-50 border-emerald-200';
|
|
123
|
-
case 'warning': return 'bg-amber-50 border-amber-200';
|
|
124
|
-
case 'error': return 'bg-rose-50 border-rose-200';
|
|
125
|
-
default: return 'bg-blue-50 border-blue-200';
|
|
126
|
-
}
|
|
127
|
-
} else {
|
|
128
|
-
switch (type) {
|
|
129
|
-
case 'success': return 'bg-emerald-900/20 border-emerald-800';
|
|
130
|
-
case 'warning': return 'bg-amber-900/20 border-amber-800';
|
|
131
|
-
case 'error': return 'bg-rose-900/20 border-rose-800';
|
|
132
|
-
default: return 'bg-blue-900/20 border-blue-800';
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
return (
|
|
138
|
-
<div className="relative font-sans">
|
|
139
|
-
{/* Bell Button */}
|
|
140
|
-
<button
|
|
141
|
-
onClick={() => setIsOpen(!isOpen)}
|
|
142
|
-
className={`relative p-2.5 rounded-xl ${t.bgHover} transition-all duration-200 group`}
|
|
143
|
-
>
|
|
144
|
-
<Bell className={`h-5 w-5 ${t.textSecondary} group-hover:${t.accentText} transition-colors`} />
|
|
145
|
-
{unreadCount > 0 && (
|
|
146
|
-
<span className={`absolute -top-0.5 -right-0.5 h-5 w-5 ${t.badge} rounded-full flex items-center justify-center text-white text-xs font-bold shadow-lg animate-pulse`}>
|
|
147
|
-
{unreadCount > 9 ? '9+' : unreadCount}
|
|
148
|
-
</span>
|
|
149
|
-
)}
|
|
150
|
-
</button>
|
|
151
|
-
|
|
152
|
-
{/* Dropdown Panel */}
|
|
153
|
-
{isOpen && (
|
|
154
|
-
<>
|
|
155
|
-
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
|
|
156
|
-
|
|
157
|
-
<div className={`absolute right-0 mt-3 w-96 ${t.bg} border ${t.border} rounded-2xl shadow-2xl z-50 overflow-hidden`}>
|
|
158
|
-
{/* Header */}
|
|
159
|
-
<div className={`px-5 py-4 border-b ${t.border} flex items-center justify-between`}>
|
|
160
|
-
<div className="flex items-center gap-3">
|
|
161
|
-
<div className={`p-2 rounded-xl ${t.bgTertiary}`}>
|
|
162
|
-
<Bell className={`h-5 w-5 ${t.accentText}`} />
|
|
163
|
-
</div>
|
|
164
|
-
<div>
|
|
165
|
-
<h3 className={`font-semibold ${t.text}`}>Notifications</h3>
|
|
166
|
-
<p className={`text-xs ${t.textMuted}`}>
|
|
167
|
-
{unreadCount > 0 ? `${unreadCount} unread` : 'All caught up!'}
|
|
168
|
-
</p>
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
<button onClick={() => setIsOpen(false)} className={`p-1.5 rounded-lg ${t.bgHover} transition-colors`}>
|
|
172
|
-
<X className={`h-4 w-4 ${t.textSecondary}`} />
|
|
173
|
-
</button>
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
{/* Admin Panel */}
|
|
177
|
-
<div className={`p-4 border-b ${t.border} ${t.bgSecondary}`}>
|
|
178
|
-
<button
|
|
179
|
-
onClick={() => setShowSendForm(!showSendForm)}
|
|
180
|
-
className={`w-full px-4 py-2.5 ${t.accent} ${t.accentHover} rounded-xl text-white text-sm font-medium flex items-center justify-center gap-2 transition-all duration-200 shadow-lg`}
|
|
181
|
-
>
|
|
182
|
-
<Send className="h-4 w-4" />
|
|
183
|
-
Send New Notification
|
|
184
|
-
</button>
|
|
185
|
-
|
|
186
|
-
{showSendForm && (
|
|
187
|
-
<div className="mt-4 space-y-3">
|
|
188
|
-
<input
|
|
189
|
-
type="text"
|
|
190
|
-
placeholder="Notification title..."
|
|
191
|
-
value={newTitle}
|
|
192
|
-
onChange={(e) => setNewTitle(e.target.value)}
|
|
193
|
-
className={`w-full px-4 py-2.5 ${t.bg} border ${t.border} rounded-xl ${t.text} text-sm focus:outline-none focus:ring-2 focus:ring-blue-500`}
|
|
194
|
-
/>
|
|
195
|
-
<input
|
|
196
|
-
type="text"
|
|
197
|
-
placeholder="Message content..."
|
|
198
|
-
value={newMessage}
|
|
199
|
-
onChange={(e) => setNewMessage(e.target.value)}
|
|
200
|
-
className={`w-full px-4 py-2.5 ${t.bg} border ${t.border} rounded-xl ${t.text} text-sm focus:outline-none focus:ring-2 focus:ring-blue-500`}
|
|
201
|
-
/>
|
|
202
|
-
<div className="flex gap-2">
|
|
203
|
-
{(['info', 'success', 'warning', 'error'] as const).map((type) => (
|
|
204
|
-
<button
|
|
205
|
-
key={type}
|
|
206
|
-
onClick={() => setNewType(type)}
|
|
207
|
-
className={`flex-1 py-2 px-3 rounded-lg text-xs font-medium capitalize transition-all border ${newType === type
|
|
208
|
-
? `${getTypeBg(type)} ${t.text}`
|
|
209
|
-
: `${t.bg} ${t.textSecondary} ${t.border} ${t.bgHover}`
|
|
210
|
-
}`}
|
|
211
|
-
>
|
|
212
|
-
{type}
|
|
213
|
-
</button>
|
|
214
|
-
))}
|
|
215
|
-
</div>
|
|
216
|
-
<button
|
|
217
|
-
onClick={sendNotification}
|
|
218
|
-
disabled={sending || !newTitle.trim() || !newMessage.trim()}
|
|
219
|
-
className={`w-full py-2.5 ${t.accent} ${t.accentHover} disabled:opacity-50 rounded-xl text-white text-sm font-medium`}
|
|
220
|
-
>
|
|
221
|
-
{sending ? 'Sending...' : 'Send Notification'}
|
|
222
|
-
</button>
|
|
223
|
-
</div>
|
|
224
|
-
)}
|
|
225
|
-
</div>
|
|
226
|
-
|
|
227
|
-
{/* Notification List */}
|
|
228
|
-
<div className="max-h-80 overflow-y-auto">
|
|
229
|
-
{notifications.length === 0 ? (
|
|
230
|
-
<div className="px-5 py-12 text-center">
|
|
231
|
-
<div className={`mx-auto w-12 h-12 rounded-full ${t.bgSecondary} flex items-center justify-center mb-3`}>
|
|
232
|
-
<Bell className={`h-6 w-6 ${t.textMuted}`} />
|
|
233
|
-
</div>
|
|
234
|
-
<p className={`${t.textSecondary} text-sm`}>No notifications yet</p>
|
|
235
|
-
</div>
|
|
236
|
-
) : (
|
|
237
|
-
<div className={`divide-y ${t.borderSecondary}`}>
|
|
238
|
-
{notifications.map((notification) => (
|
|
239
|
-
<div
|
|
240
|
-
key={notification.id}
|
|
241
|
-
onClick={() => markAsRead(notification.id)}
|
|
242
|
-
className={`px-5 py-4 ${t.bgHover} transition-colors cursor-pointer ${!notification.read ? t.bgTertiary : ''
|
|
243
|
-
}`}
|
|
244
|
-
>
|
|
245
|
-
<div className="flex gap-3">
|
|
246
|
-
<div className={`mt-0.5 p-2 rounded-lg border ${getTypeBg(notification.type)}`}>
|
|
247
|
-
{getTypeIcon(notification.type)}
|
|
248
|
-
</div>
|
|
249
|
-
<div className="flex-1 min-w-0">
|
|
250
|
-
<div className="flex items-center gap-2">
|
|
251
|
-
<p className={`text-sm font-medium ${t.text}`}>{notification.title}</p>
|
|
252
|
-
{!notification.read && <span className={`h-2 w-2 rounded-full ${t.badge}`} />}
|
|
253
|
-
</div>
|
|
254
|
-
<p className={`text-sm ${t.textSecondary} mt-0.5 line-clamp-2`}>{notification.message}</p>
|
|
255
|
-
<p className={`text-xs ${t.textMuted} mt-1.5`}>
|
|
256
|
-
{new Date(notification.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
257
|
-
</p>
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
))}
|
|
262
|
-
</div>
|
|
263
|
-
)}
|
|
264
|
-
</div>
|
|
265
|
-
|
|
266
|
-
{/* Footer */}
|
|
267
|
-
{notifications.length > 0 && (
|
|
268
|
-
<div className={`px-5 py-3 border-t ${t.border} ${t.bgSecondary} flex items-center justify-between`}>
|
|
269
|
-
<button onClick={fetchNotifications} className={`text-sm ${t.accentText} font-medium`}>Refresh</button>
|
|
270
|
-
<button className={`text-sm ${t.textSecondary}`}>View all →</button>
|
|
271
|
-
</div>
|
|
272
|
-
)}
|
|
273
|
-
</div>
|
|
274
|
-
</>
|
|
275
|
-
)}
|
|
276
|
-
</div>
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Aliases
|
|
281
|
-
export const NotificationBell = PrimusNotificationCenter;
|
|
282
|
-
export const PrimusNotifications = PrimusNotificationCenter;
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { Button } from '../shared/Button';
|
|
3
|
-
import { Input } from '../shared/Input';
|
|
4
|
-
import { LockClosedIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/solid';
|
|
5
|
-
|
|
6
|
-
export interface CheckoutFormProps {
|
|
7
|
-
amount: number;
|
|
8
|
-
currency?: string;
|
|
9
|
-
apiUrl?: string;
|
|
10
|
-
onSuccess?: (result: any) => void;
|
|
11
|
-
onError?: (error: string) => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const CheckoutForm: React.FC<CheckoutFormProps> = ({
|
|
15
|
-
amount,
|
|
16
|
-
currency = 'USD',
|
|
17
|
-
apiUrl = 'http://localhost:5221',
|
|
18
|
-
onSuccess,
|
|
19
|
-
onError
|
|
20
|
-
}) => {
|
|
21
|
-
const [loading, setLoading] = useState(false);
|
|
22
|
-
const [cardholderName, setCardholderName] = useState('');
|
|
23
|
-
const [cardNumber, setCardNumber] = useState('');
|
|
24
|
-
const [expiry, setExpiry] = useState('');
|
|
25
|
-
const [cvc, setCvc] = useState('');
|
|
26
|
-
const [error, setError] = useState<string | null>(null);
|
|
27
|
-
const [success, setSuccess] = useState(false);
|
|
28
|
-
|
|
29
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
30
|
-
e.preventDefault();
|
|
31
|
-
setError(null);
|
|
32
|
-
setSuccess(false);
|
|
33
|
-
setLoading(true);
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const response = await fetch(`${apiUrl}/api/payments/charge`, {
|
|
37
|
-
method: 'POST',
|
|
38
|
-
headers: { 'Content-Type': 'application/json' },
|
|
39
|
-
body: JSON.stringify({
|
|
40
|
-
amount,
|
|
41
|
-
currency,
|
|
42
|
-
cardholderName,
|
|
43
|
-
cardNumber: cardNumber.replace(/\s/g, ''),
|
|
44
|
-
expiry,
|
|
45
|
-
cvc
|
|
46
|
-
})
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const data = await response.json();
|
|
50
|
-
|
|
51
|
-
if (!response.ok) {
|
|
52
|
-
throw new Error(data.error || 'Payment failed');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
setSuccess(true);
|
|
56
|
-
onSuccess?.(data);
|
|
57
|
-
|
|
58
|
-
// Reset form after success
|
|
59
|
-
setTimeout(() => {
|
|
60
|
-
setCardholderName('');
|
|
61
|
-
setCardNumber('');
|
|
62
|
-
setExpiry('');
|
|
63
|
-
setCvc('');
|
|
64
|
-
setSuccess(false);
|
|
65
|
-
}, 3000);
|
|
66
|
-
|
|
67
|
-
} catch (err: any) {
|
|
68
|
-
const errorMessage = err.message || 'Payment processing failed';
|
|
69
|
-
setError(errorMessage);
|
|
70
|
-
onError?.(errorMessage);
|
|
71
|
-
} finally {
|
|
72
|
-
setLoading(false);
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
78
|
-
<div className="flex justify-between items-center mb-6">
|
|
79
|
-
<h3 className="text-lg font-medium text-gray-900">Payment Details</h3>
|
|
80
|
-
<div className="flex items-center text-gray-500 text-sm">
|
|
81
|
-
<LockClosedIcon className="h-4 w-4 mr-1" />
|
|
82
|
-
Secure Payment
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
<div className="mb-6 p-4 bg-gray-50 rounded-lg flex justify-between items-center">
|
|
87
|
-
<span className="text-gray-600">Total to pay</span>
|
|
88
|
-
<span className="text-2xl font-bold text-gray-900">
|
|
89
|
-
{new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)}
|
|
90
|
-
</span>
|
|
91
|
-
</div>
|
|
92
|
-
|
|
93
|
-
{error && (
|
|
94
|
-
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-start gap-2">
|
|
95
|
-
<XCircleIcon className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
|
|
96
|
-
<p className="text-sm text-red-700">{error}</p>
|
|
97
|
-
</div>
|
|
98
|
-
)}
|
|
99
|
-
|
|
100
|
-
{success && (
|
|
101
|
-
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg flex items-start gap-2">
|
|
102
|
-
<CheckCircleIcon className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
|
|
103
|
-
<p className="text-sm text-green-700">Payment successful!</p>
|
|
104
|
-
</div>
|
|
105
|
-
)}
|
|
106
|
-
|
|
107
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
108
|
-
<Input
|
|
109
|
-
label="Cardholder Name"
|
|
110
|
-
placeholder="John Doe"
|
|
111
|
-
value={cardholderName}
|
|
112
|
-
onChange={e => setCardholderName(e.target.value)}
|
|
113
|
-
required
|
|
114
|
-
disabled={loading}
|
|
115
|
-
/>
|
|
116
|
-
|
|
117
|
-
<div className="relative">
|
|
118
|
-
<Input
|
|
119
|
-
label="Card Number"
|
|
120
|
-
placeholder="4242 4242 4242 4242"
|
|
121
|
-
value={cardNumber}
|
|
122
|
-
onChange={e => setCardNumber(e.target.value)}
|
|
123
|
-
required
|
|
124
|
-
disabled={loading}
|
|
125
|
-
/>
|
|
126
|
-
<div className="absolute right-3 top-[34px] flex gap-1">
|
|
127
|
-
<div className="h-5 w-8 bg-gray-200 rounded"></div>
|
|
128
|
-
<div className="h-5 w-8 bg-gray-200 rounded"></div>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<div className="grid grid-cols-2 gap-4">
|
|
133
|
-
<Input
|
|
134
|
-
label="Expiry Date"
|
|
135
|
-
placeholder="MM/YY"
|
|
136
|
-
value={expiry}
|
|
137
|
-
onChange={e => setExpiry(e.target.value)}
|
|
138
|
-
maxLength={5}
|
|
139
|
-
required
|
|
140
|
-
disabled={loading}
|
|
141
|
-
/>
|
|
142
|
-
<Input
|
|
143
|
-
label="CVC"
|
|
144
|
-
placeholder="123"
|
|
145
|
-
value={cvc}
|
|
146
|
-
onChange={e => setCvc(e.target.value)}
|
|
147
|
-
maxLength={4}
|
|
148
|
-
required
|
|
149
|
-
disabled={loading}
|
|
150
|
-
/>
|
|
151
|
-
</div>
|
|
152
|
-
|
|
153
|
-
<div className="pt-4">
|
|
154
|
-
<Button type="submit" className="w-full bg-primus-600 hover:bg-primus-700" size="lg" disabled={loading}>
|
|
155
|
-
{loading ? 'Processing...' : `Pay ${new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)}`}
|
|
156
|
-
</Button>
|
|
157
|
-
<p className="text-center text-xs text-gray-400 mt-3">
|
|
158
|
-
Powered by Primus Payments. Your data is encrypted.
|
|
159
|
-
</p>
|
|
160
|
-
<p className="text-center text-xs text-gray-500 mt-2">
|
|
161
|
-
Test card: 4242 4242 4242 4242
|
|
162
|
-
</p>
|
|
163
|
-
</div>
|
|
164
|
-
</form>
|
|
165
|
-
</div>
|
|
166
|
-
);
|
|
167
|
-
};
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
interface Vulnerability {
|
|
4
|
-
id: string;
|
|
5
|
-
severity: string;
|
|
6
|
-
type: string;
|
|
7
|
-
title: string;
|
|
8
|
-
description: string;
|
|
9
|
-
file?: string;
|
|
10
|
-
line?: number;
|
|
11
|
-
recommendation: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const SecurityDashboard: React.FC<{ apiUrl?: string }> = ({
|
|
15
|
-
apiUrl = 'http://localhost:5221'
|
|
16
|
-
}) => {
|
|
17
|
-
const [vulnerabilities, setVulnerabilities] = useState<Vulnerability[]>([]);
|
|
18
|
-
const [loading, setLoading] = useState(true);
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
fetchVulnerabilities();
|
|
22
|
-
}, []);
|
|
23
|
-
|
|
24
|
-
const fetchVulnerabilities = async () => {
|
|
25
|
-
try {
|
|
26
|
-
const response = await fetch(`${apiUrl}/api/security/vulnerabilities`);
|
|
27
|
-
if (response.ok) {
|
|
28
|
-
const data = await response.json();
|
|
29
|
-
setVulnerabilities(data);
|
|
30
|
-
}
|
|
31
|
-
} catch (error) {
|
|
32
|
-
console.error('Failed to fetch vulnerabilities:', error);
|
|
33
|
-
} finally {
|
|
34
|
-
setLoading(false);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const getSeverityColor = (severity: string) => {
|
|
39
|
-
switch (severity.toLowerCase()) {
|
|
40
|
-
case 'critical': return 'bg-red-100 text-red-800';
|
|
41
|
-
case 'high': return 'bg-orange-100 text-orange-800';
|
|
42
|
-
case 'medium': return 'bg-yellow-100 text-yellow-800';
|
|
43
|
-
case 'low': return 'bg-blue-100 text-blue-800';
|
|
44
|
-
default: return 'bg-gray-100 text-gray-800';
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
if (loading) return <div className="p-4">Scanning for vulnerabilities...</div>;
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
52
|
-
<div className="flex justify-between items-center mb-4">
|
|
53
|
-
<h3 className="text-lg font-medium text-gray-900">Security Dashboard</h3>
|
|
54
|
-
<button
|
|
55
|
-
onClick={fetchVulnerabilities}
|
|
56
|
-
className="px-3 py-1 bg-primus-600 text-white rounded text-sm hover:bg-primus-700"
|
|
57
|
-
>
|
|
58
|
-
Rescan
|
|
59
|
-
</button>
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
<div className="space-y-3">
|
|
63
|
-
{vulnerabilities.map((vuln) => (
|
|
64
|
-
<div key={vuln.id} className="p-4 border border-gray-200 rounded-lg">
|
|
65
|
-
<div className="flex justify-between items-start mb-2">
|
|
66
|
-
<h4 className="font-medium text-gray-900">{vuln.title}</h4>
|
|
67
|
-
<span className={`px-2 py-1 rounded text-xs font-medium ${getSeverityColor(vuln.severity)}`}>
|
|
68
|
-
{vuln.severity}
|
|
69
|
-
</span>
|
|
70
|
-
</div>
|
|
71
|
-
<p className="text-sm text-gray-600 mb-2">{vuln.description}</p>
|
|
72
|
-
{vuln.file && (
|
|
73
|
-
<p className="text-xs text-gray-500 mb-2">
|
|
74
|
-
{vuln.file}{vuln.line ? `:${vuln.line}` : ''}
|
|
75
|
-
</p>
|
|
76
|
-
)}
|
|
77
|
-
<p className="text-sm text-primus-600">💡 {vuln.recommendation}</p>
|
|
78
|
-
</div>
|
|
79
|
-
))}
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
);
|
|
83
|
-
};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { clsx, type ClassValue } from 'clsx';
|
|
3
|
-
import { twMerge } from 'tailwind-merge';
|
|
4
|
-
|
|
5
|
-
function cn(...inputs: ClassValue[]) {
|
|
6
|
-
return twMerge(clsx(inputs));
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
10
|
-
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
|
11
|
-
size?: 'sm' | 'md' | 'lg';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
15
|
-
({ className, variant = 'primary', size = 'md', ...props }, ref) => {
|
|
16
|
-
return (
|
|
17
|
-
<button
|
|
18
|
-
ref={ref}
|
|
19
|
-
className={cn(
|
|
20
|
-
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
21
|
-
{
|
|
22
|
-
'bg-gray-900 text-gray-50 hover:bg-gray-900/90': variant === 'primary',
|
|
23
|
-
'bg-gray-100 text-gray-900 hover:bg-gray-100/80': variant === 'secondary',
|
|
24
|
-
'border border-gray-200 bg-white hover:bg-gray-100 hover:text-gray-900': variant === 'outline',
|
|
25
|
-
'hover:bg-gray-100 hover:text-gray-900': variant === 'ghost',
|
|
26
|
-
'h-9 px-3': size === 'sm',
|
|
27
|
-
'h-10 px-4 py-2': size === 'md',
|
|
28
|
-
'h-11 px-8': size === 'lg',
|
|
29
|
-
},
|
|
30
|
-
className
|
|
31
|
-
)}
|
|
32
|
-
{...props}
|
|
33
|
-
/>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
);
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { clsx, type ClassValue } from 'clsx';
|
|
3
|
-
import { twMerge } from 'tailwind-merge';
|
|
4
|
-
|
|
5
|
-
function cn(...inputs: ClassValue[]) {
|
|
6
|
-
return twMerge(clsx(inputs));
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
10
|
-
label?: string;
|
|
11
|
-
error?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
15
|
-
({ className, label, error, ...props }, ref) => {
|
|
16
|
-
return (
|
|
17
|
-
<div className="w-full">
|
|
18
|
-
{label && (
|
|
19
|
-
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 mb-2 block text-gray-700">
|
|
20
|
-
{label}
|
|
21
|
-
</label>
|
|
22
|
-
)}
|
|
23
|
-
<input
|
|
24
|
-
ref={ref}
|
|
25
|
-
className={cn(
|
|
26
|
-
"flex h-10 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
27
|
-
error && "border-red-500 focus-visible:ring-red-500",
|
|
28
|
-
className
|
|
29
|
-
)}
|
|
30
|
-
{...props}
|
|
31
|
-
/>
|
|
32
|
-
{error && <p className="text-sm text-red-500 mt-1">{error}</p>}
|
|
33
|
-
</div>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
);
|