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.
Files changed (81) hide show
  1. package/README.md +107 -0
  2. package/dist/index.js +8 -0
  3. package/dist/run.js +112 -0
  4. package/dist/utils/addService.js +204 -0
  5. package/dist/utils/appName.js +75 -0
  6. package/dist/utils/deployNow.js +18 -0
  7. package/dist/utils/generateFiles.js +326 -0
  8. package/dist/utils/generateSchemas.js +58 -0
  9. package/dist/utils/listServices.js +24 -0
  10. package/dist/utils/replaceServiceNames.js +42 -0
  11. package/dist/utils/telemetryAddon.js +91 -0
  12. package/package.json +31 -0
  13. package/templates/core/audit/audit.controller.ts +125 -0
  14. package/templates/core/audit/audit.module.ts +10 -0
  15. package/templates/core/audit/audit.schema.ts +14 -0
  16. package/templates/core/audit/audit.service.ts +47 -0
  17. package/templates/core/auth/auth.controller.ts +207 -0
  18. package/templates/core/auth/auth.dto.ts +50 -0
  19. package/templates/core/auth/auth.module.ts +10 -0
  20. package/templates/core/auth/auth.service.ts +178 -0
  21. package/templates/core/auth/utils.ts +160 -0
  22. package/templates/core/constants/environment.db.ts +27 -0
  23. package/templates/core/constants/environment.module.ts +9 -0
  24. package/templates/core/constants/environment.service.ts +69 -0
  25. package/templates/core/core.controller.ts +35 -0
  26. package/templates/core/core.module.ts +22 -0
  27. package/templates/core/database/database.module.ts +20 -0
  28. package/templates/core/database/database.provider.ts +32 -0
  29. package/templates/core/database/database.service.ts +168 -0
  30. package/templates/core/database/database.types.ts +13 -0
  31. package/templates/core/filters/audit.decorator.ts +5 -0
  32. package/templates/core/filters/audit.interceptor.ts +74 -0
  33. package/templates/core/filters/http-exception.filter.ts +43 -0
  34. package/templates/core/filters/success-message.decorator.ts +5 -0
  35. package/templates/core/filters/success-response.interceptor.ts +35 -0
  36. package/templates/core/summarize/summarize.controller.ts +74 -0
  37. package/templates/core/summarize/summarize.dto.ts +13 -0
  38. package/templates/core/summarize/summarize.module.ts +9 -0
  39. package/templates/core/summarize/summarize.service.ts +54 -0
  40. package/templates/nest-cli.json +8 -0
  41. package/templates/package.json +52 -0
  42. package/templates/service/src/__name__.controller.ts +15 -0
  43. package/templates/service/src/__name__.module.ts +11 -0
  44. package/templates/service/src/__name__.schema.ts +15 -0
  45. package/templates/service/src/__name__.service.ts +12 -0
  46. package/templates/service/src/lambda.ts +60 -0
  47. package/templates/tsconfig.json +28 -0
  48. package/templates/ui/README.md +36 -0
  49. package/templates/ui/eslint.config.mjs +18 -0
  50. package/templates/ui/next.config.ts +8 -0
  51. package/templates/ui/package.json +33 -0
  52. package/templates/ui/postcss.config.mjs +7 -0
  53. package/templates/ui/public/file.svg +1 -0
  54. package/templates/ui/public/globe.svg +1 -0
  55. package/templates/ui/public/next.svg +1 -0
  56. package/templates/ui/public/vercel.svg +1 -0
  57. package/templates/ui/public/window.svg +1 -0
  58. package/templates/ui/src/app/LandingPage.tsx +98 -0
  59. package/templates/ui/src/app/ai/summarize/page.tsx +115 -0
  60. package/templates/ui/src/app/context/AuthContext.tsx +48 -0
  61. package/templates/ui/src/app/favicon.ico +0 -0
  62. package/templates/ui/src/app/globals.css +26 -0
  63. package/templates/ui/src/app/layout.tsx +37 -0
  64. package/templates/ui/src/app/page.tsx +7 -0
  65. package/templates/ui/src/app/services/page.tsx +99 -0
  66. package/templates/ui/src/components/Auth.css +252 -0
  67. package/templates/ui/src/components/Auth.tsx +455 -0
  68. package/templates/ui/src/components/Error.tsx +32 -0
  69. package/templates/ui/src/components/FormInput.tsx +77 -0
  70. package/templates/ui/src/components/Loading.tsx +10 -0
  71. package/templates/ui/src/components/Login.tsx +171 -0
  72. package/templates/ui/src/components/Popup.css +90 -0
  73. package/templates/ui/src/components/Signup.tsx +155 -0
  74. package/templates/ui/src/utils/axiosInstance.ts +37 -0
  75. package/templates/ui/src/utils/axiosRawInstance.ts +33 -0
  76. package/templates/ui/src/utils/util.constant.ts +0 -0
  77. package/templates/ui/src/utils/util.function.ts +165 -0
  78. package/templates/ui/src/utils/util.type.ts +64 -0
  79. package/templates/ui/src/utils/variables.ts +6 -0
  80. package/templates/ui/tailwind.config.js +8 -0
  81. 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
+ };