@visns-studio/visns-components 5.5.5 → 5.5.7
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/README.md +132 -0
- package/package.json +1 -1
- package/src/components/crm/auth/ClientLogin.jsx +126 -0
- package/src/components/crm/auth/ClientOTPVerify.jsx +257 -0
- package/src/components/crm/auth/styles/ClientAuth.module.scss +167 -0
- package/src/components/crm/client/ClientDashboard.jsx +62 -0
- package/src/components/crm/client/ClientPortal.jsx +158 -0
- package/src/components/crm/client/styles/ClientDashboard.module.scss +133 -0
- package/src/components/crm/client/styles/ClientPortal.module.scss +240 -0
- package/src/components/crm/generic/GenericAuth.jsx +175 -0
- package/src/components/crm/generic/GenericClientPortal.jsx +165 -0
- package/src/components/crm/generic/GenericReport.jsx +787 -275
- package/src/components/crm/generic/styles/GenericClientPortal.module.scss +154 -0
- package/src/components/crm/generic/styles/GenericReport.module.scss +200 -0
- package/src/index.js +10 -0
package/README.md
CHANGED
|
@@ -111,6 +111,18 @@ The main component that handles authentication and routing.
|
|
|
111
111
|
routeConfig={routeConfig} // Route configuration
|
|
112
112
|
themeType="primary" // Theme type: 'primary', 'secondary', etc.
|
|
113
113
|
enforce2FA={false} // Whether to enforce 2FA (default: false)
|
|
114
|
+
clientPortalConfig={{
|
|
115
|
+
component: <ClientPortalComponent />, // Main component for client portal
|
|
116
|
+
urls: {
|
|
117
|
+
login: '/clientPortal/login',
|
|
118
|
+
verify: '/clientPortal/verify',
|
|
119
|
+
logout: '/clientPortal/logout',
|
|
120
|
+
profile: '/clientPortal/profile',
|
|
121
|
+
},
|
|
122
|
+
keys: {
|
|
123
|
+
name: ['firstname', 'surname'], // Fields to use for client name display
|
|
124
|
+
},
|
|
125
|
+
}}
|
|
114
126
|
/>
|
|
115
127
|
```
|
|
116
128
|
|
|
@@ -394,6 +406,126 @@ The QrCode component fetches QR code data from the specified URL and displays it
|
|
|
394
406
|
|
|
395
407
|
## Two-Factor Authentication (2FA)
|
|
396
408
|
|
|
409
|
+
## Client Portal
|
|
410
|
+
|
|
411
|
+
The GenericAuth component includes a client portal feature that provides a separate authentication flow and interface for clients.
|
|
412
|
+
|
|
413
|
+
### Client Portal Configuration
|
|
414
|
+
|
|
415
|
+
```jsx
|
|
416
|
+
<GenericAuth
|
|
417
|
+
// ... other props
|
|
418
|
+
clientPortalConfig={{
|
|
419
|
+
component: <ClientPortalComponent />, // Main component for client portal
|
|
420
|
+
urls: {
|
|
421
|
+
login: '/clientPortal/login', // API endpoint for client login
|
|
422
|
+
verify: '/clientPortal/verify', // API endpoint for OTP verification
|
|
423
|
+
logout: '/clientPortal/logout', // API endpoint for client logout
|
|
424
|
+
profile: '/clientPortal/profile', // API endpoint for client profile
|
|
425
|
+
},
|
|
426
|
+
routes: {
|
|
427
|
+
// Optional additional routes
|
|
428
|
+
profile: ClientProfileComponent,
|
|
429
|
+
documents: ClientDocumentsComponent,
|
|
430
|
+
},
|
|
431
|
+
keys: {
|
|
432
|
+
name: ['firstname', 'surname'], // Fields to use for client name display
|
|
433
|
+
},
|
|
434
|
+
}}
|
|
435
|
+
/>
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
#### Client Name Display Configuration
|
|
439
|
+
|
|
440
|
+
The `keys.name` property in the `clientPortalConfig` allows you to specify which fields from the client profile should be used to display the client's name in the portal header.
|
|
441
|
+
|
|
442
|
+
- If `keys.name` is an array of strings, the values from those fields will be concatenated with spaces in between.
|
|
443
|
+
- If `keys.name` is a single string, the value from that field will be used directly.
|
|
444
|
+
- If the specified fields are empty or not found, it will fall back to `clientProfile.name` or `clientProfile.email`.
|
|
445
|
+
|
|
446
|
+
Examples:
|
|
447
|
+
|
|
448
|
+
```jsx
|
|
449
|
+
// Using multiple fields (firstname and surname)
|
|
450
|
+
keys: {
|
|
451
|
+
name: ['firstname', 'surname'];
|
|
452
|
+
}
|
|
453
|
+
// Result: "John Smith"
|
|
454
|
+
|
|
455
|
+
// Using a single field
|
|
456
|
+
keys: {
|
|
457
|
+
name: 'fullname';
|
|
458
|
+
}
|
|
459
|
+
// Result: "John Smith"
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Passing Props to Client Portal Components
|
|
463
|
+
|
|
464
|
+
To pass props to your client portal component, use a function component approach:
|
|
465
|
+
|
|
466
|
+
```jsx
|
|
467
|
+
<GenericAuth
|
|
468
|
+
// ... other props
|
|
469
|
+
clientPortalConfig={{
|
|
470
|
+
component: (props) => (
|
|
471
|
+
<ClientPortalComponent {...props} customProp="value" />
|
|
472
|
+
),
|
|
473
|
+
// ... other config
|
|
474
|
+
}}
|
|
475
|
+
/>
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
The function will receive these props:
|
|
479
|
+
|
|
480
|
+
- `clientProfile`: The full client profile object
|
|
481
|
+
- `setClientProfile`: Function to update the client profile
|
|
482
|
+
- `clientId`: The client ID extracted from clientProfile.id
|
|
483
|
+
|
|
484
|
+
You can then access these props in your component:
|
|
485
|
+
|
|
486
|
+
```jsx
|
|
487
|
+
const ClientPortalComponent = ({
|
|
488
|
+
clientProfile,
|
|
489
|
+
setClientProfile,
|
|
490
|
+
clientId,
|
|
491
|
+
customProp, // Your custom prop
|
|
492
|
+
}) => {
|
|
493
|
+
return (
|
|
494
|
+
<div>
|
|
495
|
+
<h1>
|
|
496
|
+
Welcome, {clientProfile.firstname} {clientProfile.surname}
|
|
497
|
+
</h1>
|
|
498
|
+
<p>Client ID: {clientId}</p>
|
|
499
|
+
<p>Custom prop: {customProp}</p>
|
|
500
|
+
{/* Your component content */}
|
|
501
|
+
</div>
|
|
502
|
+
);
|
|
503
|
+
};
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Client Authentication Flow
|
|
507
|
+
|
|
508
|
+
1. Client enters email address on the client login page
|
|
509
|
+
2. System sends a one-time password (OTP) to the client's email
|
|
510
|
+
3. Client enters the OTP on the verification page
|
|
511
|
+
4. If the OTP is valid, the client is authenticated and redirected to the client portal
|
|
512
|
+
|
|
513
|
+
### Client Portal Routes
|
|
514
|
+
|
|
515
|
+
The client portal uses these routes:
|
|
516
|
+
|
|
517
|
+
- `/client/login` - Client login page
|
|
518
|
+
- `/client/verify` - OTP verification page
|
|
519
|
+
- `/client/portal` - Main client portal (protected, requires authentication)
|
|
520
|
+
- `/client/portal/profile` - Client profile page (if configured in routes)
|
|
521
|
+
- `/client/portal/documents` - Client documents page (if configured in routes)
|
|
522
|
+
|
|
523
|
+
### Route Grouping
|
|
524
|
+
|
|
525
|
+
Routes in the client portal are grouped by prefix for better organization. All client-related routes are grouped under the `/client` prefix, which makes the routing structure cleaner and more maintainable.
|
|
526
|
+
|
|
527
|
+
This grouping is handled internally by the GenericAuth component, so you don't need to include the `/client` prefix in your route configurations.
|
|
528
|
+
|
|
397
529
|
### How 2FA Works
|
|
398
530
|
|
|
399
531
|
1. User enters email and password on the login screen
|
package/package.json
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
83
83
|
},
|
|
84
84
|
"name": "@visns-studio/visns-components",
|
|
85
|
-
"version": "5.5.
|
|
85
|
+
"version": "5.5.7",
|
|
86
86
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
87
87
|
"main": "src/index.js",
|
|
88
88
|
"files": [
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import '../../styles/global.css';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { useLocation, useNavigate } from 'react-router-dom';
|
|
5
|
+
import { toast } from 'react-toastify';
|
|
6
|
+
import { Person } from 'akar-icons';
|
|
7
|
+
|
|
8
|
+
import CustomFetch from '../Fetch';
|
|
9
|
+
|
|
10
|
+
import styles from './styles/ClientAuth.module.scss';
|
|
11
|
+
|
|
12
|
+
const ClientLogin = ({ logo, setSystemAuth, urls = {} }) => {
|
|
13
|
+
const location = useLocation();
|
|
14
|
+
const navigate = useNavigate();
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
const [email, setEmail] = useState('');
|
|
17
|
+
const [inputError, setInputError] = useState(false);
|
|
18
|
+
|
|
19
|
+
const handleChange = (e) => {
|
|
20
|
+
setEmail(e.target.value);
|
|
21
|
+
setInputError(false);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const handleSubmit = async (e) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
|
|
27
|
+
if (!email.trim()) {
|
|
28
|
+
setInputError(true);
|
|
29
|
+
toast.error('Please enter your email address');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setIsLoading(true);
|
|
34
|
+
toast.info('Sending verification code...', { autoClose: 2000 });
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const loginUrl = urls.login;
|
|
38
|
+
const result = await CustomFetch(loginUrl, 'POST', {
|
|
39
|
+
email: email,
|
|
40
|
+
location:
|
|
41
|
+
location.state && location.state.previousUrl
|
|
42
|
+
? location.state.previousUrl
|
|
43
|
+
: '',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
|
|
48
|
+
if (result.data && result.data.success) {
|
|
49
|
+
// Redirect to OTP verification page with email and uuid
|
|
50
|
+
const verifyUrl = '/client/verify';
|
|
51
|
+
navigate(verifyUrl, {
|
|
52
|
+
state: {
|
|
53
|
+
email: email,
|
|
54
|
+
uuid: result.data.uuid,
|
|
55
|
+
previousUrl: location.state?.previousUrl || '',
|
|
56
|
+
// In development, we can pass the OTP for testing
|
|
57
|
+
...(process.env.NODE_ENV === 'development' && {
|
|
58
|
+
otp: result.data.otp,
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
toast.success(
|
|
63
|
+
result.data.message ||
|
|
64
|
+
'Verification code sent to your email'
|
|
65
|
+
);
|
|
66
|
+
} else {
|
|
67
|
+
setSystemAuth(false);
|
|
68
|
+
setInputError(true);
|
|
69
|
+
toast.error(
|
|
70
|
+
(result.data && result.data.message) ||
|
|
71
|
+
'Could not find an account with this email address.'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
setIsLoading(false);
|
|
76
|
+
setInputError(true);
|
|
77
|
+
toast.error(String(error));
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className={styles.container}>
|
|
83
|
+
<div className={styles.loginBox}>
|
|
84
|
+
<img src={logo} alt="Client Portal" className={styles.logo} />
|
|
85
|
+
|
|
86
|
+
<h1 className={styles.title}>Client Login</h1>
|
|
87
|
+
<p className={styles.subtitle}>
|
|
88
|
+
Enter your email address to receive a verification code
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<form onSubmit={handleSubmit}>
|
|
92
|
+
<div className={styles.formGroup}>
|
|
93
|
+
<label htmlFor="email" className={styles.inputLabel}>
|
|
94
|
+
Email
|
|
95
|
+
</label>
|
|
96
|
+
<input
|
|
97
|
+
id="email"
|
|
98
|
+
type="email"
|
|
99
|
+
name="email"
|
|
100
|
+
value={email}
|
|
101
|
+
onChange={handleChange}
|
|
102
|
+
className={`${styles.input} ${
|
|
103
|
+
inputError ? styles.error : ''
|
|
104
|
+
}`}
|
|
105
|
+
placeholder="Enter your email address"
|
|
106
|
+
autoComplete="email"
|
|
107
|
+
/>
|
|
108
|
+
<span className={styles.inputIcon}>
|
|
109
|
+
<Person strokeWidth={2} size={18} />
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<button
|
|
114
|
+
className={styles.button}
|
|
115
|
+
type="submit"
|
|
116
|
+
disabled={isLoading}
|
|
117
|
+
>
|
|
118
|
+
{isLoading ? 'Sending...' : 'Continue'}
|
|
119
|
+
</button>
|
|
120
|
+
</form>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default ClientLogin;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import '../../styles/global.css';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { useNavigate, useLocation } from 'react-router-dom';
|
|
5
|
+
import { toast } from 'react-toastify';
|
|
6
|
+
import { LockOff, ArrowLeft } from 'akar-icons';
|
|
7
|
+
|
|
8
|
+
import CustomFetch from '../Fetch';
|
|
9
|
+
|
|
10
|
+
import styles from './styles/ClientAuth.module.scss';
|
|
11
|
+
|
|
12
|
+
const ClientOTPVerify = ({
|
|
13
|
+
logo,
|
|
14
|
+
setSystemAuth,
|
|
15
|
+
setUserProfile,
|
|
16
|
+
urls = {},
|
|
17
|
+
}) => {
|
|
18
|
+
const navigate = useNavigate();
|
|
19
|
+
const location = useLocation();
|
|
20
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
21
|
+
|
|
22
|
+
// Get data from location state
|
|
23
|
+
const email = location.state?.email || null;
|
|
24
|
+
const uuid = location.state?.uuid || null;
|
|
25
|
+
const previousUrl = location.state?.previousUrl || '';
|
|
26
|
+
const devOtp = location.state?.otp || null; // For development testing
|
|
27
|
+
|
|
28
|
+
// If no email or uuid, redirect to client login
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!email || !uuid) {
|
|
31
|
+
const loginUrl = '/client/login';
|
|
32
|
+
navigate(loginUrl);
|
|
33
|
+
}
|
|
34
|
+
}, [email, uuid, navigate]);
|
|
35
|
+
|
|
36
|
+
// Auto-fill OTP in development mode
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (process.env.NODE_ENV === 'development' && devOtp) {
|
|
39
|
+
setVerificationCode(devOtp);
|
|
40
|
+
}
|
|
41
|
+
}, [devOtp]);
|
|
42
|
+
|
|
43
|
+
const [verificationCode, setVerificationCode] = useState('');
|
|
44
|
+
const [codeClass, setCodeClass] = useState('');
|
|
45
|
+
const [resendDisabled, setResendDisabled] = useState(false);
|
|
46
|
+
const [countdown, setCountdown] = useState(0);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
let timer;
|
|
50
|
+
if (countdown > 0) {
|
|
51
|
+
timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
|
52
|
+
} else if (countdown === 0) {
|
|
53
|
+
setResendDisabled(false);
|
|
54
|
+
}
|
|
55
|
+
return () => clearTimeout(timer);
|
|
56
|
+
}, [countdown]);
|
|
57
|
+
|
|
58
|
+
const handleChange = (e) => {
|
|
59
|
+
if (e) {
|
|
60
|
+
const value = e.target.value;
|
|
61
|
+
// Only allow digits and limit to 6 characters
|
|
62
|
+
if (/^\d*$/.test(value) && value.length <= 6) {
|
|
63
|
+
setVerificationCode(value);
|
|
64
|
+
setCodeClass('');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleResendCode = async () => {
|
|
70
|
+
if (resendDisabled) return;
|
|
71
|
+
|
|
72
|
+
setResendDisabled(true);
|
|
73
|
+
setCountdown(60); // 60 seconds cooldown
|
|
74
|
+
|
|
75
|
+
toast.info('Resending verification code...', { autoClose: 2000 });
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const loginUrl = urls.login;
|
|
79
|
+
const result = await CustomFetch(loginUrl, 'POST', {
|
|
80
|
+
email: email,
|
|
81
|
+
resend: true,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (result.data && result.data.success) {
|
|
85
|
+
toast.success(
|
|
86
|
+
result.data.message ||
|
|
87
|
+
'Verification code resent to your email'
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Update UUID with the new one
|
|
91
|
+
if (result.data.uuid) {
|
|
92
|
+
navigate('/client/verify', {
|
|
93
|
+
state: {
|
|
94
|
+
email: email,
|
|
95
|
+
uuid: result.data.uuid,
|
|
96
|
+
previousUrl: previousUrl,
|
|
97
|
+
...(process.env.NODE_ENV === 'development' && {
|
|
98
|
+
otp: result.data.otp,
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
replace: true, // Replace current entry in history
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
toast.error(
|
|
106
|
+
(result.data && result.data.message) ||
|
|
107
|
+
'Could not resend verification code. Please try again later.'
|
|
108
|
+
);
|
|
109
|
+
setResendDisabled(false);
|
|
110
|
+
setCountdown(0);
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
toast.error(String(error));
|
|
114
|
+
setResendDisabled(false);
|
|
115
|
+
setCountdown(0);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleSubmit = async (e) => {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
|
|
122
|
+
if (!verificationCode.trim() || verificationCode.length !== 6) {
|
|
123
|
+
setCodeClass('inputError');
|
|
124
|
+
toast.error('Please enter a valid 6-digit code');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setIsLoading(true);
|
|
129
|
+
toast.info('Verifying code...', { autoClose: 2000 });
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const verifyUrl = urls.verify;
|
|
133
|
+
const result = await CustomFetch(verifyUrl, 'POST', {
|
|
134
|
+
email: email,
|
|
135
|
+
uuid: uuid,
|
|
136
|
+
otp: verificationCode,
|
|
137
|
+
previous_url: previousUrl,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
setIsLoading(false);
|
|
141
|
+
|
|
142
|
+
// Check if the result contains an error message
|
|
143
|
+
if (!result.data || !result.data.success) {
|
|
144
|
+
setCodeClass('inputError');
|
|
145
|
+
toast.error(
|
|
146
|
+
(result.data && result.data.message) ||
|
|
147
|
+
'Invalid verification code'
|
|
148
|
+
);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Successful verification
|
|
153
|
+
setUserProfile(result.data.user);
|
|
154
|
+
toast.success(result.data.message || 'Login successful!');
|
|
155
|
+
|
|
156
|
+
// Set authentication state first
|
|
157
|
+
setSystemAuth(true);
|
|
158
|
+
|
|
159
|
+
if (result.data.redirect_url && result.data.redirect_url !== '') {
|
|
160
|
+
window.location.href = result.data.redirect_url;
|
|
161
|
+
} else {
|
|
162
|
+
// Use replace: true to avoid back button issues
|
|
163
|
+
navigate('/client/portal', { replace: true });
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
setIsLoading(false);
|
|
167
|
+
setCodeClass('inputError');
|
|
168
|
+
toast.error(String(error));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div className={styles.container}>
|
|
174
|
+
<div className={styles.loginBox}>
|
|
175
|
+
<img src={logo} alt="Client Portal" className={styles.logo} />
|
|
176
|
+
|
|
177
|
+
<h1 className={styles.title}>Verification Code</h1>
|
|
178
|
+
<p className={styles.subtitle}>
|
|
179
|
+
Please enter the 6-digit code sent to your email address:{' '}
|
|
180
|
+
<span className={styles.emailHighlight}>{email}</span>
|
|
181
|
+
</p>
|
|
182
|
+
|
|
183
|
+
<form onSubmit={handleSubmit}>
|
|
184
|
+
<div className={styles.formGroup}>
|
|
185
|
+
<label
|
|
186
|
+
htmlFor="verificationCode"
|
|
187
|
+
className={styles.inputLabel}
|
|
188
|
+
>
|
|
189
|
+
Verification Code
|
|
190
|
+
</label>
|
|
191
|
+
<input
|
|
192
|
+
id="verificationCode"
|
|
193
|
+
type="text"
|
|
194
|
+
name="verificationCode"
|
|
195
|
+
value={verificationCode}
|
|
196
|
+
onChange={handleChange}
|
|
197
|
+
className={`${styles.input} ${styles.otpInput} ${
|
|
198
|
+
codeClass === 'inputError' ? styles.error : ''
|
|
199
|
+
}`}
|
|
200
|
+
placeholder="Enter 6-digit code"
|
|
201
|
+
autoComplete="one-time-code"
|
|
202
|
+
inputMode="numeric"
|
|
203
|
+
maxLength="6"
|
|
204
|
+
/>
|
|
205
|
+
<span className={styles.inputIcon}>
|
|
206
|
+
<LockOff strokeWidth={2} size={18} />
|
|
207
|
+
</span>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<button
|
|
211
|
+
className={styles.button}
|
|
212
|
+
type="submit"
|
|
213
|
+
disabled={isLoading}
|
|
214
|
+
>
|
|
215
|
+
{isLoading ? 'Verifying...' : 'Verify'}
|
|
216
|
+
</button>
|
|
217
|
+
|
|
218
|
+
<div className={styles.linkText}>
|
|
219
|
+
Didn't receive the code?{' '}
|
|
220
|
+
<a
|
|
221
|
+
href="#"
|
|
222
|
+
onClick={(e) => {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
handleResendCode();
|
|
225
|
+
}}
|
|
226
|
+
className={
|
|
227
|
+
resendDisabled
|
|
228
|
+
? styles.disabled
|
|
229
|
+
: styles.resendLink
|
|
230
|
+
}
|
|
231
|
+
>
|
|
232
|
+
{resendDisabled
|
|
233
|
+
? `Resend in ${countdown}s`
|
|
234
|
+
: 'Resend Code'}
|
|
235
|
+
</a>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div className={styles.linkText}>
|
|
239
|
+
<a
|
|
240
|
+
href="#"
|
|
241
|
+
onClick={(e) => {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
navigate('/client/login');
|
|
244
|
+
}}
|
|
245
|
+
className={styles.backLink}
|
|
246
|
+
>
|
|
247
|
+
<ArrowLeft strokeWidth={2} size={16} /> Back to
|
|
248
|
+
Login
|
|
249
|
+
</a>
|
|
250
|
+
</div>
|
|
251
|
+
</form>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export default ClientOTPVerify;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Client Auth styles
|
|
2
|
+
.container {
|
|
3
|
+
min-height: 100vh;
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
justify-content: center;
|
|
7
|
+
background-color: #f5f5f5;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.loginBox {
|
|
11
|
+
width: 100%;
|
|
12
|
+
max-width: 500px;
|
|
13
|
+
padding: 2.5rem;
|
|
14
|
+
margin: 0 auto;
|
|
15
|
+
background-color: white;
|
|
16
|
+
border-radius: 8px;
|
|
17
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.logo {
|
|
21
|
+
display: block;
|
|
22
|
+
width: 100%;
|
|
23
|
+
max-width: 240px;
|
|
24
|
+
height: auto;
|
|
25
|
+
margin: 0 auto 2rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.title {
|
|
29
|
+
font-size: 1.75rem;
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
color: #333;
|
|
32
|
+
margin-bottom: 0.5rem;
|
|
33
|
+
text-align: center;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.subtitle {
|
|
37
|
+
font-size: 1rem;
|
|
38
|
+
color: #666;
|
|
39
|
+
margin-bottom: 2rem;
|
|
40
|
+
text-align: center;
|
|
41
|
+
line-height: 1.5;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.formGroup {
|
|
45
|
+
margin-bottom: 1.5rem;
|
|
46
|
+
position: relative;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.inputLabel {
|
|
50
|
+
display: block;
|
|
51
|
+
font-size: 0.9rem;
|
|
52
|
+
font-weight: 500;
|
|
53
|
+
color: #555;
|
|
54
|
+
margin-bottom: 0.5rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.input {
|
|
58
|
+
width: 100%;
|
|
59
|
+
padding: 0.75rem 1rem;
|
|
60
|
+
font-size: 1rem;
|
|
61
|
+
border: 1px solid #ddd;
|
|
62
|
+
border-radius: 4px;
|
|
63
|
+
background-color: white;
|
|
64
|
+
transition: border-color 0.2s ease;
|
|
65
|
+
|
|
66
|
+
&:focus {
|
|
67
|
+
outline: none;
|
|
68
|
+
border-color: var(--primary-color, #4a6741);
|
|
69
|
+
box-shadow: 0 0 0 2px rgba(74, 103, 65, 0.1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&.error {
|
|
73
|
+
border-color: #e53935;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.inputIcon {
|
|
78
|
+
position: absolute;
|
|
79
|
+
right: 1rem;
|
|
80
|
+
top: 2.4rem;
|
|
81
|
+
color: #999;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.button {
|
|
85
|
+
width: 100%;
|
|
86
|
+
padding: 0.9rem 1.5rem;
|
|
87
|
+
font-size: 1rem;
|
|
88
|
+
font-weight: 500;
|
|
89
|
+
color: white;
|
|
90
|
+
background-color: var(--primary-color, #0f4229);
|
|
91
|
+
border: none;
|
|
92
|
+
border-radius: 4px;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
transition: background-color 0.2s ease;
|
|
95
|
+
|
|
96
|
+
&:hover {
|
|
97
|
+
background-color: var(--secondary-color, #0a2e1c);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
&:disabled {
|
|
101
|
+
background-color: #ccc;
|
|
102
|
+
cursor: not-allowed;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.linkText {
|
|
107
|
+
display: block;
|
|
108
|
+
text-align: center;
|
|
109
|
+
margin-top: 1.5rem;
|
|
110
|
+
font-size: 0.9rem;
|
|
111
|
+
color: #666;
|
|
112
|
+
|
|
113
|
+
a {
|
|
114
|
+
color: var(--primary-color, #0f4229);
|
|
115
|
+
text-decoration: none;
|
|
116
|
+
font-weight: 500;
|
|
117
|
+
|
|
118
|
+
&:hover {
|
|
119
|
+
text-decoration: underline;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// OTP verification specific styles
|
|
125
|
+
.otpInput {
|
|
126
|
+
letter-spacing: 0.25rem;
|
|
127
|
+
font-size: 1.2rem;
|
|
128
|
+
text-align: center;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.resendLink {
|
|
132
|
+
color: var(--primary-color, #0f4229);
|
|
133
|
+
text-decoration: none;
|
|
134
|
+
font-weight: 500;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
|
|
137
|
+
&:hover {
|
|
138
|
+
text-decoration: underline;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
&.disabled {
|
|
142
|
+
color: #999;
|
|
143
|
+
cursor: default;
|
|
144
|
+
text-decoration: none;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.emailHighlight {
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.backLink {
|
|
153
|
+
display: inline-flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
margin-top: 1rem;
|
|
156
|
+
color: #666;
|
|
157
|
+
text-decoration: none;
|
|
158
|
+
font-size: 0.9rem;
|
|
159
|
+
|
|
160
|
+
svg {
|
|
161
|
+
margin-right: 0.5rem;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
&:hover {
|
|
165
|
+
color: var(--primary-color, #0f4229);
|
|
166
|
+
}
|
|
167
|
+
}
|