nebula-starter-kit 0.0.1
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 +107 -0
- package/dist/index.js +8 -0
- package/dist/run.js +112 -0
- package/dist/utils/addService.js +204 -0
- package/dist/utils/appName.js +75 -0
- package/dist/utils/deployNow.js +18 -0
- package/dist/utils/generateFiles.js +326 -0
- package/dist/utils/generateSchemas.js +58 -0
- package/dist/utils/listServices.js +24 -0
- package/dist/utils/replaceServiceNames.js +42 -0
- package/dist/utils/telemetryAddon.js +91 -0
- package/package.json +31 -0
- package/templates/core/audit/audit.controller.ts +125 -0
- package/templates/core/audit/audit.module.ts +10 -0
- package/templates/core/audit/audit.schema.ts +14 -0
- package/templates/core/audit/audit.service.ts +47 -0
- package/templates/core/auth/auth.controller.ts +207 -0
- package/templates/core/auth/auth.dto.ts +50 -0
- package/templates/core/auth/auth.module.ts +10 -0
- package/templates/core/auth/auth.service.ts +178 -0
- package/templates/core/auth/utils.ts +160 -0
- package/templates/core/constants/environment.db.ts +27 -0
- package/templates/core/constants/environment.module.ts +9 -0
- package/templates/core/constants/environment.service.ts +69 -0
- package/templates/core/core.controller.ts +35 -0
- package/templates/core/core.module.ts +22 -0
- package/templates/core/database/database.module.ts +20 -0
- package/templates/core/database/database.provider.ts +32 -0
- package/templates/core/database/database.service.ts +168 -0
- package/templates/core/database/database.types.ts +13 -0
- package/templates/core/filters/audit.decorator.ts +5 -0
- package/templates/core/filters/audit.interceptor.ts +74 -0
- package/templates/core/filters/http-exception.filter.ts +43 -0
- package/templates/core/filters/success-message.decorator.ts +5 -0
- package/templates/core/filters/success-response.interceptor.ts +35 -0
- package/templates/core/summarize/summarize.controller.ts +74 -0
- package/templates/core/summarize/summarize.dto.ts +13 -0
- package/templates/core/summarize/summarize.module.ts +9 -0
- package/templates/core/summarize/summarize.service.ts +54 -0
- package/templates/nest-cli.json +8 -0
- package/templates/package.json +52 -0
- package/templates/service/src/__name__.controller.ts +15 -0
- package/templates/service/src/__name__.module.ts +11 -0
- package/templates/service/src/__name__.schema.ts +15 -0
- package/templates/service/src/__name__.service.ts +12 -0
- package/templates/service/src/lambda.ts +60 -0
- package/templates/tsconfig.json +28 -0
- package/templates/ui/README.md +36 -0
- package/templates/ui/eslint.config.mjs +18 -0
- package/templates/ui/next.config.ts +8 -0
- package/templates/ui/package.json +33 -0
- package/templates/ui/postcss.config.mjs +7 -0
- package/templates/ui/public/file.svg +1 -0
- package/templates/ui/public/globe.svg +1 -0
- package/templates/ui/public/next.svg +1 -0
- package/templates/ui/public/vercel.svg +1 -0
- package/templates/ui/public/window.svg +1 -0
- package/templates/ui/src/app/LandingPage.tsx +98 -0
- package/templates/ui/src/app/ai/summarize/page.tsx +115 -0
- package/templates/ui/src/app/context/AuthContext.tsx +48 -0
- package/templates/ui/src/app/favicon.ico +0 -0
- package/templates/ui/src/app/globals.css +26 -0
- package/templates/ui/src/app/layout.tsx +37 -0
- package/templates/ui/src/app/page.tsx +7 -0
- package/templates/ui/src/app/services/page.tsx +99 -0
- package/templates/ui/src/components/Auth.css +252 -0
- package/templates/ui/src/components/Auth.tsx +455 -0
- package/templates/ui/src/components/Error.tsx +32 -0
- package/templates/ui/src/components/FormInput.tsx +77 -0
- package/templates/ui/src/components/Loading.tsx +10 -0
- package/templates/ui/src/components/Login.tsx +171 -0
- package/templates/ui/src/components/Popup.css +90 -0
- package/templates/ui/src/components/Signup.tsx +155 -0
- package/templates/ui/src/utils/axiosInstance.ts +37 -0
- package/templates/ui/src/utils/axiosRawInstance.ts +33 -0
- package/templates/ui/src/utils/util.constant.ts +0 -0
- package/templates/ui/src/utils/util.function.ts +165 -0
- package/templates/ui/src/utils/util.type.ts +64 -0
- package/templates/ui/src/utils/variables.ts +6 -0
- package/templates/ui/tailwind.config.js +8 -0
- package/templates/ui/tsconfig.json +43 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { AxiosResponse } from 'axios';
|
|
4
|
+
|
|
5
|
+
import axiosInstance from '../utils/axiosInstance';
|
|
6
|
+
import { validate } from '../utils/util.function';
|
|
7
|
+
import {
|
|
8
|
+
type Step,
|
|
9
|
+
type Values,
|
|
10
|
+
// decrypt,
|
|
11
|
+
} from '../utils/util.type';
|
|
12
|
+
import ErrorMessage from './Error';
|
|
13
|
+
import FormInput from './FormInput';
|
|
14
|
+
import { appServices } from '@/utils/variables';
|
|
15
|
+
import { useAuth } from '@/app/context/AuthContext';
|
|
16
|
+
|
|
17
|
+
type LoginProps = {
|
|
18
|
+
onShowSignup: () => void;
|
|
19
|
+
onSetLoggedIn: () => void;
|
|
20
|
+
onSetStep: () => void;
|
|
21
|
+
onFetchRecords: () => void;
|
|
22
|
+
onForgotPassword: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const Login: React.FC<LoginProps> = ({
|
|
26
|
+
onShowSignup,
|
|
27
|
+
onSetLoggedIn,
|
|
28
|
+
onSetStep,
|
|
29
|
+
onFetchRecords,
|
|
30
|
+
onForgotPassword
|
|
31
|
+
}) => {
|
|
32
|
+
const { login, isLoggedIn, logout } = useAuth();
|
|
33
|
+
|
|
34
|
+
const [values, setValues] = useState<Values>({
|
|
35
|
+
email: '',
|
|
36
|
+
password: '',
|
|
37
|
+
name: '',
|
|
38
|
+
confirmationCode: '',
|
|
39
|
+
score: '',
|
|
40
|
+
});
|
|
41
|
+
const [errors, setErrors] = useState<Partial<Values>>({});
|
|
42
|
+
const [touched, setTouched] = useState<
|
|
43
|
+
Partial<Record<keyof Values, boolean>>
|
|
44
|
+
>({});
|
|
45
|
+
const [loginLoading, setLoginLoading] = useState<boolean>(false);
|
|
46
|
+
const { email, password } = values;
|
|
47
|
+
|
|
48
|
+
const handleChange = (step: Step, field: keyof Values, value: string) => {
|
|
49
|
+
setValues((prev) => ({ ...prev, [field]: value }));
|
|
50
|
+
setTouched((prev) => ({ ...prev, [field]: true }));
|
|
51
|
+
const newErrors = validate(
|
|
52
|
+
step as any,
|
|
53
|
+
{ ...values, [field]: value },
|
|
54
|
+
field,
|
|
55
|
+
value,
|
|
56
|
+
);
|
|
57
|
+
// console.log('handleChange', step, field, value, newErrors);
|
|
58
|
+
setErrors(newErrors);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleLogin = async () => {
|
|
62
|
+
try {
|
|
63
|
+
setErrors({});
|
|
64
|
+
// console.log('logging in');
|
|
65
|
+
const newErrors = validate('login', values);
|
|
66
|
+
// mark all fields as touched
|
|
67
|
+
setTouched(
|
|
68
|
+
Object.keys(values).reduce((acc, key) => ({ ...acc, [key]: true }), {}),
|
|
69
|
+
);
|
|
70
|
+
if (Object.keys(newErrors).length > 0) {
|
|
71
|
+
setErrors(newErrors);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setLoginLoading(true);
|
|
75
|
+
const loginResponse: AxiosResponse = await axiosInstance.post(
|
|
76
|
+
`${appServices}/auth/login`,
|
|
77
|
+
{
|
|
78
|
+
email,
|
|
79
|
+
password,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
const userData = loginResponse.data.data;
|
|
88
|
+
console.log('encrypted', loginResponse.data.data && 'valid');
|
|
89
|
+
console.log('decrypted', userData && 'valid');
|
|
90
|
+
// ✅ Persist session
|
|
91
|
+
localStorage.setItem('email', userData.email);
|
|
92
|
+
localStorage.setItem('token', userData.token || '');
|
|
93
|
+
|
|
94
|
+
// ✅ Update global auth state
|
|
95
|
+
login(); // from useAuth()
|
|
96
|
+
onSetLoggedIn();
|
|
97
|
+
onSetStep();
|
|
98
|
+
onFetchRecords();
|
|
99
|
+
} catch (err: any) {
|
|
100
|
+
console.log(err.message);
|
|
101
|
+
setLoginLoading(false);
|
|
102
|
+
setErrors((prev) => ({
|
|
103
|
+
...prev,
|
|
104
|
+
loginError: err.message,
|
|
105
|
+
}));
|
|
106
|
+
} finally {
|
|
107
|
+
setLoginLoading(false);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div>
|
|
113
|
+
<h2 data-testid="login-header"></h2>
|
|
114
|
+
|
|
115
|
+
<FormInput
|
|
116
|
+
label='Email Address'
|
|
117
|
+
name="email"
|
|
118
|
+
type="text"
|
|
119
|
+
placeholder="email"
|
|
120
|
+
value={values.email}
|
|
121
|
+
onChange={(e) => handleChange('login', 'email', e.target.value)}
|
|
122
|
+
errors={errors}
|
|
123
|
+
touched={touched}
|
|
124
|
+
/>
|
|
125
|
+
<FormInput
|
|
126
|
+
label='Password'
|
|
127
|
+
name="password"
|
|
128
|
+
type="password"
|
|
129
|
+
placeholder="Password"
|
|
130
|
+
value={values.password}
|
|
131
|
+
onChange={(e) => handleChange('login', 'password', e.target.value)}
|
|
132
|
+
errors={errors}
|
|
133
|
+
touched={touched}
|
|
134
|
+
allowTogglePassword
|
|
135
|
+
/>
|
|
136
|
+
|
|
137
|
+
<p
|
|
138
|
+
className="text-md text-blue-500 cursor-pointer text-right mb-2"
|
|
139
|
+
onClick={() => onForgotPassword()}
|
|
140
|
+
>
|
|
141
|
+
Forgot Password?
|
|
142
|
+
</p>
|
|
143
|
+
|
|
144
|
+
{loginLoading ? (
|
|
145
|
+
<div className="spinner">
|
|
146
|
+
<div></div>
|
|
147
|
+
</div>
|
|
148
|
+
) : (
|
|
149
|
+
<button
|
|
150
|
+
data-testid="submit-login-button"
|
|
151
|
+
className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition cursor-pointer"
|
|
152
|
+
onClick={handleLogin}
|
|
153
|
+
>
|
|
154
|
+
Login
|
|
155
|
+
</button>
|
|
156
|
+
)}
|
|
157
|
+
<div className="mt-14">
|
|
158
|
+
Not registered?{' '}
|
|
159
|
+
<span className="click-link" onClick={onShowSignup}>
|
|
160
|
+
SignUp
|
|
161
|
+
</span>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div>
|
|
165
|
+
<ErrorMessage id="login-error" touched message={errors.loginError} />
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export default Login;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
.popup {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
background: rgba(0, 0, 0, 0.5);
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
align-items: center;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.popup-content {
|
|
14
|
+
background: white;
|
|
15
|
+
padding: 2rem;
|
|
16
|
+
border-radius: 12px;
|
|
17
|
+
text-align: center;
|
|
18
|
+
max-width: 400px;
|
|
19
|
+
width: 90%;
|
|
20
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.popup-content h3 {
|
|
24
|
+
margin-bottom: 1rem;
|
|
25
|
+
color: green;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.popup-content button {
|
|
29
|
+
margin-top: 1rem;
|
|
30
|
+
padding: 0.5rem 1rem;
|
|
31
|
+
border: none;
|
|
32
|
+
background: #007bff;
|
|
33
|
+
color: white;
|
|
34
|
+
border-radius: 8px;
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.popup-content .delete {
|
|
39
|
+
margin-top: 1rem;
|
|
40
|
+
padding: 0.5rem 1rem;
|
|
41
|
+
border: none;
|
|
42
|
+
background: #dc3545;
|
|
43
|
+
color: white;
|
|
44
|
+
border-radius: 8px;
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.popup-content .cancel {
|
|
49
|
+
margin-top: 1rem;
|
|
50
|
+
padding: 0.5rem 1rem;
|
|
51
|
+
border: none;
|
|
52
|
+
background: #ddd;
|
|
53
|
+
color: #333;
|
|
54
|
+
border-radius: 8px;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
margin-right: 20px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.popup-content .cancel:hover {
|
|
60
|
+
background: #ddd;
|
|
61
|
+
color: #000;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.popup-content button:hover {
|
|
65
|
+
background: #0056b3;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
button[disabled] {
|
|
69
|
+
opacity: 0.6;
|
|
70
|
+
cursor: not-allowed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.spinner {
|
|
74
|
+
display: inline-block;
|
|
75
|
+
width: 24px;
|
|
76
|
+
height: 24px;
|
|
77
|
+
border: 3px solid #f3f3f3;
|
|
78
|
+
border-top: 3px solid #007bff;
|
|
79
|
+
border-radius: 50%;
|
|
80
|
+
animation: spin 0.8s linear infinite;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@keyframes spin {
|
|
84
|
+
0% {
|
|
85
|
+
transform: rotate(0deg);
|
|
86
|
+
}
|
|
87
|
+
100% {
|
|
88
|
+
transform: rotate(360deg);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import axiosInstance from '../utils/axiosInstance';
|
|
4
|
+
import { validate } from '../utils/util.function';
|
|
5
|
+
import type { Step, Values } from '../utils/util.type';
|
|
6
|
+
import ErrorMessage from './Error';
|
|
7
|
+
import FormInput from './FormInput';
|
|
8
|
+
import { appServices } from '@/utils/variables';
|
|
9
|
+
|
|
10
|
+
type SignupProps = {
|
|
11
|
+
onShowLogin: () => void;
|
|
12
|
+
onSetSignupSuccess: () => void;
|
|
13
|
+
onSetStep: () => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const Signup: React.FC<SignupProps> = ({
|
|
17
|
+
onShowLogin,
|
|
18
|
+
onSetSignupSuccess,
|
|
19
|
+
onSetStep,
|
|
20
|
+
}) => {
|
|
21
|
+
const [values, setValues] = useState<Values>({
|
|
22
|
+
email: '',
|
|
23
|
+
password: '',
|
|
24
|
+
name: '',
|
|
25
|
+
confirmationCode: '',
|
|
26
|
+
});
|
|
27
|
+
const [errors, setErrors] = useState<Partial<Values>>({});
|
|
28
|
+
const [touched, setTouched] = useState<
|
|
29
|
+
Partial<Record<keyof Values, boolean>>
|
|
30
|
+
>({});
|
|
31
|
+
const [signupLoading, setSignupLoading] = useState<boolean>(false);
|
|
32
|
+
const { email, name, password } = values;
|
|
33
|
+
|
|
34
|
+
const handleChange = (step: Step, field: keyof Values, value: string) => {
|
|
35
|
+
setValues((prev) => ({ ...prev, [field]: value }));
|
|
36
|
+
setTouched((prev) => ({ ...prev, [field]: true }));
|
|
37
|
+
const newErrors = validate(
|
|
38
|
+
step as any,
|
|
39
|
+
{ ...values, [field]: value },
|
|
40
|
+
field,
|
|
41
|
+
value,
|
|
42
|
+
);
|
|
43
|
+
// console.log('handleChange', step, field, value, newErrors);
|
|
44
|
+
setErrors(newErrors);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleSignup = async () => {
|
|
48
|
+
try {
|
|
49
|
+
setErrors({});
|
|
50
|
+
// console.log('signing up');
|
|
51
|
+
const newErrors = validate('signup', values);
|
|
52
|
+
// mark all fields as touched
|
|
53
|
+
setTouched(
|
|
54
|
+
Object.keys(values).reduce((acc, key) => ({ ...acc, [key]: true }), {}),
|
|
55
|
+
);
|
|
56
|
+
if (Object.keys(newErrors).length > 0) {
|
|
57
|
+
setErrors(newErrors);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
setSignupLoading(true);
|
|
61
|
+
localStorage.setItem('email', JSON.stringify(email));
|
|
62
|
+
|
|
63
|
+
await axiosInstance.post(
|
|
64
|
+
`${appServices}/auth/register`,
|
|
65
|
+
{
|
|
66
|
+
email,
|
|
67
|
+
name,
|
|
68
|
+
password,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
onSetSignupSuccess();
|
|
78
|
+
onSetStep();
|
|
79
|
+
} catch (err: any) {
|
|
80
|
+
console.log(err.message);
|
|
81
|
+
setSignupLoading(false);
|
|
82
|
+
setErrors((prev) => ({
|
|
83
|
+
...prev,
|
|
84
|
+
signupError: err.message,
|
|
85
|
+
}));
|
|
86
|
+
} finally {
|
|
87
|
+
setSignupLoading(false);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div>
|
|
93
|
+
<h2 data-testid="signup-header"></h2>
|
|
94
|
+
<FormInput
|
|
95
|
+
label='Email Address'
|
|
96
|
+
name="email"
|
|
97
|
+
type="text"
|
|
98
|
+
placeholder="Email"
|
|
99
|
+
value={values.email}
|
|
100
|
+
onChange={(e) => handleChange('signup', 'email', e.target.value)}
|
|
101
|
+
errors={errors}
|
|
102
|
+
touched={touched}
|
|
103
|
+
/>
|
|
104
|
+
<FormInput
|
|
105
|
+
label='Name'
|
|
106
|
+
name="name"
|
|
107
|
+
type="text"
|
|
108
|
+
placeholder="Name"
|
|
109
|
+
value={values.name}
|
|
110
|
+
onChange={(e) => handleChange('signup', 'name', e.target.value)}
|
|
111
|
+
errors={errors}
|
|
112
|
+
touched={touched}
|
|
113
|
+
/>
|
|
114
|
+
<FormInput
|
|
115
|
+
label='Password'
|
|
116
|
+
name="password"
|
|
117
|
+
type="password"
|
|
118
|
+
placeholder="Password"
|
|
119
|
+
value={values.password}
|
|
120
|
+
onChange={(e) => handleChange('signup', 'password', e.target.value)}
|
|
121
|
+
errors={errors}
|
|
122
|
+
touched={touched}
|
|
123
|
+
allowTogglePassword
|
|
124
|
+
/>
|
|
125
|
+
<div className="mb-4">
|
|
126
|
+
{signupLoading ? (
|
|
127
|
+
<div className="spinner">
|
|
128
|
+
<div></div>
|
|
129
|
+
</div>
|
|
130
|
+
) : (
|
|
131
|
+
// <>
|
|
132
|
+
<button
|
|
133
|
+
data-testid="submit-signup-button"
|
|
134
|
+
className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition cursor-pointer"
|
|
135
|
+
onClick={handleSignup}
|
|
136
|
+
>
|
|
137
|
+
Sign Up
|
|
138
|
+
</button>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
{/* <div className="mt-14"> */}
|
|
142
|
+
Already registered?{' '}
|
|
143
|
+
<span className="click-link" onClick={onShowLogin}>
|
|
144
|
+
Login
|
|
145
|
+
</span>
|
|
146
|
+
{/* </div> */}
|
|
147
|
+
|
|
148
|
+
<div>
|
|
149
|
+
<ErrorMessage id="signup-error" touched message={errors.signupError} />
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export default Signup;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import axios, { AxiosError } from 'axios';
|
|
2
|
+
import { apiUrl } from './variables';
|
|
3
|
+
|
|
4
|
+
const baseUrl = apiUrl || 'http://localhost:3001/';
|
|
5
|
+
console.log('baseurl:', baseUrl);
|
|
6
|
+
const axiosInstance = axios.create({
|
|
7
|
+
baseURL: baseUrl,
|
|
8
|
+
headers: {
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
axiosInstance.interceptors.response.use(
|
|
14
|
+
(response) => response,
|
|
15
|
+
(error: AxiosError<{ message?: string; details?: string }>) => {
|
|
16
|
+
let msg = 'Something went wrong';
|
|
17
|
+
|
|
18
|
+
if (error.response) {
|
|
19
|
+
msg =
|
|
20
|
+
error.response.data?.details ??
|
|
21
|
+
error.response.data?.message ??
|
|
22
|
+
error.message;
|
|
23
|
+
|
|
24
|
+
console.log('API Error:', msg, error.response);
|
|
25
|
+
} else if (error.request) {
|
|
26
|
+
msg = 'No response from server. Please try again.';
|
|
27
|
+
console.log('No response:', error.request);
|
|
28
|
+
} else {
|
|
29
|
+
msg = error.message || msg;
|
|
30
|
+
console.log('Unexpected:', msg);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return Promise.reject({ message: msg, original: error });
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export default axiosInstance;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import axios, { AxiosError } from 'axios';
|
|
2
|
+
|
|
3
|
+
const axiosRawInstance = axios.create({
|
|
4
|
+
headers: {
|
|
5
|
+
'Content-Type': 'application/json',
|
|
6
|
+
},
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
axiosRawInstance.interceptors.response.use(
|
|
10
|
+
(response) => response,
|
|
11
|
+
(error: AxiosError<{ message?: string; details?: string }>) => {
|
|
12
|
+
let msg = 'Something went wrong';
|
|
13
|
+
|
|
14
|
+
if (error.response) {
|
|
15
|
+
msg =
|
|
16
|
+
error.response.data?.details ??
|
|
17
|
+
error.response.data?.message ??
|
|
18
|
+
error.message;
|
|
19
|
+
|
|
20
|
+
console.error('API Error:', msg, error.response);
|
|
21
|
+
} else if (error.request) {
|
|
22
|
+
msg = 'No response from server. Please try again.';
|
|
23
|
+
console.error('No response:', error.request);
|
|
24
|
+
} else {
|
|
25
|
+
msg = error.message || msg;
|
|
26
|
+
console.error('Unexpected:', msg);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Promise.reject({ message: msg, original: error });
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export default axiosRawInstance;
|
|
File without changes
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { format, formatDistanceToNow, isValid } from 'date-fns';
|
|
2
|
+
import { Mode, Values } from './util.type';
|
|
3
|
+
|
|
4
|
+
export const phoneRegex = /^(?:\+234|0)[7-9][0-1]\d{8}$/;
|
|
5
|
+
export const emailRegex = /\S+@\S+\.\S+/;
|
|
6
|
+
export const passwordRegex =
|
|
7
|
+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
|
|
8
|
+
|
|
9
|
+
export type FormValues = {
|
|
10
|
+
email?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
password?: string;
|
|
13
|
+
confirmationCode?: string;
|
|
14
|
+
phone?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const passwordRules = [
|
|
18
|
+
{
|
|
19
|
+
test: (v: string) => /[A-Z]/.test(v),
|
|
20
|
+
message: 'Password must include an uppercase letter',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
test: (v: string) => /[a-z]/.test(v),
|
|
24
|
+
message: 'Password must include a lowercase letter',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
test: (v: string) => /\d/.test(v),
|
|
28
|
+
message: 'Password must include a number',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
test: (v: string) => /[^A-Za-z0-9]/.test(v),
|
|
32
|
+
message: 'Password must include a special character',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
test: (v: string) => v.length >= 8,
|
|
36
|
+
message: 'Password must be at least 8 characters',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function validatePassword(value: string): string | null {
|
|
41
|
+
if (!value) return 'Password is required';
|
|
42
|
+
|
|
43
|
+
for (const rule of passwordRules) {
|
|
44
|
+
if (!rule.test(value)) {
|
|
45
|
+
return rule.message; // ⬅️ stops at first unmet requirement
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null; // ✅ all requirements met
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const validate = (
|
|
53
|
+
type: Mode,
|
|
54
|
+
values: any,
|
|
55
|
+
field?: string,
|
|
56
|
+
currentValue?: string,
|
|
57
|
+
) => {
|
|
58
|
+
const {
|
|
59
|
+
email,
|
|
60
|
+
password,
|
|
61
|
+
confirmPassword,
|
|
62
|
+
name,
|
|
63
|
+
newPassword,
|
|
64
|
+
lastName,
|
|
65
|
+
phone,
|
|
66
|
+
} = values;
|
|
67
|
+
const newErrors: Record<string, string> = {};
|
|
68
|
+
const isEmpty = (value?: string) => !value || !value.trim();
|
|
69
|
+
|
|
70
|
+
// reset only the field being validated (so old error messages disappear if fixed)
|
|
71
|
+
if (field) delete newErrors[field];
|
|
72
|
+
|
|
73
|
+
// Dynamic lookup: use currentValue if provided, otherwise use state
|
|
74
|
+
const getValue = (name: string, fallback: string | undefined) =>
|
|
75
|
+
field === name && currentValue !== undefined ? currentValue : fallback;
|
|
76
|
+
|
|
77
|
+
if (type === 'login') {
|
|
78
|
+
const emailVal = getValue('email', email) ?? '';
|
|
79
|
+
if ((!field || field === 'email') && isEmpty(emailVal)) {
|
|
80
|
+
newErrors.email = 'Email is required';
|
|
81
|
+
} else if ((!field || field === 'email') && !emailRegex.test(emailVal)) {
|
|
82
|
+
newErrors.email = 'Enter a valid email';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const passwordVal = getValue('password', password) ?? '';
|
|
86
|
+
if (!field || field === 'password') {
|
|
87
|
+
const error = validatePassword(passwordVal);
|
|
88
|
+
if (error) {
|
|
89
|
+
newErrors.password = error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (type === 'forgotPassword') {
|
|
95
|
+
const emailVal = getValue('email', email) ?? '';
|
|
96
|
+
if ((!field || field === 'email') && isEmpty(emailVal)) {
|
|
97
|
+
newErrors.email = 'Email is required';
|
|
98
|
+
} else if ((!field || field === 'email') && !emailRegex.test(emailVal)) {
|
|
99
|
+
newErrors.email = 'Enter a valid email';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (type === 'signup') {
|
|
104
|
+
const emailVal = getValue('email', email) ?? '';
|
|
105
|
+
if ((!field || field === 'email') && isEmpty(emailVal)) {
|
|
106
|
+
newErrors.email = 'Email is required';
|
|
107
|
+
} else if ((!field || field === 'email') && !emailRegex.test(emailVal)) {
|
|
108
|
+
newErrors.email = 'Enter a valid email';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const nameVal = getValue('name', name);
|
|
112
|
+
if ((!field || field === 'name') && isEmpty(nameVal))
|
|
113
|
+
newErrors.name = 'Name is required';
|
|
114
|
+
|
|
115
|
+
const passwordVal = getValue('password', password) ?? '';
|
|
116
|
+
if (!field || field === 'password') {
|
|
117
|
+
const error = validatePassword(passwordVal);
|
|
118
|
+
if (error) {
|
|
119
|
+
newErrors.password = error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (type === 'resetPassword') {
|
|
125
|
+
const newPasswordVal = getValue('newPassword', newPassword) ?? '';
|
|
126
|
+
if (!field || field === 'password') {
|
|
127
|
+
const error = validatePassword(newPasswordVal);
|
|
128
|
+
if (error) {
|
|
129
|
+
newErrors.newPassword = error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const confirmPasswordVal =
|
|
134
|
+
getValue('confirmPassword', confirmPassword) ?? '';
|
|
135
|
+
if (
|
|
136
|
+
(!field || field === 'confirmPassword') &&
|
|
137
|
+
isEmpty(confirmPasswordVal)
|
|
138
|
+
) {
|
|
139
|
+
newErrors.confirmPassword = 'Confirm Password is required';
|
|
140
|
+
} else if (
|
|
141
|
+
(!field || field === 'confirmPassword') &&
|
|
142
|
+
confirmPasswordVal !== newPasswordVal
|
|
143
|
+
) {
|
|
144
|
+
newErrors.confirmPassword = 'Passwords do not match';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return newErrors;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const numberRegex = /^(0|[1-9][0-9]*)$/;
|
|
151
|
+
|
|
152
|
+
export const timeAgo = (
|
|
153
|
+
dateInput: string | Date,
|
|
154
|
+
type: 'full' | 'relative',
|
|
155
|
+
) => {
|
|
156
|
+
const date = new Date(dateInput);
|
|
157
|
+
|
|
158
|
+
if (!isValid(date)) return 'Invalid date';
|
|
159
|
+
|
|
160
|
+
if (type === 'full') {
|
|
161
|
+
return format(date, 'dd MMM yyyy, HH:mm');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return formatDistanceToNow(date, { addSuffix: true });
|
|
165
|
+
};
|