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,88 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
|
|
3
|
-
export const AICopilot: React.FC<{ apiUrl?: string }> = ({
|
|
4
|
-
apiUrl = 'http://localhost:5221'
|
|
5
|
-
}) => {
|
|
6
|
-
const [messages, setMessages] = useState<Array<{ role: string; content: string }>>([]);
|
|
7
|
-
const [input, setInput] = useState('');
|
|
8
|
-
const [loading, setLoading] = useState(false);
|
|
9
|
-
|
|
10
|
-
const sendMessage = async () => {
|
|
11
|
-
if (!input.trim()) return;
|
|
12
|
-
|
|
13
|
-
const userMessage = { role: 'user', content: input };
|
|
14
|
-
setMessages(prev => [...prev, userMessage]);
|
|
15
|
-
setInput('');
|
|
16
|
-
setLoading(true);
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const response = await fetch(`${apiUrl}/api/ai/chat`, {
|
|
20
|
-
method: 'POST',
|
|
21
|
-
headers: { 'Content-Type': 'application/json' },
|
|
22
|
-
body: JSON.stringify({ message: input })
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
if (response.ok) {
|
|
26
|
-
const data = await response.json();
|
|
27
|
-
setMessages(prev => [...prev, { role: 'assistant', content: data.message }]);
|
|
28
|
-
}
|
|
29
|
-
} catch (error) {
|
|
30
|
-
console.error('Failed to send message:', error);
|
|
31
|
-
} finally {
|
|
32
|
-
setLoading(false);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div className="bg-white rounded-lg border border-gray-200 shadow-sm flex flex-col h-[600px]">
|
|
38
|
-
<div className="p-4 border-b border-gray-200">
|
|
39
|
-
<h3 className="text-lg font-medium text-gray-900">AI Copilot</h3>
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
43
|
-
{messages.map((msg, index) => (
|
|
44
|
-
<div key={index} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
45
|
-
<div className={`max-w-[70%] p-3 rounded-lg ${msg.role === 'user'
|
|
46
|
-
? 'bg-primus-600 text-white'
|
|
47
|
-
: 'bg-gray-100 text-gray-900'
|
|
48
|
-
}`}>
|
|
49
|
-
{msg.content}
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
))}
|
|
53
|
-
{loading && (
|
|
54
|
-
<div className="flex justify-start">
|
|
55
|
-
<div className="bg-gray-100 p-3 rounded-lg">
|
|
56
|
-
<div className="flex gap-1">
|
|
57
|
-
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
|
58
|
-
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
|
59
|
-
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
)}
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
<div className="p-4 border-t border-gray-200">
|
|
67
|
-
<div className="flex gap-2">
|
|
68
|
-
<input
|
|
69
|
-
type="text"
|
|
70
|
-
value={input}
|
|
71
|
-
onChange={(e) => setInput(e.target.value)}
|
|
72
|
-
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
|
|
73
|
-
placeholder="Ask me anything..."
|
|
74
|
-
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primus-500"
|
|
75
|
-
disabled={loading}
|
|
76
|
-
/>
|
|
77
|
-
<button
|
|
78
|
-
onClick={sendMessage}
|
|
79
|
-
disabled={loading || !input.trim()}
|
|
80
|
-
className="px-4 py-2 bg-primus-600 text-white rounded-lg hover:bg-primus-700 disabled:opacity-50"
|
|
81
|
-
>
|
|
82
|
-
Send
|
|
83
|
-
</button>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
);
|
|
88
|
-
};
|
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import { usePrimusTheme } from '../../context/PrimusThemeProvider';
|
|
3
|
-
|
|
4
|
-
// ============================================================
|
|
5
|
-
// TYPES
|
|
6
|
-
// ============================================================
|
|
7
|
-
|
|
8
|
-
export type SocialProvider = 'google' | 'azure' | 'github' | 'apple' | 'microsoft' | 'facebook';
|
|
9
|
-
|
|
10
|
-
export interface PrimusLoginProps {
|
|
11
|
-
/** Callback when login is successful */
|
|
12
|
-
onLogin?: (credentials: { username: string; password: string }) => void;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Social login providers to display
|
|
16
|
-
* @example socialProviders={['google', 'azure']}
|
|
17
|
-
*/
|
|
18
|
-
socialProviders?: SocialProvider[];
|
|
19
|
-
|
|
20
|
-
/** Callback when social provider is clicked */
|
|
21
|
-
onSocialLogin?: (provider: SocialProvider) => void;
|
|
22
|
-
|
|
23
|
-
/** OAuth endpoint base URL (default: /api/auth) */
|
|
24
|
-
authEndpoint?: string;
|
|
25
|
-
|
|
26
|
-
/** Show email/password form (default: true) */
|
|
27
|
-
showEmailLogin?: boolean;
|
|
28
|
-
|
|
29
|
-
/** App title displayed above form */
|
|
30
|
-
title?: string;
|
|
31
|
-
|
|
32
|
-
/** Subtitle/description text */
|
|
33
|
-
subtitle?: string;
|
|
34
|
-
|
|
35
|
-
/** Logo element or URL */
|
|
36
|
-
logo?: React.ReactNode | string;
|
|
37
|
-
|
|
38
|
-
/** Force specific theme */
|
|
39
|
-
theme?: 'light' | 'dark';
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ============================================================
|
|
43
|
-
// PROVIDER CONFIGS (internal)
|
|
44
|
-
// ============================================================
|
|
45
|
-
|
|
46
|
-
const providerConfigs: Record<SocialProvider, { name: string; icon: JSX.Element; colors: { light: string; dark: string } }> = {
|
|
47
|
-
google: {
|
|
48
|
-
name: 'Google',
|
|
49
|
-
icon: (
|
|
50
|
-
<svg className="h-5 w-5" viewBox="0 0 24 24">
|
|
51
|
-
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
|
52
|
-
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
|
53
|
-
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
|
54
|
-
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
|
55
|
-
</svg>
|
|
56
|
-
),
|
|
57
|
-
colors: { light: 'bg-white hover:bg-gray-50 text-gray-700 border-gray-300', dark: 'bg-white hover:bg-gray-100 text-gray-700 border-gray-300' }
|
|
58
|
-
},
|
|
59
|
-
azure: {
|
|
60
|
-
name: 'Microsoft',
|
|
61
|
-
icon: (
|
|
62
|
-
<svg className="h-5 w-5" viewBox="0 0 23 23">
|
|
63
|
-
<path fill="#f35325" d="M1 1h10v10H1z" />
|
|
64
|
-
<path fill="#81bc06" d="M12 1h10v10H12z" />
|
|
65
|
-
<path fill="#05a6f0" d="M1 12h10v10H1z" />
|
|
66
|
-
<path fill="#ffba08" d="M12 12h10v10H12z" />
|
|
67
|
-
</svg>
|
|
68
|
-
),
|
|
69
|
-
colors: { light: 'bg-white hover:bg-gray-50 text-gray-700 border-gray-300', dark: 'bg-white hover:bg-gray-100 text-gray-700 border-gray-300' }
|
|
70
|
-
},
|
|
71
|
-
github: {
|
|
72
|
-
name: 'GitHub',
|
|
73
|
-
icon: (
|
|
74
|
-
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
|
75
|
-
<path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
|
|
76
|
-
</svg>
|
|
77
|
-
),
|
|
78
|
-
colors: { light: 'bg-gray-900 hover:bg-gray-800 text-white border-gray-900', dark: 'bg-white hover:bg-gray-100 text-gray-900 border-gray-300' }
|
|
79
|
-
},
|
|
80
|
-
microsoft: {
|
|
81
|
-
name: 'Microsoft',
|
|
82
|
-
icon: (
|
|
83
|
-
<svg className="h-5 w-5" viewBox="0 0 24 24">
|
|
84
|
-
<path fill="#F25022" d="M1 1h10v10H1z" />
|
|
85
|
-
<path fill="#00A4EF" d="M1 13h10v10H1z" />
|
|
86
|
-
<path fill="#7FBA00" d="M13 1h10v10H13z" />
|
|
87
|
-
<path fill="#FFB900" d="M13 13h10v10H13z" />
|
|
88
|
-
</svg>
|
|
89
|
-
),
|
|
90
|
-
colors: { light: 'bg-white hover:bg-gray-50 text-gray-700 border-gray-300', dark: 'bg-white hover:bg-gray-100 text-gray-700 border-gray-300' }
|
|
91
|
-
},
|
|
92
|
-
apple: {
|
|
93
|
-
name: 'Apple',
|
|
94
|
-
icon: (
|
|
95
|
-
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
|
96
|
-
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
|
97
|
-
</svg>
|
|
98
|
-
),
|
|
99
|
-
colors: { light: 'bg-black hover:bg-gray-900 text-white border-black', dark: 'bg-white hover:bg-gray-100 text-black border-gray-300' }
|
|
100
|
-
},
|
|
101
|
-
facebook: {
|
|
102
|
-
name: 'Facebook',
|
|
103
|
-
icon: (
|
|
104
|
-
<svg className="h-5 w-5" fill="white" viewBox="0 0 24 24">
|
|
105
|
-
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
|
|
106
|
-
</svg>
|
|
107
|
-
),
|
|
108
|
-
colors: { light: 'bg-[#1877F2] hover:bg-[#166FE5] text-white border-[#1877F2]', dark: 'bg-[#1877F2] hover:bg-[#166FE5] text-white border-[#1877F2]' }
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// ============================================================
|
|
113
|
-
// COMPONENT
|
|
114
|
-
// ============================================================
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* PrimusLogin - Complete Login Component
|
|
118
|
-
*
|
|
119
|
-
* A single component that handles all login methods. Enable features via props.
|
|
120
|
-
*
|
|
121
|
-
* @example
|
|
122
|
-
* ```tsx
|
|
123
|
-
* // Basic email/password only
|
|
124
|
-
* <PrimusLogin onLogin={(creds) => console.log(creds)} />
|
|
125
|
-
*
|
|
126
|
-
* // With social providers
|
|
127
|
-
* <PrimusLogin
|
|
128
|
-
* socialProviders={['google', 'azure']}
|
|
129
|
-
* onSocialLogin={(provider) => redirectToOAuth(provider)}
|
|
130
|
-
* />
|
|
131
|
-
*
|
|
132
|
-
* // Social only (no email form)
|
|
133
|
-
* <PrimusLogin
|
|
134
|
-
* showEmailLogin={false}
|
|
135
|
-
* socialProviders={['google', 'github', 'azure']}
|
|
136
|
-
* />
|
|
137
|
-
* ```
|
|
138
|
-
*/
|
|
139
|
-
export function PrimusLogin({
|
|
140
|
-
onLogin,
|
|
141
|
-
socialProviders = [],
|
|
142
|
-
onSocialLogin,
|
|
143
|
-
authEndpoint = '/api/auth',
|
|
144
|
-
showEmailLogin = true,
|
|
145
|
-
title = 'Welcome Back',
|
|
146
|
-
subtitle = 'Enter your credentials to continue',
|
|
147
|
-
logo,
|
|
148
|
-
theme: themeOverride,
|
|
149
|
-
}: PrimusLoginProps) {
|
|
150
|
-
const themeContext = usePrimusTheme();
|
|
151
|
-
const theme = themeOverride || themeContext.theme;
|
|
152
|
-
const isLight = theme === 'light';
|
|
153
|
-
|
|
154
|
-
const [username, setUsername] = useState('');
|
|
155
|
-
const [password, setPassword] = useState('');
|
|
156
|
-
const [loading, setLoading] = useState(false);
|
|
157
|
-
|
|
158
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
159
|
-
e.preventDefault();
|
|
160
|
-
if (!username || !password) return;
|
|
161
|
-
|
|
162
|
-
setLoading(true);
|
|
163
|
-
try {
|
|
164
|
-
onLogin?.({ username, password });
|
|
165
|
-
} finally {
|
|
166
|
-
setLoading(false);
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const handleSocialClick = (provider: SocialProvider) => {
|
|
171
|
-
if (onSocialLogin) {
|
|
172
|
-
onSocialLogin(provider);
|
|
173
|
-
} else {
|
|
174
|
-
window.location.href = `${authEndpoint}/${provider}`;
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
// Theme colors
|
|
179
|
-
const bgGradient = isLight
|
|
180
|
-
? 'bg-gradient-to-br from-gray-50 via-white to-violet-50'
|
|
181
|
-
: 'bg-gray-950';
|
|
182
|
-
const cardBg = isLight
|
|
183
|
-
? 'bg-white border-gray-200 shadow-xl'
|
|
184
|
-
: 'bg-gray-900/50 border-white/10';
|
|
185
|
-
const headingColor = isLight ? 'text-gray-900' : 'text-white';
|
|
186
|
-
const subtextColor = isLight ? 'text-gray-500' : 'text-slate-400';
|
|
187
|
-
const labelColor = isLight ? 'text-gray-600' : 'text-slate-300';
|
|
188
|
-
const inputClasses = isLight
|
|
189
|
-
? 'bg-gray-50 border-gray-300 text-gray-900 placeholder:text-gray-400 focus:border-violet-500 focus:ring-violet-500'
|
|
190
|
-
: 'bg-gray-950/50 border-slate-700 text-white placeholder:text-slate-600 focus:border-blue-500 focus:ring-blue-500';
|
|
191
|
-
const buttonGradient = isLight
|
|
192
|
-
? 'bg-gradient-to-r from-violet-600 to-purple-600'
|
|
193
|
-
: 'bg-gradient-to-r from-blue-600 to-indigo-600';
|
|
194
|
-
const logoBg = isLight ? 'bg-violet-600 shadow-violet-500/20' : 'bg-blue-600 shadow-blue-500/20';
|
|
195
|
-
const dividerColor = isLight ? 'border-gray-200' : 'border-slate-700';
|
|
196
|
-
|
|
197
|
-
const hasSocial = socialProviders.length > 0;
|
|
198
|
-
|
|
199
|
-
return (
|
|
200
|
-
<div className={`min-h-screen ${bgGradient} flex items-center justify-center p-4`}>
|
|
201
|
-
{!isLight && (
|
|
202
|
-
<div className="absolute inset-0 overflow-hidden">
|
|
203
|
-
<div className="absolute -top-[20%] -left-[10%] w-[50%] h-[50%] bg-blue-600/20 rounded-full blur-[120px]" />
|
|
204
|
-
<div className="absolute -bottom-[20%] -right-[10%] w-[50%] h-[50%] bg-purple-600/20 rounded-full blur-[120px]" />
|
|
205
|
-
</div>
|
|
206
|
-
)}
|
|
207
|
-
|
|
208
|
-
<div className={`w-full max-w-md ${cardBg} p-8 rounded-2xl border backdrop-blur-xl relative z-10`}>
|
|
209
|
-
{/* Logo & Title */}
|
|
210
|
-
<div className="text-center mb-8">
|
|
211
|
-
{logo ? (
|
|
212
|
-
typeof logo === 'string'
|
|
213
|
-
? <img src={logo} alt="Logo" className="h-12 w-12 mx-auto mb-4 rounded-xl" />
|
|
214
|
-
: <div className="mb-4">{logo}</div>
|
|
215
|
-
) : (
|
|
216
|
-
<div className={`h-12 w-12 ${logoBg} rounded-xl mx-auto flex items-center justify-center mb-4 shadow-lg`}>
|
|
217
|
-
<span className="font-bold text-xl text-white">P</span>
|
|
218
|
-
</div>
|
|
219
|
-
)}
|
|
220
|
-
<h1 className={`text-2xl font-bold ${headingColor} tracking-tight`}>{title}</h1>
|
|
221
|
-
<p className={`${subtextColor} text-sm mt-2`}>{subtitle}</p>
|
|
222
|
-
</div>
|
|
223
|
-
|
|
224
|
-
{/* Social Login Buttons */}
|
|
225
|
-
{hasSocial && (
|
|
226
|
-
<div className="space-y-3 mb-6">
|
|
227
|
-
{socialProviders.map((provider) => {
|
|
228
|
-
const config = providerConfigs[provider];
|
|
229
|
-
if (!config) return null;
|
|
230
|
-
|
|
231
|
-
return (
|
|
232
|
-
<button
|
|
233
|
-
key={provider}
|
|
234
|
-
onClick={() => handleSocialClick(provider)}
|
|
235
|
-
className={`w-full flex items-center justify-center gap-3 px-4 py-2.5 border rounded-lg font-medium text-sm transition-all duration-200 ${isLight ? config.colors.light : config.colors.dark
|
|
236
|
-
}`}
|
|
237
|
-
>
|
|
238
|
-
{config.icon}
|
|
239
|
-
<span>Continue with {config.name}</span>
|
|
240
|
-
</button>
|
|
241
|
-
);
|
|
242
|
-
})}
|
|
243
|
-
</div>
|
|
244
|
-
)}
|
|
245
|
-
|
|
246
|
-
{/* Divider */}
|
|
247
|
-
{hasSocial && showEmailLogin && (
|
|
248
|
-
<div className="relative my-6">
|
|
249
|
-
<div className="absolute inset-0 flex items-center">
|
|
250
|
-
<div className={`w-full border-t ${dividerColor}`}></div>
|
|
251
|
-
</div>
|
|
252
|
-
<div className="relative flex justify-center text-xs uppercase">
|
|
253
|
-
<span className={`${isLight ? 'bg-white' : 'bg-gray-900/50'} px-2 ${subtextColor}`}>
|
|
254
|
-
Or continue with email
|
|
255
|
-
</span>
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
)}
|
|
259
|
-
|
|
260
|
-
{/* Email/Password Form */}
|
|
261
|
-
{showEmailLogin && (
|
|
262
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
263
|
-
<div className="space-y-2">
|
|
264
|
-
<label className={`text-xs font-medium ${labelColor} ml-1`}>EMAIL</label>
|
|
265
|
-
<input
|
|
266
|
-
type="text"
|
|
267
|
-
value={username}
|
|
268
|
-
onChange={e => setUsername(e.target.value)}
|
|
269
|
-
className={`w-full px-4 py-2.5 ${inputClasses} border rounded-lg focus:outline-none focus:ring-1 transition-all text-sm`}
|
|
270
|
-
placeholder="you@example.com"
|
|
271
|
-
/>
|
|
272
|
-
</div>
|
|
273
|
-
<div className="space-y-2">
|
|
274
|
-
<label className={`text-xs font-medium ${labelColor} ml-1`}>PASSWORD</label>
|
|
275
|
-
<input
|
|
276
|
-
type="password"
|
|
277
|
-
value={password}
|
|
278
|
-
onChange={e => setPassword(e.target.value)}
|
|
279
|
-
className={`w-full px-4 py-2.5 ${inputClasses} border rounded-lg focus:outline-none focus:ring-1 transition-all text-sm`}
|
|
280
|
-
placeholder="••••••••"
|
|
281
|
-
/>
|
|
282
|
-
</div>
|
|
283
|
-
<button
|
|
284
|
-
type="submit"
|
|
285
|
-
disabled={loading}
|
|
286
|
-
className={`w-full mt-4 py-2.5 ${buttonGradient} text-white font-medium rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50`}
|
|
287
|
-
>
|
|
288
|
-
{loading ? 'Signing in...' : 'Sign In'}
|
|
289
|
-
</button>
|
|
290
|
-
</form>
|
|
291
|
-
)}
|
|
292
|
-
</div>
|
|
293
|
-
</div>
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Export alias
|
|
298
|
-
export const LoginPage = PrimusLogin;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { usePrimusAuth } from '../../hooks/usePrimusAuth';
|
|
3
|
-
import { Button } from '../shared/Button';
|
|
4
|
-
|
|
5
|
-
export const UserProfile: React.FC = () => {
|
|
6
|
-
const { user, logout, isAuthenticated } = usePrimusAuth();
|
|
7
|
-
|
|
8
|
-
if (!isAuthenticated || !user) {
|
|
9
|
-
return null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<div className="flex items-center gap-4 p-4 rounded-lg bg-gray-50 border border-gray-200">
|
|
14
|
-
<div className="h-10 w-10 rounded-full bg-primus-100 flex items-center justify-center text-primus-600 font-bold">
|
|
15
|
-
{user.name?.charAt(0) || 'U'}
|
|
16
|
-
</div>
|
|
17
|
-
<div className="flex-1">
|
|
18
|
-
<p className="text-sm font-medium text-gray-900">{user.name}</p>
|
|
19
|
-
<p className="text-xs text-gray-500">{user.email}</p>
|
|
20
|
-
</div>
|
|
21
|
-
<Button variant="outline" size="sm" onClick={() => logout()}>
|
|
22
|
-
Sign out
|
|
23
|
-
</Button>
|
|
24
|
-
</div>
|
|
25
|
-
);
|
|
26
|
-
};
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
interface Account {
|
|
4
|
-
id: string;
|
|
5
|
-
accountNumber: string;
|
|
6
|
-
type: string;
|
|
7
|
-
balance: number;
|
|
8
|
-
currency: string;
|
|
9
|
-
status: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const AccountDashboard: React.FC<{ apiUrl?: string }> = ({
|
|
13
|
-
apiUrl = 'http://localhost:5221'
|
|
14
|
-
}) => {
|
|
15
|
-
const [accounts, setAccounts] = useState<Account[]>([]);
|
|
16
|
-
const [loading, setLoading] = useState(true);
|
|
17
|
-
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
fetchAccounts();
|
|
20
|
-
}, []);
|
|
21
|
-
|
|
22
|
-
const fetchAccounts = async () => {
|
|
23
|
-
try {
|
|
24
|
-
const response = await fetch(`${apiUrl}/api/banking/accounts`);
|
|
25
|
-
if (response.ok) {
|
|
26
|
-
const data = await response.json();
|
|
27
|
-
setAccounts(data);
|
|
28
|
-
}
|
|
29
|
-
} catch (error) {
|
|
30
|
-
console.error('Failed to fetch accounts:', error);
|
|
31
|
-
} finally {
|
|
32
|
-
setLoading(false);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
if (loading) return <div className="p-4">Loading accounts...</div>;
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
40
|
-
<h3 className="text-lg font-medium text-gray-900 mb-4">Bank Accounts</h3>
|
|
41
|
-
<div className="space-y-4">
|
|
42
|
-
{accounts.map((account) => (
|
|
43
|
-
<div key={account.id} className="p-4 bg-gray-50 rounded-lg border border-gray-200">
|
|
44
|
-
<div className="flex justify-between items-start">
|
|
45
|
-
<div>
|
|
46
|
-
<h4 className="font-medium text-gray-900">{account.type}</h4>
|
|
47
|
-
<p className="text-sm text-gray-500">{account.accountNumber}</p>
|
|
48
|
-
</div>
|
|
49
|
-
<div className="text-right">
|
|
50
|
-
<p className="text-2xl font-bold text-gray-900">
|
|
51
|
-
${account.balance.toLocaleString()}
|
|
52
|
-
</p>
|
|
53
|
-
<p className="text-sm text-gray-500">{account.currency}</p>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
<div className="mt-2">
|
|
57
|
-
<span className={`px-2 py-1 rounded text-xs font-medium ${account.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
|
58
|
-
}`}>
|
|
59
|
-
{account.status}
|
|
60
|
-
</span>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
))}
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
};
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { clsx } from 'clsx';
|
|
3
|
-
import { WifiIcon } from '@heroicons/react/24/outline'; // Approximate for contactless
|
|
4
|
-
|
|
5
|
-
export interface CreditCardProps {
|
|
6
|
-
cardHolder: string;
|
|
7
|
-
last4: string;
|
|
8
|
-
expiry: string;
|
|
9
|
-
brand?: 'visa' | 'mastercard' | 'amex';
|
|
10
|
-
variant?: 'black' | 'blue' | 'gold' | 'platinum';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const CreditCardVisual: React.FC<CreditCardProps> = ({
|
|
14
|
-
cardHolder,
|
|
15
|
-
last4,
|
|
16
|
-
expiry,
|
|
17
|
-
brand = 'visa',
|
|
18
|
-
variant = 'black'
|
|
19
|
-
}) => {
|
|
20
|
-
const getBackground = () => {
|
|
21
|
-
switch (variant) {
|
|
22
|
-
case 'blue': return 'bg-gradient-to-br from-blue-600 to-blue-800';
|
|
23
|
-
case 'gold': return 'bg-gradient-to-br from-yellow-500 to-yellow-700';
|
|
24
|
-
case 'platinum': return 'bg-gradient-to-br from-gray-300 to-gray-500';
|
|
25
|
-
default: return 'bg-gradient-to-br from-gray-900 to-gray-800';
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div className={clsx(
|
|
31
|
-
"w-80 h-48 rounded-2xl p-6 text-white shadow-xl relative overflow-hidden transition-transform hover:scale-105 duration-300",
|
|
32
|
-
getBackground()
|
|
33
|
-
)}>
|
|
34
|
-
{/* Decorative circles */}
|
|
35
|
-
<div className="absolute top-0 right-0 -mr-10 -mt-10 w-40 h-40 bg-white opacity-5 rounded-full blur-xl" />
|
|
36
|
-
<div className="absolute bottom-0 left-0 -ml-10 -mb-10 w-40 h-40 bg-white opacity-5 rounded-full blur-xl" />
|
|
37
|
-
|
|
38
|
-
<div className="flex justify-between items-start mb-8">
|
|
39
|
-
{/* Chip */}
|
|
40
|
-
<div className="w-12 h-9 bg-yellow-400 rounded-md opacity-80 flex items-center justify-center">
|
|
41
|
-
<div className="w-8 h-6 border border-yellow-600 rounded opacity-50" />
|
|
42
|
-
</div>
|
|
43
|
-
<WifiIcon className="h-6 w-6 rotate-90 opacity-70" />
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<div className="mb-6">
|
|
47
|
-
<p className="font-mono text-xl tracking-widest text-shadow">
|
|
48
|
-
•••• •••• •••• {last4}
|
|
49
|
-
</p>
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
<div className="flex justify-between items-end">
|
|
53
|
-
<div>
|
|
54
|
-
<p className="text-[10px] uppercase text-gray-300 tracking-wider mb-1">Card Holder</p>
|
|
55
|
-
<p className="font-medium tracking-wide uppercase text-sm truncate max-w-[160px]">{cardHolder}</p>
|
|
56
|
-
</div>
|
|
57
|
-
<div>
|
|
58
|
-
<p className="text-[10px] uppercase text-gray-300 tracking-wider mb-1">Expires</p>
|
|
59
|
-
<p className="font-medium tracking-wide text-sm">{expiry}</p>
|
|
60
|
-
</div>
|
|
61
|
-
<div className="font-bold text-lg italic opacity-90">
|
|
62
|
-
{brand.toUpperCase()}
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
};
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
|
|
3
|
-
interface CreditScore {
|
|
4
|
-
score: number;
|
|
5
|
-
rating: string;
|
|
6
|
-
lastUpdated: string;
|
|
7
|
-
factors: Array<{ factor: string; impact: string; weight: number }>;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const CreditScoreCard: React.FC<{ userId: string; apiUrl?: string }> = ({
|
|
11
|
-
userId,
|
|
12
|
-
apiUrl = 'http://localhost:5221'
|
|
13
|
-
}) => {
|
|
14
|
-
const [score, setScore] = useState<CreditScore | null>(null);
|
|
15
|
-
const [loading, setLoading] = useState(true);
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
fetchScore();
|
|
19
|
-
}, [userId]);
|
|
20
|
-
|
|
21
|
-
const fetchScore = async () => {
|
|
22
|
-
try {
|
|
23
|
-
const response = await fetch(`${apiUrl}/api/banking/credit-score/${userId}`);
|
|
24
|
-
if (response.ok) {
|
|
25
|
-
const data = await response.json();
|
|
26
|
-
setScore(data);
|
|
27
|
-
}
|
|
28
|
-
} catch (error) {
|
|
29
|
-
console.error('Failed to fetch credit score:', error);
|
|
30
|
-
} finally {
|
|
31
|
-
setLoading(false);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
if (loading) return <div className="p-4">Loading credit score...</div>;
|
|
36
|
-
if (!score) return <div className="p-4">No credit score data</div>;
|
|
37
|
-
|
|
38
|
-
const getScoreColor = (score: number) => {
|
|
39
|
-
if (score >= 740) return 'text-green-600';
|
|
40
|
-
if (score >= 670) return 'text-blue-600';
|
|
41
|
-
if (score >= 580) return 'text-yellow-600';
|
|
42
|
-
return 'text-red-600';
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
|
|
47
|
-
<h3 className="text-lg font-medium text-gray-900 mb-4">Credit Score</h3>
|
|
48
|
-
|
|
49
|
-
<div className="text-center mb-6">
|
|
50
|
-
<div className={`text-6xl font-bold ${getScoreColor(score.score)}`}>
|
|
51
|
-
{score.score}
|
|
52
|
-
</div>
|
|
53
|
-
<div className="text-lg text-gray-600 mt-2">{score.rating}</div>
|
|
54
|
-
<div className="text-sm text-gray-400 mt-1">
|
|
55
|
-
Updated: {new Date(score.lastUpdated).toLocaleDateString()}
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<div>
|
|
60
|
-
<h4 className="font-medium text-gray-900 mb-3">Score Factors</h4>
|
|
61
|
-
<div className="space-y-2">
|
|
62
|
-
{score.factors.map((factor, index) => (
|
|
63
|
-
<div key={index} className="flex justify-between items-center p-2 bg-gray-50 rounded">
|
|
64
|
-
<span className="text-sm text-gray-900">{factor.factor}</span>
|
|
65
|
-
<div className="flex items-center gap-2">
|
|
66
|
-
<span className="text-xs text-gray-500">{factor.weight}%</span>
|
|
67
|
-
<span className={`px-2 py-0.5 rounded text-xs font-medium ${factor.impact === 'positive' ? 'bg-green-100 text-green-800' :
|
|
68
|
-
factor.impact === 'negative' ? 'bg-red-100 text-red-800' :
|
|
69
|
-
'bg-gray-100 text-gray-800'
|
|
70
|
-
}`}>
|
|
71
|
-
{factor.impact}
|
|
72
|
-
</span>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
))}
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
);
|
|
80
|
-
};
|