goobs-frontend 0.7.58 → 0.7.59
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/package.json +14 -14
- package/src/components/Button/index.tsx +6 -65
- package/src/components/Form/Popup/index.tsx +139 -124
- package/src/components/StyledComponent/helperfooter/useHelperFooter.tsx +225 -89
- package/src/components/StyledComponent/index.tsx +54 -15
- package/src/components/StyledComponent/useEffects/index.tsx +30 -52
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goobs-frontend",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.59",
|
|
4
4
|
"description": "A comprehensive React-based UI library built on Material-UI, offering a wide range of customizable components including grids, typography, buttons, cards, forms, navigation, pricing tables, steppers, tooltips, accordions, and more. Designed for building responsive and consistent user interfaces with advanced features like form validation, theming, and code syntax highlighting.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -25,29 +25,29 @@
|
|
|
25
25
|
"@emotion/cache": "^11.11.0",
|
|
26
26
|
"@emotion/react": "^11.11.4",
|
|
27
27
|
"@emotion/styled": "^11.11.5",
|
|
28
|
-
"@mui/icons-material": "^5.
|
|
29
|
-
"@mui/material": "^5.
|
|
30
|
-
"@types/lodash": "^4.17.
|
|
31
|
-
"goobs-cache": "^1.
|
|
32
|
-
"highlight.js": "^11.
|
|
28
|
+
"@mui/icons-material": "^5.16.0",
|
|
29
|
+
"@mui/material": "^5.16.0",
|
|
30
|
+
"@types/lodash": "^4.17.6",
|
|
31
|
+
"goobs-cache": "^1.3.1",
|
|
32
|
+
"highlight.js": "^11.10.0",
|
|
33
33
|
"lodash": "^4.17.21",
|
|
34
|
-
"next": "14.2.
|
|
34
|
+
"next": "14.2.5"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@next/eslint-plugin-next": "^14.2.
|
|
38
|
-
"@types/node": "^20.14.
|
|
39
|
-
"@types/react": "18.3.
|
|
37
|
+
"@next/eslint-plugin-next": "^14.2.5",
|
|
38
|
+
"@types/node": "^20.14.10",
|
|
39
|
+
"@types/react": "18.3.3",
|
|
40
40
|
"@types/react-dom": "^18.3.0",
|
|
41
|
-
"@typescript-eslint/eslint-plugin": "^7.
|
|
42
|
-
"@typescript-eslint/parser": "^7.
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
|
42
|
+
"@typescript-eslint/parser": "^7.16.0",
|
|
43
43
|
"eslint": "^8.57.0",
|
|
44
|
-
"eslint-config-next": "^14.2.
|
|
44
|
+
"eslint-config-next": "^14.2.5",
|
|
45
45
|
"eslint-config-prettier": "^9.1.0",
|
|
46
46
|
"eslint-plugin-prettier": "^5.1.3",
|
|
47
47
|
"prettier": "^3.3.2",
|
|
48
48
|
"react": "^18.3.1",
|
|
49
49
|
"react-dom": "^18.3.1",
|
|
50
|
-
"typescript": "^5.5.
|
|
50
|
+
"typescript": "^5.5.3"
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
53
|
"src"
|
|
@@ -7,46 +7,24 @@ import Typography from '../Typography'
|
|
|
7
7
|
import { get, JSONValue } from 'goobs-cache'
|
|
8
8
|
import { red } from '../../styles/palette'
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Defines the possible alignment options for button content.
|
|
12
|
-
*/
|
|
13
10
|
export type ButtonAlignment = 'left' | 'center' | 'right'
|
|
14
11
|
|
|
15
|
-
/**
|
|
16
|
-
* Defines the structure of helper footer messages used for form validation.
|
|
17
|
-
*/
|
|
18
12
|
export interface HelperFooterMessage {
|
|
19
|
-
/** Indicates whether the message represents an error or success state */
|
|
20
13
|
status: 'error' | 'success'
|
|
21
|
-
/** The message to display in the status area */
|
|
22
14
|
statusMessage: string
|
|
23
|
-
/** The message to spread across multiple components */
|
|
24
15
|
spreadMessage: string
|
|
25
|
-
/** Priority of the spread message for determining which message to show */
|
|
26
16
|
spreadMessagePriority: number
|
|
27
|
-
/** The name of the form this message is associated with */
|
|
28
17
|
formname: string
|
|
29
|
-
/** Indicates if the field associated with this message is required */
|
|
30
18
|
required: boolean
|
|
31
19
|
}
|
|
32
20
|
|
|
33
|
-
/**
|
|
34
|
-
* Props for the CustomButton component.
|
|
35
|
-
* Extends ButtonProps from Material-UI, omitting 'color' and 'variant'.
|
|
36
|
-
*/
|
|
37
21
|
export interface CustomButtonProps
|
|
38
22
|
extends Omit<ButtonProps, 'color' | 'variant'> {
|
|
39
|
-
/** Text to display on the button */
|
|
40
23
|
text?: string
|
|
41
|
-
/** Background color of the button */
|
|
42
24
|
backgroundcolor?: string
|
|
43
|
-
/** Color of the button's outline */
|
|
44
25
|
outlinecolor?: string
|
|
45
|
-
/** Color of the button's text */
|
|
46
26
|
fontcolor?: string
|
|
47
|
-
/** Alignment of the button's text */
|
|
48
27
|
fontlocation?: ButtonAlignment
|
|
49
|
-
/** Typography variant for the button's text */
|
|
50
28
|
fontvariant?:
|
|
51
29
|
| 'arapeyh1'
|
|
52
30
|
| 'arapeyh2'
|
|
@@ -75,32 +53,18 @@ export interface CustomButtonProps
|
|
|
75
53
|
| 'merriparagraph'
|
|
76
54
|
| 'merrihelperheader'
|
|
77
55
|
| 'merrihelperfooter'
|
|
78
|
-
/** Icon to display on the button. Set to false to hide the icon */
|
|
79
56
|
icon?: React.ReactNode | false
|
|
80
|
-
/** Color of the icon */
|
|
81
57
|
iconcolor?: string
|
|
82
|
-
/** Size of the icon */
|
|
83
58
|
iconsize?: string
|
|
84
|
-
/** Position of the icon relative to the text */
|
|
85
59
|
iconlocation?: 'left' | 'top' | 'right'
|
|
86
|
-
/** Style variant of the button */
|
|
87
60
|
variant?: 'text' | 'outlined' | 'contained'
|
|
88
|
-
/** Function to call when the button is clicked */
|
|
89
61
|
onClick?: () => void
|
|
90
|
-
/** Helper footer message for form validation */
|
|
91
62
|
helperfooter?: HelperFooterMessage
|
|
92
|
-
/** Width of the button */
|
|
93
63
|
width?: string
|
|
94
|
-
/** Name of the form this button is associated with */
|
|
95
64
|
formname?: string
|
|
96
|
-
/** Name attribute for the button element */
|
|
97
65
|
name?: string
|
|
98
66
|
}
|
|
99
67
|
|
|
100
|
-
/**
|
|
101
|
-
* CustomButton component renders a customizable button with integrated form validation.
|
|
102
|
-
* It displays error messages based on helper footers and form validation status.
|
|
103
|
-
*/
|
|
104
68
|
const CustomButton: React.FC<CustomButtonProps> = props => {
|
|
105
69
|
const {
|
|
106
70
|
text,
|
|
@@ -121,24 +85,16 @@ const CustomButton: React.FC<CustomButtonProps> = props => {
|
|
|
121
85
|
width,
|
|
122
86
|
} = props
|
|
123
87
|
|
|
124
|
-
/** State for storing the current error message */
|
|
125
88
|
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
|
126
89
|
undefined
|
|
127
90
|
)
|
|
128
|
-
/** State for tracking whether the associated form is valid */
|
|
129
91
|
const [isFormValid, setIsFormValid] = useState<boolean>(true)
|
|
130
|
-
/** State for storing helper footer messages */
|
|
131
92
|
const [helperFooterValue, setHelperFooterValue] = useState<
|
|
132
93
|
Record<string, HelperFooterMessage>
|
|
133
94
|
>({})
|
|
134
95
|
|
|
135
|
-
/**
|
|
136
|
-
* Updates the form validation status and error message based on helper footers.
|
|
137
|
-
* This function filters relevant footers, checks for errors and empty required fields,
|
|
138
|
-
* and updates the error message and form validity accordingly.
|
|
139
|
-
*/
|
|
140
96
|
const updateFormValidation = useCallback(() => {
|
|
141
|
-
if (formname) {
|
|
97
|
+
if (formname && helperFooterValue) {
|
|
142
98
|
const relevantFooters = Object.values(helperFooterValue).filter(
|
|
143
99
|
footer => footer?.formname === formname
|
|
144
100
|
)
|
|
@@ -168,13 +124,12 @@ const CustomButton: React.FC<CustomButtonProps> = props => {
|
|
|
168
124
|
setErrorMessage(undefined)
|
|
169
125
|
setIsFormValid(true)
|
|
170
126
|
}
|
|
127
|
+
} else {
|
|
128
|
+
setErrorMessage(undefined)
|
|
129
|
+
setIsFormValid(true)
|
|
171
130
|
}
|
|
172
131
|
}, [formname, helperFooterValue])
|
|
173
132
|
|
|
174
|
-
/**
|
|
175
|
-
* Fetches helper footer data from the cache when formname changes.
|
|
176
|
-
* This effect runs whenever the formname prop changes.
|
|
177
|
-
*/
|
|
178
133
|
useEffect(() => {
|
|
179
134
|
const fetchHelperFooter = async () => {
|
|
180
135
|
const helperFooterResult = await get('helperFooter', 'client')
|
|
@@ -189,28 +144,18 @@ const CustomButton: React.FC<CustomButtonProps> = props => {
|
|
|
189
144
|
HelperFooterMessage
|
|
190
145
|
>
|
|
191
146
|
)
|
|
147
|
+
} else {
|
|
148
|
+
setHelperFooterValue({})
|
|
192
149
|
}
|
|
193
150
|
}
|
|
194
151
|
|
|
195
152
|
fetchHelperFooter()
|
|
196
153
|
}, [formname])
|
|
197
154
|
|
|
198
|
-
/**
|
|
199
|
-
* Triggers form validation whenever helperFooterValue changes.
|
|
200
|
-
* This effect ensures that the form validation is updated whenever
|
|
201
|
-
* the helper footer messages change.
|
|
202
|
-
*/
|
|
203
155
|
useEffect(() => {
|
|
204
156
|
updateFormValidation()
|
|
205
157
|
}, [updateFormValidation])
|
|
206
158
|
|
|
207
|
-
/**
|
|
208
|
-
* Renders the icon element based on the provided icon prop.
|
|
209
|
-
* If the icon prop is false, it returns null.
|
|
210
|
-
* If the icon is a valid React element, it clones it with the specified size.
|
|
211
|
-
* Otherwise, it renders a default StarIcon.
|
|
212
|
-
* @returns The rendered icon element or null.
|
|
213
|
-
*/
|
|
214
159
|
const renderIcon = () => {
|
|
215
160
|
if (icon === false) {
|
|
216
161
|
return null
|
|
@@ -223,10 +168,6 @@ const CustomButton: React.FC<CustomButtonProps> = props => {
|
|
|
223
168
|
return <StarIcon style={{ fontSize: iconsize }} />
|
|
224
169
|
}
|
|
225
170
|
|
|
226
|
-
/**
|
|
227
|
-
* Handles the button click event.
|
|
228
|
-
* If the form is valid and an onClick handler is provided, it calls the handler.
|
|
229
|
-
*/
|
|
230
171
|
const handleButtonClick = async () => {
|
|
231
172
|
if (!isFormValid) {
|
|
232
173
|
return
|
|
@@ -1,172 +1,187 @@
|
|
|
1
|
-
// File: src/components/PopupForm/index.tsx
|
|
2
1
|
'use client'
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
forwardRef,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useRef,
|
|
7
|
+
useMemo,
|
|
8
|
+
useCallback,
|
|
9
|
+
} from 'react'
|
|
4
10
|
import { Close } from '@mui/icons-material'
|
|
5
11
|
import { Dialog, IconButton, Box } from '@mui/material'
|
|
6
12
|
import ContentSection, { ContentSectionProps } from '../../Content'
|
|
7
13
|
import { formContainerStyle } from './../../../styles/Form'
|
|
8
14
|
import { ExtendedTypographyProps } from '../../Content/Structure/typography/useGridTypography'
|
|
9
|
-
import Typography from '../../Typography'
|
|
10
15
|
|
|
11
16
|
/**
|
|
12
17
|
* Props for the PopupForm component.
|
|
18
|
+
* @interface PopupFormProps
|
|
13
19
|
*/
|
|
14
20
|
export interface PopupFormProps {
|
|
15
|
-
/**
|
|
21
|
+
/** The title of the popup form */
|
|
16
22
|
title?: string
|
|
17
|
-
/**
|
|
23
|
+
/** The description of the popup form */
|
|
18
24
|
description?: string
|
|
19
|
-
/**
|
|
20
|
-
open: boolean
|
|
21
|
-
/** Function to call when closing the dialog */
|
|
22
|
-
onClose: () => void
|
|
23
|
-
/** ContentSectionProps to render the form content */
|
|
25
|
+
/** The grid configuration for the form content */
|
|
24
26
|
grids?: ContentSectionProps['grids']
|
|
25
|
-
/**
|
|
26
|
-
onSubmit?: () => void
|
|
27
|
-
/**
|
|
27
|
+
/** Callback function to handle form submission */
|
|
28
|
+
onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void
|
|
29
|
+
/** Custom content to render inside the form */
|
|
28
30
|
content?: React.ReactNode
|
|
31
|
+
/** The type of popup to render ('dialog' or 'modal') */
|
|
32
|
+
popupType: 'dialog' | 'modal'
|
|
33
|
+
/** Whether the popup is open (only applicable for 'dialog' type) */
|
|
34
|
+
open?: boolean
|
|
35
|
+
/** Callback function to handle closing the popup (only applicable for 'dialog' type) */
|
|
36
|
+
onClose?: () => void
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
/**
|
|
32
|
-
* PopupForm
|
|
33
|
-
*
|
|
34
|
-
*
|
|
40
|
+
* PopupForm Component
|
|
41
|
+
*
|
|
42
|
+
* A flexible popup form component that can be rendered as either a dialog or a modal.
|
|
43
|
+
* It supports custom content, grids, and header configuration.
|
|
35
44
|
*
|
|
36
|
-
* @
|
|
37
|
-
* @
|
|
38
|
-
*
|
|
45
|
+
* @component
|
|
46
|
+
* @example
|
|
47
|
+
* <PopupForm
|
|
48
|
+
* title="Login"
|
|
49
|
+
* description="Please enter your credentials"
|
|
50
|
+
* popupType="dialog"
|
|
51
|
+
* open={isOpen}
|
|
52
|
+
* onClose={handleClose}
|
|
53
|
+
* onSubmit={handleSubmit}
|
|
54
|
+
* content={<LoginForm />}
|
|
55
|
+
* />
|
|
39
56
|
*/
|
|
40
57
|
const PopupForm = forwardRef<HTMLFormElement, PopupFormProps>(
|
|
41
|
-
(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
(
|
|
59
|
+
{ title, description, grids, onSubmit, content, popupType, open, onClose },
|
|
60
|
+
ref
|
|
61
|
+
) => {
|
|
45
62
|
const internalFormRef = useRef<HTMLFormElement>(null)
|
|
46
63
|
|
|
64
|
+
// Expose the internal form ref to the parent component
|
|
47
65
|
useImperativeHandle(ref, () => internalFormRef.current as HTMLFormElement)
|
|
48
66
|
|
|
49
67
|
/**
|
|
50
|
-
*
|
|
51
|
-
* It includes the title and description as typography items.
|
|
68
|
+
* Memoized header grid configuration
|
|
52
69
|
*/
|
|
53
|
-
const headerGrid: ContentSectionProps['grids'][0] =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
marginbottom: 1,
|
|
58
|
-
gridwidth: '100%',
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
typography: [
|
|
62
|
-
{
|
|
63
|
-
text: title,
|
|
64
|
-
fontvariant: 'merrih5',
|
|
65
|
-
fontcolor: 'black',
|
|
66
|
-
columnconfig: {
|
|
67
|
-
column: 1,
|
|
70
|
+
const headerGrid: ContentSectionProps['grids'][0] = useMemo(
|
|
71
|
+
() => ({
|
|
72
|
+
grid: {
|
|
73
|
+
gridconfig: {
|
|
68
74
|
gridname: 'formHeader',
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
marginbottom: 0.5,
|
|
75
|
+
marginbottom: 1,
|
|
76
|
+
gridwidth: '100%',
|
|
72
77
|
},
|
|
73
78
|
},
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
typography: [
|
|
80
|
+
{
|
|
81
|
+
text: title,
|
|
82
|
+
fontvariant: 'merrih5',
|
|
83
|
+
fontcolor: 'black',
|
|
84
|
+
columnconfig: {
|
|
85
|
+
row: 1,
|
|
86
|
+
column: 1,
|
|
87
|
+
gridname: 'formHeader',
|
|
88
|
+
columnwidth: '100%',
|
|
89
|
+
alignment: 'left',
|
|
90
|
+
marginbottom: 1.5,
|
|
91
|
+
},
|
|
82
92
|
},
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
{
|
|
94
|
+
text: description,
|
|
95
|
+
fontvariant: 'merriparagraph',
|
|
96
|
+
fontcolor: 'black',
|
|
97
|
+
columnconfig: {
|
|
98
|
+
row: 2,
|
|
99
|
+
column: 1,
|
|
100
|
+
alignment: 'left',
|
|
101
|
+
gridname: 'formHeader',
|
|
102
|
+
columnwidth: '100%',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
] as ExtendedTypographyProps[],
|
|
106
|
+
}),
|
|
107
|
+
[title, description]
|
|
108
|
+
)
|
|
92
109
|
|
|
93
110
|
/**
|
|
94
|
-
*
|
|
95
|
-
* @param event - The form submission event
|
|
111
|
+
* Handle form submission
|
|
112
|
+
* @param {React.FormEvent<HTMLFormElement>} event - The form submission event
|
|
96
113
|
*/
|
|
97
|
-
const handleSubmit = (
|
|
98
|
-
event.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
setSubmittedData(data)
|
|
107
|
-
|
|
108
|
-
if (onSubmit) {
|
|
109
|
-
onSubmit()
|
|
110
|
-
}
|
|
111
|
-
}
|
|
114
|
+
const handleSubmit = useCallback(
|
|
115
|
+
(event: React.FormEvent<HTMLFormElement>) => {
|
|
116
|
+
event.preventDefault()
|
|
117
|
+
if (onSubmit) {
|
|
118
|
+
onSubmit(event)
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
[onSubmit]
|
|
122
|
+
)
|
|
112
123
|
|
|
113
124
|
/**
|
|
114
|
-
*
|
|
125
|
+
* Memoized header render function
|
|
115
126
|
*/
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
<Typography key={index} {...typo} />
|
|
121
|
-
)
|
|
122
|
-
)
|
|
123
|
-
} else if (headerGrid.typography) {
|
|
124
|
-
return <Typography {...headerGrid.typography} />
|
|
125
|
-
}
|
|
126
|
-
return null
|
|
127
|
-
}
|
|
127
|
+
const renderHeader = useMemo(
|
|
128
|
+
() => <ContentSection grids={[headerGrid]} />,
|
|
129
|
+
[headerGrid]
|
|
130
|
+
)
|
|
128
131
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
sx={{
|
|
135
|
-
position: 'absolute',
|
|
136
|
-
right: 8,
|
|
137
|
-
top: 8,
|
|
138
|
-
color: theme => theme.palette.grey[500],
|
|
139
|
-
}}
|
|
140
|
-
>
|
|
141
|
-
<Close />
|
|
142
|
-
</IconButton>
|
|
132
|
+
/**
|
|
133
|
+
* Memoized content render function
|
|
134
|
+
*/
|
|
135
|
+
const renderContent = useMemo(
|
|
136
|
+
() => (
|
|
143
137
|
<Box
|
|
144
138
|
// @ts-ignore
|
|
145
139
|
sx={formContainerStyle}
|
|
146
140
|
>
|
|
141
|
+
<Box mb={0}>{renderHeader}</Box>
|
|
147
142
|
<form onSubmit={handleSubmit} ref={internalFormRef}>
|
|
148
|
-
{content
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
) : (
|
|
154
|
-
<ContentSection grids={contentSectionGrids} />
|
|
155
|
-
)}
|
|
143
|
+
{React.isValidElement(content)
|
|
144
|
+
? React.cloneElement(content as React.ReactElement, {
|
|
145
|
+
onSubmit: handleSubmit,
|
|
146
|
+
})
|
|
147
|
+
: content || (grids && <ContentSection grids={grids} />)}
|
|
156
148
|
</form>
|
|
157
|
-
{Object.keys(submittedData).length > 0 && (
|
|
158
|
-
<Box mt={2}>
|
|
159
|
-
<Typography fontvariant="merrih6" text="Submitted Data:" />
|
|
160
|
-
{Object.entries(submittedData).map(([key, value]) => (
|
|
161
|
-
<Typography
|
|
162
|
-
key={key}
|
|
163
|
-
fontvariant="merriparagraph"
|
|
164
|
-
text={`${key}: ${value}`}
|
|
165
|
-
/>
|
|
166
|
-
))}
|
|
167
|
-
</Box>
|
|
168
|
-
)}
|
|
169
149
|
</Box>
|
|
150
|
+
),
|
|
151
|
+
[renderHeader, handleSubmit, content, grids]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if (popupType === 'modal') {
|
|
155
|
+
return (
|
|
156
|
+
<Dialog
|
|
157
|
+
open={true}
|
|
158
|
+
fullWidth
|
|
159
|
+
maxWidth="sm"
|
|
160
|
+
disableEscapeKeyDown
|
|
161
|
+
hideBackdrop
|
|
162
|
+
>
|
|
163
|
+
{renderContent}
|
|
164
|
+
</Dialog>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<Dialog open={open || false} onClose={onClose} fullWidth maxWidth="sm">
|
|
170
|
+
{onClose && (
|
|
171
|
+
<IconButton
|
|
172
|
+
size="small"
|
|
173
|
+
onClick={onClose}
|
|
174
|
+
sx={{
|
|
175
|
+
position: 'absolute',
|
|
176
|
+
right: 8,
|
|
177
|
+
top: 8,
|
|
178
|
+
color: theme => theme.palette.grey[500],
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<Close />
|
|
182
|
+
</IconButton>
|
|
183
|
+
)}
|
|
184
|
+
{renderContent}
|
|
170
185
|
</Dialog>
|
|
171
186
|
)
|
|
172
187
|
}
|
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useCallback } from 'react'
|
|
3
|
+
import { useCallback, useState, useMemo, useRef, useEffect } from 'react'
|
|
4
4
|
import { debounce } from 'lodash'
|
|
5
|
-
import
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import { get, set, StringValue } from 'goobs-cache'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Validates if the given string is
|
|
9
|
-
*
|
|
10
|
-
* @
|
|
9
|
+
* Validates if the given string is a valid email format.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} email - The email string to validate.
|
|
12
|
+
* @returns {boolean} True if the email is valid, false otherwise.
|
|
11
13
|
*/
|
|
12
14
|
const isValidEmailFormat = (email: string): boolean => {
|
|
13
15
|
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
|
14
|
-
|
|
15
|
-
return result
|
|
16
|
+
return emailRegex.test(email)
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
|
-
*
|
|
20
|
+
* Represents a message for the helper footer.
|
|
21
|
+
*
|
|
22
|
+
* @interface HelperFooterMessage
|
|
23
|
+
* @property {('error' | 'success')} status - The status of the message.
|
|
24
|
+
* @property {string} statusMessage - The detailed status message.
|
|
25
|
+
* @property {string} spreadMessage - A condensed version of the message for spreading.
|
|
26
|
+
* @property {number} spreadMessagePriority - The priority of the spread message.
|
|
27
|
+
* @property {string} formname - The name of the form this message is associated with.
|
|
28
|
+
* @property {boolean} required - Indicates if the field associated with this message is required.
|
|
20
29
|
*/
|
|
21
30
|
export interface HelperFooterMessage {
|
|
22
31
|
status: 'error' | 'success'
|
|
@@ -28,12 +37,25 @@ export interface HelperFooterMessage {
|
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
40
|
+
* A custom hook for managing form validation and helper messages.
|
|
41
|
+
*
|
|
42
|
+
* @returns {Object} An object containing validation functions and helper footer state.
|
|
33
43
|
*/
|
|
34
44
|
export const useHelperFooter = () => {
|
|
45
|
+
const [helperFooterValue, setHelperFooterValue] = useState<
|
|
46
|
+
Record<string, HelperFooterMessage>
|
|
47
|
+
>({})
|
|
48
|
+
const helperFooterRef = useRef<Record<string, HelperFooterMessage>>({})
|
|
49
|
+
|
|
35
50
|
/**
|
|
36
|
-
* Handles generic error
|
|
51
|
+
* Handles creation of generic error messages for form fields.
|
|
52
|
+
*
|
|
53
|
+
* @param {FormData} formData - The form data to validate.
|
|
54
|
+
* @param {string} name - The name of the form field.
|
|
55
|
+
* @param {string} label - The label of the form field.
|
|
56
|
+
* @param {boolean} required - Whether the field is required.
|
|
57
|
+
* @param {string} formname - The name of the form.
|
|
58
|
+
* @returns {HelperFooterMessage | undefined} An error message object if there's an error, undefined otherwise.
|
|
37
59
|
*/
|
|
38
60
|
const handleGenericErrorCreation = useCallback(
|
|
39
61
|
(
|
|
@@ -43,9 +65,10 @@ export const useHelperFooter = () => {
|
|
|
43
65
|
required: boolean,
|
|
44
66
|
formname: string
|
|
45
67
|
): HelperFooterMessage | undefined => {
|
|
68
|
+
console.log(`Handling generic error creation for ${name}`)
|
|
46
69
|
const value = formData.get(name) as string
|
|
47
|
-
|
|
48
70
|
if (required && (!value || !value.trim())) {
|
|
71
|
+
console.log(`Error: ${label} is required`)
|
|
49
72
|
return {
|
|
50
73
|
status: 'error',
|
|
51
74
|
statusMessage: `${label} is required. Please enter a ${label.toLowerCase()}.`,
|
|
@@ -55,14 +78,19 @@ export const useHelperFooter = () => {
|
|
|
55
78
|
required,
|
|
56
79
|
}
|
|
57
80
|
}
|
|
58
|
-
|
|
81
|
+
console.log(`No error for ${name}`)
|
|
59
82
|
return undefined
|
|
60
83
|
},
|
|
61
84
|
[]
|
|
62
85
|
)
|
|
63
86
|
|
|
64
87
|
/**
|
|
65
|
-
* Handles
|
|
88
|
+
* Handles creation of email-specific error messages.
|
|
89
|
+
*
|
|
90
|
+
* @param {FormData} formData - The form data to validate.
|
|
91
|
+
* @param {boolean} required - Whether the email field is required.
|
|
92
|
+
* @param {string} formname - The name of the form.
|
|
93
|
+
* @returns {HelperFooterMessage | undefined} An error or success message object, or undefined if no message is needed.
|
|
66
94
|
*/
|
|
67
95
|
const handleEmailErrorCreation = useCallback(
|
|
68
96
|
(
|
|
@@ -70,9 +98,10 @@ export const useHelperFooter = () => {
|
|
|
70
98
|
required: boolean,
|
|
71
99
|
formname: string
|
|
72
100
|
): HelperFooterMessage | undefined => {
|
|
101
|
+
console.log('Handling email error creation')
|
|
73
102
|
const email = formData.get('email') as string
|
|
74
|
-
|
|
75
103
|
if (required && (!email || !email.trim())) {
|
|
104
|
+
console.log('Error: Email is required')
|
|
76
105
|
return {
|
|
77
106
|
status: 'error',
|
|
78
107
|
statusMessage: 'Please enter an email address.',
|
|
@@ -82,12 +111,9 @@ export const useHelperFooter = () => {
|
|
|
82
111
|
required,
|
|
83
112
|
}
|
|
84
113
|
}
|
|
85
|
-
|
|
86
|
-
if (!email) {
|
|
87
|
-
return undefined
|
|
88
|
-
}
|
|
89
|
-
|
|
114
|
+
if (!email) return undefined
|
|
90
115
|
if (email && !isValidEmailFormat(email)) {
|
|
116
|
+
console.log('Error: Invalid email format')
|
|
91
117
|
return {
|
|
92
118
|
status: 'error',
|
|
93
119
|
statusMessage: 'Please enter a valid email address.',
|
|
@@ -97,7 +123,7 @@ export const useHelperFooter = () => {
|
|
|
97
123
|
required,
|
|
98
124
|
}
|
|
99
125
|
}
|
|
100
|
-
|
|
126
|
+
console.log('Email is valid')
|
|
101
127
|
return {
|
|
102
128
|
status: 'success',
|
|
103
129
|
statusMessage: 'Email is valid.',
|
|
@@ -111,25 +137,61 @@ export const useHelperFooter = () => {
|
|
|
111
137
|
)
|
|
112
138
|
|
|
113
139
|
/**
|
|
114
|
-
* Handles password
|
|
140
|
+
* Handles creation of password-specific error messages and stores the password in the client cache.
|
|
141
|
+
*
|
|
142
|
+
* @param {FormData} formData - The form data to validate.
|
|
143
|
+
* @param {boolean} required - Whether the password field is required.
|
|
144
|
+
* @param {string} formname - The name of the form.
|
|
145
|
+
* @returns {HelperFooterMessage | undefined} An error or success message object, or undefined if no message is needed.
|
|
115
146
|
*/
|
|
116
147
|
const handlePasswordErrorCreation = useCallback(
|
|
117
|
-
|
|
148
|
+
(
|
|
118
149
|
formData: FormData,
|
|
119
150
|
required: boolean,
|
|
120
151
|
formname: string
|
|
121
|
-
):
|
|
152
|
+
): HelperFooterMessage | undefined => {
|
|
153
|
+
console.log('Handling password error creation')
|
|
122
154
|
const password = formData.get('verifyPassword') as string
|
|
155
|
+
console.log('Password received:', password)
|
|
156
|
+
|
|
157
|
+
const debouncedPasswordStorage = debounce(async () => {
|
|
158
|
+
console.log('Attempting to store password in goobs-cache client store')
|
|
159
|
+
try {
|
|
160
|
+
if (password) {
|
|
161
|
+
console.log('Setting password in goobs-cache:', password)
|
|
162
|
+
await set(
|
|
163
|
+
'verifyPassword',
|
|
164
|
+
{ type: 'string', value: password } as StringValue,
|
|
165
|
+
new Date(Date.now() + 30 * 60 * 1000),
|
|
166
|
+
'client'
|
|
167
|
+
)
|
|
168
|
+
console.log('Password set operation completed')
|
|
169
|
+
|
|
170
|
+
// Immediately retrieve the password to verify it was stored
|
|
171
|
+
console.log('Attempting to retrieve password from client store')
|
|
172
|
+
const storedPassword = await get('verifyPassword', 'client')
|
|
173
|
+
console.log('Raw stored password result:', storedPassword)
|
|
174
|
+
|
|
175
|
+
if (storedPassword && storedPassword.value) {
|
|
176
|
+
console.log(
|
|
177
|
+
'Immediately retrieved password from client store:',
|
|
178
|
+
storedPassword.value
|
|
179
|
+
)
|
|
180
|
+
} else {
|
|
181
|
+
console.log('Retrieved password is null or undefined')
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
console.log('Password is null or empty, not storing in goobs-cache')
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error('Error interacting with goobs-cache:', error)
|
|
188
|
+
}
|
|
189
|
+
}, 2000)
|
|
123
190
|
|
|
124
|
-
|
|
125
|
-
await set(
|
|
126
|
-
'verifyPassword',
|
|
127
|
-
{ type: 'string', value: password } as StringValue,
|
|
128
|
-
new Date(Date.now() + 30 * 60 * 1000),
|
|
129
|
-
'client'
|
|
130
|
-
)
|
|
191
|
+
debouncedPasswordStorage()
|
|
131
192
|
|
|
132
193
|
if (required && (!password || !password.trim())) {
|
|
194
|
+
console.log('Error: Password is required')
|
|
133
195
|
return {
|
|
134
196
|
status: 'error',
|
|
135
197
|
statusMessage: 'Password is required.',
|
|
@@ -139,11 +201,7 @@ export const useHelperFooter = () => {
|
|
|
139
201
|
required,
|
|
140
202
|
}
|
|
141
203
|
}
|
|
142
|
-
|
|
143
|
-
if (!password) {
|
|
144
|
-
return undefined
|
|
145
|
-
}
|
|
146
|
-
|
|
204
|
+
if (!password) return undefined
|
|
147
205
|
const passwordRegex =
|
|
148
206
|
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
|
|
149
207
|
const passwordComplexityStatus: 'error' | 'success' = passwordRegex.test(
|
|
@@ -151,8 +209,8 @@ export const useHelperFooter = () => {
|
|
|
151
209
|
)
|
|
152
210
|
? 'success'
|
|
153
211
|
: 'error'
|
|
154
|
-
|
|
155
212
|
if (passwordComplexityStatus === 'error') {
|
|
213
|
+
console.log('Error: Invalid password complexity')
|
|
156
214
|
return {
|
|
157
215
|
status: 'error',
|
|
158
216
|
statusMessage:
|
|
@@ -163,7 +221,7 @@ export const useHelperFooter = () => {
|
|
|
163
221
|
required,
|
|
164
222
|
}
|
|
165
223
|
}
|
|
166
|
-
|
|
224
|
+
console.log('Password meets all requirements')
|
|
167
225
|
return {
|
|
168
226
|
status: 'success',
|
|
169
227
|
statusMessage: 'Password meets all requirements.',
|
|
@@ -177,7 +235,12 @@ export const useHelperFooter = () => {
|
|
|
177
235
|
)
|
|
178
236
|
|
|
179
237
|
/**
|
|
180
|
-
* Handles
|
|
238
|
+
* Handles creation of confirm password error messages by comparing with the stored password.
|
|
239
|
+
*
|
|
240
|
+
* @param {FormData} formData - The form data to validate.
|
|
241
|
+
* @param {boolean} required - Whether the confirm password field is required.
|
|
242
|
+
* @param {string} formname - The name of the form.
|
|
243
|
+
* @returns {Promise<HelperFooterMessage | undefined>} A promise that resolves to an error or success message object, or undefined if no message is needed.
|
|
181
244
|
*/
|
|
182
245
|
const handleConfirmPasswordErrorCreation = useCallback(
|
|
183
246
|
async (
|
|
@@ -185,9 +248,12 @@ export const useHelperFooter = () => {
|
|
|
185
248
|
required: boolean,
|
|
186
249
|
formname: string
|
|
187
250
|
): Promise<HelperFooterMessage | undefined> => {
|
|
251
|
+
console.log('Handling confirm password error creation')
|
|
188
252
|
const confirmPassword = formData.get('confirmPassword') as string
|
|
253
|
+
console.log('Confirm password received:', confirmPassword)
|
|
189
254
|
|
|
190
255
|
if (required && (!confirmPassword || !confirmPassword.trim())) {
|
|
256
|
+
console.log('Error: Password confirmation is required')
|
|
191
257
|
return {
|
|
192
258
|
status: 'error',
|
|
193
259
|
statusMessage: 'Please confirm your password.',
|
|
@@ -197,23 +263,38 @@ export const useHelperFooter = () => {
|
|
|
197
263
|
required,
|
|
198
264
|
}
|
|
199
265
|
}
|
|
266
|
+
if (!confirmPassword) return undefined
|
|
200
267
|
|
|
201
|
-
|
|
202
|
-
|
|
268
|
+
console.log(
|
|
269
|
+
'Attempting to retrieve verify password from goobs-cache client store'
|
|
270
|
+
)
|
|
271
|
+
let verifyPasswordResult
|
|
272
|
+
try {
|
|
273
|
+
verifyPasswordResult = await get('verifyPassword', 'client')
|
|
274
|
+
console.log('Retrieved verify password result:', verifyPasswordResult)
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error('Error retrieving password from goobs-cache:', error)
|
|
203
277
|
}
|
|
204
278
|
|
|
205
|
-
const verifyPasswordResult = await get('verifyPassword', 'client')
|
|
206
279
|
let verifyPassword: string | undefined
|
|
207
|
-
|
|
208
280
|
if (
|
|
209
281
|
verifyPasswordResult &&
|
|
210
282
|
typeof verifyPasswordResult === 'object' &&
|
|
211
|
-
'value' in verifyPasswordResult
|
|
283
|
+
'value' in verifyPasswordResult &&
|
|
284
|
+
verifyPasswordResult.value &&
|
|
285
|
+
typeof verifyPasswordResult.value === 'object' &&
|
|
286
|
+
'value' in verifyPasswordResult.value
|
|
212
287
|
) {
|
|
213
|
-
verifyPassword =
|
|
288
|
+
verifyPassword = verifyPasswordResult.value.value
|
|
289
|
+
console.log('Verify password from client store:', verifyPassword)
|
|
290
|
+
} else {
|
|
291
|
+
console.log(
|
|
292
|
+
'Verify password not found in client store or in unexpected format'
|
|
293
|
+
)
|
|
214
294
|
}
|
|
215
295
|
|
|
216
296
|
if (!verifyPassword) {
|
|
297
|
+
console.log('Error: Password not set')
|
|
217
298
|
return {
|
|
218
299
|
status: 'error',
|
|
219
300
|
statusMessage: 'Please enter your password first.',
|
|
@@ -225,6 +306,7 @@ export const useHelperFooter = () => {
|
|
|
225
306
|
}
|
|
226
307
|
|
|
227
308
|
if (confirmPassword !== verifyPassword) {
|
|
309
|
+
console.log('Error: Passwords do not match')
|
|
228
310
|
return {
|
|
229
311
|
status: 'error',
|
|
230
312
|
statusMessage: 'Passwords do not match.',
|
|
@@ -235,6 +317,7 @@ export const useHelperFooter = () => {
|
|
|
235
317
|
}
|
|
236
318
|
}
|
|
237
319
|
|
|
320
|
+
console.log('Passwords match')
|
|
238
321
|
return {
|
|
239
322
|
status: 'success',
|
|
240
323
|
statusMessage: 'Passwords match.',
|
|
@@ -248,7 +331,12 @@ export const useHelperFooter = () => {
|
|
|
248
331
|
)
|
|
249
332
|
|
|
250
333
|
/**
|
|
251
|
-
* Handles phone number
|
|
334
|
+
* Handles creation of phone number error messages.
|
|
335
|
+
*
|
|
336
|
+
* @param {FormData} formData - The form data to validate.
|
|
337
|
+
* @param {boolean} required - Whether the phone number field is required.
|
|
338
|
+
* @param {string} formname - The name of the form.
|
|
339
|
+
* @returns {HelperFooterMessage | undefined} An error or success message object, or undefined if no message is needed.
|
|
252
340
|
*/
|
|
253
341
|
const handlePhoneNumberErrorCreation = useCallback(
|
|
254
342
|
(
|
|
@@ -256,9 +344,10 @@ export const useHelperFooter = () => {
|
|
|
256
344
|
required: boolean,
|
|
257
345
|
formname: string
|
|
258
346
|
): HelperFooterMessage | undefined => {
|
|
347
|
+
console.log('Handling phone number error creation')
|
|
259
348
|
const phoneNumber = formData.get('phoneNumber') as string
|
|
260
|
-
|
|
261
349
|
if (required && (!phoneNumber || !phoneNumber.trim())) {
|
|
350
|
+
console.log('Error: Phone number is required')
|
|
262
351
|
return {
|
|
263
352
|
status: 'error',
|
|
264
353
|
statusMessage:
|
|
@@ -269,18 +358,14 @@ export const useHelperFooter = () => {
|
|
|
269
358
|
required,
|
|
270
359
|
}
|
|
271
360
|
}
|
|
272
|
-
|
|
273
|
-
if (!phoneNumber) {
|
|
274
|
-
return undefined
|
|
275
|
-
}
|
|
276
|
-
|
|
361
|
+
if (!phoneNumber) return undefined
|
|
277
362
|
const digitsOnly = phoneNumber.replace(/[^\d]/g, '')
|
|
278
363
|
const length = digitsOnly.length
|
|
279
|
-
|
|
280
364
|
if (
|
|
281
365
|
(length === 10 && !digitsOnly.startsWith('1')) ||
|
|
282
366
|
(length === 11 && digitsOnly.startsWith('1'))
|
|
283
367
|
) {
|
|
368
|
+
console.log('Phone number is valid')
|
|
284
369
|
return {
|
|
285
370
|
status: 'success',
|
|
286
371
|
statusMessage: 'Phone number is valid.',
|
|
@@ -290,6 +375,7 @@ export const useHelperFooter = () => {
|
|
|
290
375
|
required,
|
|
291
376
|
}
|
|
292
377
|
} else {
|
|
378
|
+
console.log('Error: Invalid phone number format')
|
|
293
379
|
return {
|
|
294
380
|
status: 'error',
|
|
295
381
|
statusMessage:
|
|
@@ -305,7 +391,13 @@ export const useHelperFooter = () => {
|
|
|
305
391
|
)
|
|
306
392
|
|
|
307
393
|
/**
|
|
308
|
-
* Validates a
|
|
394
|
+
* Validates a specific field in the form.
|
|
395
|
+
*
|
|
396
|
+
* @param {string} name - The name of the field to validate.
|
|
397
|
+
* @param {FormData} formData - The form data to validate.
|
|
398
|
+
* @param {string} label - The label of the field.
|
|
399
|
+
* @param {boolean} required - Whether the field is required.
|
|
400
|
+
* @param {string} formname - The name of the form.
|
|
309
401
|
*/
|
|
310
402
|
const validateField = useCallback(
|
|
311
403
|
(
|
|
@@ -315,10 +407,9 @@ export const useHelperFooter = () => {
|
|
|
315
407
|
required: boolean,
|
|
316
408
|
formname: string
|
|
317
409
|
) => {
|
|
318
|
-
|
|
410
|
+
console.log(`Validating field: ${name}`)
|
|
411
|
+
const validation = () => {
|
|
319
412
|
let validationResult: HelperFooterMessage | undefined
|
|
320
|
-
|
|
321
|
-
// Determine which validation function to use based on the field name
|
|
322
413
|
switch (name) {
|
|
323
414
|
case 'email':
|
|
324
415
|
validationResult = handleEmailErrorCreation(
|
|
@@ -328,19 +419,22 @@ export const useHelperFooter = () => {
|
|
|
328
419
|
)
|
|
329
420
|
break
|
|
330
421
|
case 'verifyPassword':
|
|
331
|
-
validationResult =
|
|
422
|
+
validationResult = handlePasswordErrorCreation(
|
|
332
423
|
formData,
|
|
333
424
|
required,
|
|
334
425
|
formname
|
|
335
426
|
)
|
|
336
427
|
break
|
|
337
428
|
case 'confirmPassword':
|
|
338
|
-
|
|
429
|
+
handleConfirmPasswordErrorCreation(
|
|
339
430
|
formData,
|
|
340
431
|
required,
|
|
341
432
|
formname
|
|
342
|
-
)
|
|
343
|
-
|
|
433
|
+
).then(result => {
|
|
434
|
+
validationResult = result
|
|
435
|
+
updateHelperFooter(name, validationResult)
|
|
436
|
+
})
|
|
437
|
+
return
|
|
344
438
|
case 'phoneNumber':
|
|
345
439
|
validationResult = handlePhoneNumberErrorCreation(
|
|
346
440
|
formData,
|
|
@@ -358,33 +452,10 @@ export const useHelperFooter = () => {
|
|
|
358
452
|
)
|
|
359
453
|
}
|
|
360
454
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
let currentHelperFooter: Record<string, HelperFooterMessage> = {}
|
|
364
|
-
if (
|
|
365
|
-
helperFooterResult &&
|
|
366
|
-
typeof helperFooterResult === 'object' &&
|
|
367
|
-
'value' in helperFooterResult
|
|
368
|
-
) {
|
|
369
|
-
currentHelperFooter = (helperFooterResult as JSONValue)
|
|
370
|
-
.value as Record<string, HelperFooterMessage>
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (validationResult) {
|
|
374
|
-
currentHelperFooter[name] = validationResult
|
|
375
|
-
} else {
|
|
376
|
-
delete currentHelperFooter[name]
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
await set(
|
|
380
|
-
'helperFooter',
|
|
381
|
-
{ type: 'json', value: currentHelperFooter } as JSONValue,
|
|
382
|
-
new Date(Date.now() + 30 * 60 * 1000), // 30 minutes expiration
|
|
383
|
-
'client'
|
|
384
|
-
)
|
|
385
|
-
}, 300)
|
|
455
|
+
updateHelperFooter(name, validationResult)
|
|
456
|
+
}
|
|
386
457
|
|
|
387
|
-
|
|
458
|
+
validation()
|
|
388
459
|
},
|
|
389
460
|
[
|
|
390
461
|
handleEmailErrorCreation,
|
|
@@ -395,7 +466,72 @@ export const useHelperFooter = () => {
|
|
|
395
466
|
]
|
|
396
467
|
)
|
|
397
468
|
|
|
398
|
-
|
|
399
|
-
|
|
469
|
+
const updateHelperFooter = (
|
|
470
|
+
name: string,
|
|
471
|
+
validationResult: HelperFooterMessage | undefined
|
|
472
|
+
) => {
|
|
473
|
+
if (validationResult) {
|
|
474
|
+
helperFooterRef.current = {
|
|
475
|
+
...helperFooterRef.current,
|
|
476
|
+
[name]: validationResult,
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
helperFooterRef.current = Object.fromEntries(
|
|
480
|
+
Object.entries(helperFooterRef.current).filter(([key]) => key !== name)
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
console.log('Updated helperFooterRef:', helperFooterRef.current)
|
|
485
|
+
setHelperFooterValue({ ...helperFooterRef.current })
|
|
486
|
+
console.log('Helper footer updated in state')
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const validateRequiredField = useCallback(
|
|
490
|
+
(required: boolean, formname: string, name: string, label: string) => {
|
|
491
|
+
console.log('validateRequiredField: Starting', {
|
|
492
|
+
required,
|
|
493
|
+
formname,
|
|
494
|
+
name,
|
|
495
|
+
label,
|
|
496
|
+
})
|
|
497
|
+
if (required && formname && name && label) {
|
|
498
|
+
console.log('validateRequiredField: Validating required field')
|
|
499
|
+
const emptyFormData = new FormData()
|
|
500
|
+
emptyFormData.append(name, '')
|
|
501
|
+
validateField(name, emptyFormData, label, required, formname)
|
|
502
|
+
} else {
|
|
503
|
+
console.log(
|
|
504
|
+
'validateRequiredField: Not validating (missing required props)'
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
[validateField]
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
const useShowErrorEffect = (
|
|
512
|
+
formSubmitted: boolean,
|
|
513
|
+
hasInput: boolean,
|
|
514
|
+
isFocused: boolean,
|
|
515
|
+
setShowError: React.Dispatch<React.SetStateAction<boolean>>
|
|
516
|
+
) => {
|
|
517
|
+
useEffect(() => {
|
|
518
|
+
const showError = formSubmitted || (hasInput && !isFocused)
|
|
519
|
+
console.log('useShowErrorEffect: Setting showError to', showError, {
|
|
520
|
+
formSubmitted,
|
|
521
|
+
hasInput,
|
|
522
|
+
isFocused,
|
|
523
|
+
})
|
|
524
|
+
setShowError(showError)
|
|
525
|
+
}, [formSubmitted, hasInput, isFocused, setShowError])
|
|
400
526
|
}
|
|
527
|
+
|
|
528
|
+
return useMemo(
|
|
529
|
+
() => ({
|
|
530
|
+
validateField,
|
|
531
|
+
validateRequiredField,
|
|
532
|
+
helperFooterValue,
|
|
533
|
+
useShowErrorEffect,
|
|
534
|
+
}),
|
|
535
|
+
[validateField, validateRequiredField, helperFooterValue]
|
|
536
|
+
)
|
|
401
537
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import React, { useState, useRef } from 'react'
|
|
3
|
+
import React, { useState, useRef, useEffect } from 'react'
|
|
4
4
|
import { Box, InputLabel, OutlinedInput, styled } from '@mui/material'
|
|
5
5
|
import { useDropdown } from './hooks/useDropdown'
|
|
6
6
|
import { usePhoneNumber } from './hooks/usePhoneNumber'
|
|
@@ -14,13 +14,7 @@ import {
|
|
|
14
14
|
HelperFooterMessage,
|
|
15
15
|
} from './helperfooter/useHelperFooter'
|
|
16
16
|
import labelStyles from '../../styles/StyledComponent/Label'
|
|
17
|
-
import {
|
|
18
|
-
useHelperFooterEffect,
|
|
19
|
-
useHasInputEffect,
|
|
20
|
-
usePreventAutocompleteEffect,
|
|
21
|
-
useValidateRequiredEffect,
|
|
22
|
-
useShowErrorEffect,
|
|
23
|
-
} from './useEffects'
|
|
17
|
+
import { useHasInputEffect, usePreventAutocompleteEffect } from './useEffects'
|
|
24
18
|
|
|
25
19
|
/**
|
|
26
20
|
* Props interface for the StyledComponent
|
|
@@ -118,10 +112,16 @@ const StyledComponent: React.FC<StyledComponentProps> = props => {
|
|
|
118
112
|
'aria-describedby': ariaDescribedBy,
|
|
119
113
|
} = props
|
|
120
114
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
console.log('StyledComponent: Initializing with props', {
|
|
116
|
+
name,
|
|
117
|
+
label,
|
|
118
|
+
required,
|
|
119
|
+
formname,
|
|
120
|
+
formSubmitted,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const { validateField, validateRequiredField, helperFooterValue } =
|
|
124
|
+
useHelperFooter()
|
|
125
125
|
const [isFocused, setIsFocused] = useState(false)
|
|
126
126
|
const [hasInput, setHasInput] = useState(false)
|
|
127
127
|
const [showError, setShowError] = useState(false)
|
|
@@ -142,13 +142,25 @@ const StyledComponent: React.FC<StyledComponentProps> = props => {
|
|
|
142
142
|
} = useSplitButton(props)
|
|
143
143
|
|
|
144
144
|
// useEffect hooks
|
|
145
|
-
useHelperFooterEffect(setHelperFooterValue)
|
|
146
145
|
useHasInputEffect(value, valuestatus, setHasInput)
|
|
147
146
|
usePreventAutocompleteEffect(inputRefInternal)
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (required && formname && name && label) {
|
|
150
|
+
validateRequiredField(required, formname, name, label)
|
|
151
|
+
}
|
|
152
|
+
}, [required, formname, name, label, validateRequiredField])
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const timer = setTimeout(() => {
|
|
156
|
+
setShowError(formSubmitted || hasInput)
|
|
157
|
+
}, 1000)
|
|
158
|
+
|
|
159
|
+
return () => clearTimeout(timer)
|
|
160
|
+
}, [formSubmitted, hasInput])
|
|
150
161
|
|
|
151
162
|
const currentHelperFooter = name ? helperFooterValue[name] : undefined
|
|
163
|
+
console.log('StyledComponent: Current helper footer', currentHelperFooter)
|
|
152
164
|
|
|
153
165
|
/**
|
|
154
166
|
* Handle the change event of the input element.
|
|
@@ -157,6 +169,11 @@ const StyledComponent: React.FC<StyledComponentProps> = props => {
|
|
|
157
169
|
const handleChange = (
|
|
158
170
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
159
171
|
) => {
|
|
172
|
+
console.log('StyledComponent: handleChange called', {
|
|
173
|
+
name: e.target.name,
|
|
174
|
+
value: e.target.value,
|
|
175
|
+
})
|
|
176
|
+
|
|
160
177
|
if (componentvariant === 'phonenumber') {
|
|
161
178
|
handlePhoneNumberChange(e)
|
|
162
179
|
} else if (componentvariant === 'splitbutton') {
|
|
@@ -170,6 +187,12 @@ const StyledComponent: React.FC<StyledComponentProps> = props => {
|
|
|
170
187
|
const formData = new FormData()
|
|
171
188
|
formData.append(e.target.name, e.target.value)
|
|
172
189
|
if (name && label && formname) {
|
|
190
|
+
console.log('StyledComponent: Calling validateField', {
|
|
191
|
+
name,
|
|
192
|
+
label,
|
|
193
|
+
required,
|
|
194
|
+
formname,
|
|
195
|
+
})
|
|
173
196
|
validateField(name, formData, label, required, formname)
|
|
174
197
|
}
|
|
175
198
|
}
|
|
@@ -178,6 +201,7 @@ const StyledComponent: React.FC<StyledComponentProps> = props => {
|
|
|
178
201
|
* Handle the focus event of the input element.
|
|
179
202
|
*/
|
|
180
203
|
const handleFocus = () => {
|
|
204
|
+
console.log('StyledComponent: handleFocus called')
|
|
181
205
|
setIsFocused(true)
|
|
182
206
|
}
|
|
183
207
|
|
|
@@ -185,8 +209,15 @@ const StyledComponent: React.FC<StyledComponentProps> = props => {
|
|
|
185
209
|
* Handle the blur event of the input element.
|
|
186
210
|
*/
|
|
187
211
|
const handleBlur = () => {
|
|
212
|
+
console.log('StyledComponent: handleBlur called')
|
|
188
213
|
setIsFocused(false)
|
|
189
214
|
if (name && label && !hasInput && formname) {
|
|
215
|
+
console.log('StyledComponent: Calling validateField on blur', {
|
|
216
|
+
name,
|
|
217
|
+
label,
|
|
218
|
+
required,
|
|
219
|
+
formname,
|
|
220
|
+
})
|
|
190
221
|
const formData = new FormData()
|
|
191
222
|
formData.append(name, '')
|
|
192
223
|
validateField(name, formData, label, required, formname)
|
|
@@ -213,6 +244,14 @@ const StyledComponent: React.FC<StyledComponentProps> = props => {
|
|
|
213
244
|
hasInput ||
|
|
214
245
|
componentvariant === 'phonenumber'
|
|
215
246
|
|
|
247
|
+
console.log('StyledComponent: Rendering', {
|
|
248
|
+
name,
|
|
249
|
+
showError,
|
|
250
|
+
hasHelperFooter: !!currentHelperFooter,
|
|
251
|
+
helperFooterStatus: currentHelperFooter?.status,
|
|
252
|
+
helperFooterMessage: currentHelperFooter?.statusMessage,
|
|
253
|
+
})
|
|
254
|
+
|
|
216
255
|
return (
|
|
217
256
|
<Box
|
|
218
257
|
sx={{
|
|
@@ -1,76 +1,54 @@
|
|
|
1
1
|
import React, { useEffect } from 'react'
|
|
2
|
-
import { get, JSONValue } from 'goobs-cache'
|
|
3
|
-
import {
|
|
4
|
-
HelperFooterMessage,
|
|
5
|
-
useHelperFooter,
|
|
6
|
-
} from '../helperfooter/useHelperFooter'
|
|
7
|
-
|
|
8
|
-
export const useHelperFooterEffect = (
|
|
9
|
-
setHelperFooterValue: React.Dispatch<
|
|
10
|
-
React.SetStateAction<Record<string, HelperFooterMessage>>
|
|
11
|
-
>
|
|
12
|
-
) => {
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
const fetchHelperFooter = async () => {
|
|
15
|
-
const result = await get('helperFooter', 'client')
|
|
16
|
-
if (result && typeof result === 'object' && 'value' in result) {
|
|
17
|
-
setHelperFooterValue(
|
|
18
|
-
(result as JSONValue).value as Record<string, HelperFooterMessage>
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
fetchHelperFooter()
|
|
23
|
-
}, [setHelperFooterValue])
|
|
24
|
-
}
|
|
25
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook that tracks whether an input field has a value or a specific status.
|
|
5
|
+
*
|
|
6
|
+
* @param {string | undefined} value - The current value of the input field.
|
|
7
|
+
* @param {boolean | undefined} valuestatus - A boolean status associated with the input field.
|
|
8
|
+
* @param {React.Dispatch<React.SetStateAction<boolean>>} setHasInput - State setter function to update the hasInput state.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const [hasInput, setHasInput] = useState(false);
|
|
12
|
+
* useHasInputEffect(inputValue, inputStatus, setHasInput);
|
|
13
|
+
*/
|
|
26
14
|
export const useHasInputEffect = (
|
|
27
15
|
value: string | undefined,
|
|
28
16
|
valuestatus: boolean | undefined,
|
|
29
17
|
setHasInput: React.Dispatch<React.SetStateAction<boolean>>
|
|
30
18
|
) => {
|
|
31
19
|
useEffect(() => {
|
|
32
|
-
|
|
20
|
+
const hasInput = !!value || !!valuestatus
|
|
21
|
+
console.log('useHasInputEffect: Setting hasInput to', hasInput)
|
|
22
|
+
setHasInput(hasInput)
|
|
33
23
|
}, [value, valuestatus, setHasInput])
|
|
34
24
|
}
|
|
35
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Custom hook that prevents autocomplete, autocorrect, autocapitalize, and spellcheck on an input field.
|
|
28
|
+
* This is particularly useful for password fields or other sensitive inputs.
|
|
29
|
+
*
|
|
30
|
+
* @param {React.RefObject<HTMLInputElement>} inputRefInternal - React ref object for the input element.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const inputRef = useRef<HTMLInputElement>(null);
|
|
34
|
+
* usePreventAutocompleteEffect(inputRef);
|
|
35
|
+
* // In your JSX:
|
|
36
|
+
* <input ref={inputRef} ... />
|
|
37
|
+
*/
|
|
36
38
|
export const usePreventAutocompleteEffect = (
|
|
37
39
|
inputRefInternal: React.RefObject<HTMLInputElement>
|
|
38
40
|
) => {
|
|
39
41
|
useEffect(() => {
|
|
42
|
+
console.log('usePreventAutocompleteEffect: Starting effect')
|
|
40
43
|
const input = inputRefInternal.current
|
|
41
44
|
if (input) {
|
|
45
|
+
console.log('usePreventAutocompleteEffect: Setting input attributes')
|
|
42
46
|
input.setAttribute('autocomplete', 'new-password')
|
|
43
47
|
input.setAttribute('autocorrect', 'off')
|
|
44
48
|
input.setAttribute('autocapitalize', 'none')
|
|
45
49
|
input.setAttribute('spellcheck', 'false')
|
|
50
|
+
} else {
|
|
51
|
+
console.log('usePreventAutocompleteEffect: Input ref is null')
|
|
46
52
|
}
|
|
47
53
|
}, [inputRefInternal])
|
|
48
54
|
}
|
|
49
|
-
|
|
50
|
-
export const useValidateRequiredEffect = (
|
|
51
|
-
required: boolean | undefined,
|
|
52
|
-
formname: string | undefined,
|
|
53
|
-
name: string | undefined,
|
|
54
|
-
label: string | undefined
|
|
55
|
-
) => {
|
|
56
|
-
const { validateField } = useHelperFooter()
|
|
57
|
-
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
if (required && formname && name && label) {
|
|
60
|
-
const emptyFormData = new FormData()
|
|
61
|
-
emptyFormData.append(name, '')
|
|
62
|
-
validateField(name, emptyFormData, label, required, formname)
|
|
63
|
-
}
|
|
64
|
-
}, [required, formname, name, label, validateField])
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export const useShowErrorEffect = (
|
|
68
|
-
formSubmitted: boolean,
|
|
69
|
-
hasInput: boolean,
|
|
70
|
-
isFocused: boolean,
|
|
71
|
-
setShowError: React.Dispatch<React.SetStateAction<boolean>>
|
|
72
|
-
) => {
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
setShowError(formSubmitted || (hasInput && !isFocused))
|
|
75
|
-
}, [formSubmitted, hasInput, isFocused, setShowError])
|
|
76
|
-
}
|