goobs-frontend 0.7.61 → 0.7.64

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goobs-frontend",
3
- "version": "0.7.61",
3
+ "version": "0.7.64",
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",
@@ -28,7 +28,7 @@
28
28
  "@mui/icons-material": "^5.16.0",
29
29
  "@mui/material": "^5.16.0",
30
30
  "@types/lodash": "^4.17.6",
31
- "goobs-cache": "^1.3.2",
31
+ "goobs-cache": "^1.4.0",
32
32
  "highlight.js": "^11.10.0",
33
33
  "lodash": "^4.17.21",
34
34
  "next": "14.2.5"
@@ -0,0 +1,251 @@
1
+ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
2
+ import { get } from 'goobs-cache'
3
+
4
+ /**
5
+ * Represents the structure of a helper footer message.
6
+ * @interface HelperFooterMessage
7
+ */
8
+ export interface HelperFooterMessage {
9
+ /** The status of the message, either 'error' or 'success'. */
10
+ status: 'error' | 'success'
11
+ /** A message describing the status. */
12
+ statusMessage: string
13
+ /** A message to be displayed to the user. */
14
+ spreadMessage: string
15
+ /** A number indicating the priority of the message. Lower numbers indicate higher priority. */
16
+ spreadMessagePriority: number
17
+ /** The name of the form associated with this message. */
18
+ formname: string
19
+ /** Indicates whether this message is required to be addressed. */
20
+ required: boolean
21
+ }
22
+
23
+ /**
24
+ * A type definition for the interval ID returned by setInterval.
25
+ */
26
+ type IntervalID = ReturnType<typeof setInterval>
27
+
28
+ /**
29
+ * A custom hook for managing helper footer messages and form validation.
30
+ *
31
+ * @param {string} [initialFormname] - The initial name of the form to fetch helper footers for.
32
+ * @returns {Object} An object containing the current error message, form validity state, and functions to update and fetch form validation.
33
+ */
34
+ const useHelperFooter = (initialFormname?: string) => {
35
+ /**
36
+ * State for storing the current error message.
37
+ */
38
+ const [errorMessage, setErrorMessage] = useState<string | undefined>(
39
+ undefined
40
+ )
41
+
42
+ /**
43
+ * State for storing the current form validity.
44
+ */
45
+ const [isFormValid, setIsFormValid] = useState<boolean>(true)
46
+
47
+ /**
48
+ * State for storing the current helper footers.
49
+ */
50
+ const [helperFooters, setHelperFooters] = useState<HelperFooterMessage[]>([])
51
+
52
+ /**
53
+ * Ref for storing the previous helper footers to compare against.
54
+ */
55
+ const prevHelperFooters = useRef<HelperFooterMessage[]>([])
56
+
57
+ /**
58
+ * Ref for storing the previous error message to compare against.
59
+ */
60
+ const prevErrorMessage = useRef<string | undefined>(undefined)
61
+
62
+ /**
63
+ * Ref for storing the previous form validity to compare against.
64
+ */
65
+ const prevIsFormValid = useRef<boolean>(true)
66
+
67
+ /**
68
+ * Ref for storing the interval ID for periodic helper footer fetching.
69
+ */
70
+ const intervalIdRef = useRef<IntervalID | null>(null)
71
+
72
+ /**
73
+ * Fetches helper footer messages from the cache.
74
+ *
75
+ * @param {string} [formname] - The name of the form to fetch helper footers for.
76
+ * @returns {Promise<HelperFooterMessage[]>} A promise that resolves to an array of HelperFooterMessage objects.
77
+ */
78
+ const fetchHelperFooters = useCallback(
79
+ async (formname?: string): Promise<HelperFooterMessage[]> => {
80
+ const currentFormname = formname || initialFormname
81
+ if (!currentFormname) {
82
+ return []
83
+ }
84
+
85
+ const helperFooterResult = await get(
86
+ 'helperfooter',
87
+ currentFormname,
88
+ 'client'
89
+ )
90
+
91
+ if (
92
+ helperFooterResult &&
93
+ typeof helperFooterResult === 'object' &&
94
+ 'type' in helperFooterResult &&
95
+ helperFooterResult.type === 'json' &&
96
+ 'value' in helperFooterResult &&
97
+ typeof helperFooterResult.value === 'object' &&
98
+ helperFooterResult.value !== null
99
+ ) {
100
+ const fetchedHelperFooters = Object.entries(
101
+ helperFooterResult.value as Record<string, unknown>
102
+ )
103
+ .map(([key, value]): HelperFooterMessage | null => {
104
+ if (
105
+ typeof value === 'object' &&
106
+ value !== null &&
107
+ 'status' in value &&
108
+ 'statusMessage' in value &&
109
+ 'spreadMessage' in value &&
110
+ 'spreadMessagePriority' in value &&
111
+ 'required' in value
112
+ ) {
113
+ return {
114
+ status: value.status as 'error' | 'success',
115
+ statusMessage: String(value.statusMessage),
116
+ spreadMessage: String(value.spreadMessage),
117
+ spreadMessagePriority: Number(value.spreadMessagePriority),
118
+ required: Boolean(value.required),
119
+ formname: key,
120
+ }
121
+ }
122
+ return null
123
+ })
124
+ .filter((value): value is HelperFooterMessage => value !== null)
125
+
126
+ if (
127
+ JSON.stringify(fetchedHelperFooters) !==
128
+ JSON.stringify(prevHelperFooters.current)
129
+ ) {
130
+ setHelperFooters(fetchedHelperFooters)
131
+ prevHelperFooters.current = fetchedHelperFooters
132
+ }
133
+ return fetchedHelperFooters
134
+ }
135
+
136
+ if (helperFooters.length > 0) {
137
+ setHelperFooters([])
138
+ prevHelperFooters.current = []
139
+ }
140
+ return []
141
+ },
142
+ [initialFormname, helperFooters]
143
+ )
144
+
145
+ /**
146
+ * Updates the form validation state based on the fetched helper footers.
147
+ *
148
+ * @param {string} [formname] - The name of the form to update validation for.
149
+ * @returns {Promise<boolean>} A promise that resolves to a boolean indicating whether the form is valid.
150
+ */
151
+ const updateFormValidation = useCallback(
152
+ async (formname?: string): Promise<boolean> => {
153
+ const fetchedHelperFooters = await fetchHelperFooters(formname)
154
+
155
+ if (fetchedHelperFooters.length === 0) {
156
+ setErrorMessage(undefined)
157
+ setIsFormValid(true)
158
+ return true
159
+ }
160
+
161
+ const errorFooters = fetchedHelperFooters.filter(
162
+ footer => footer.status === 'error' && footer.required
163
+ )
164
+
165
+ if (errorFooters.length > 0) {
166
+ const highestPriorityError = errorFooters.reduce((prev, current) =>
167
+ prev.spreadMessagePriority < current.spreadMessagePriority
168
+ ? prev
169
+ : current
170
+ )
171
+ setErrorMessage(highestPriorityError.spreadMessage)
172
+ setIsFormValid(false)
173
+ return false
174
+ }
175
+
176
+ setErrorMessage(undefined)
177
+ setIsFormValid(true)
178
+ return true
179
+ },
180
+ [fetchHelperFooters]
181
+ )
182
+
183
+ /**
184
+ * Effect to run form validation when the initial form name changes.
185
+ */
186
+ useEffect(() => {
187
+ void updateFormValidation()
188
+ }, [initialFormname, updateFormValidation])
189
+
190
+ /**
191
+ * Effect to set up periodic helper footer fetching.
192
+ */
193
+ useEffect(() => {
194
+ if (initialFormname) {
195
+ const fetchAndUpdateHelperFooters = async () => {
196
+ await updateFormValidation(initialFormname)
197
+ }
198
+
199
+ void fetchAndUpdateHelperFooters()
200
+ intervalIdRef.current = setInterval(fetchAndUpdateHelperFooters, 1000)
201
+
202
+ return () => {
203
+ if (intervalIdRef.current) {
204
+ clearInterval(intervalIdRef.current)
205
+ }
206
+ }
207
+ }
208
+ }, [initialFormname, updateFormValidation])
209
+
210
+ /**
211
+ * Effect to update refs when error message or form validity changes.
212
+ */
213
+ useEffect(() => {
214
+ if (
215
+ errorMessage !== prevErrorMessage.current ||
216
+ isFormValid !== prevIsFormValid.current
217
+ ) {
218
+ prevErrorMessage.current = errorMessage
219
+ prevIsFormValid.current = isFormValid
220
+ }
221
+ }, [errorMessage, isFormValid])
222
+
223
+ /**
224
+ * Memoized helper footers to prevent unnecessary re-renders.
225
+ */
226
+ const memoizedHelperFooters = useMemo(() => helperFooters, [helperFooters])
227
+
228
+ /**
229
+ * Memoized return value of the hook to prevent unnecessary re-renders.
230
+ */
231
+ const returnValue = useMemo(
232
+ () => ({
233
+ errorMessage,
234
+ isFormValid,
235
+ updateFormValidation,
236
+ fetchHelperFooters,
237
+ helperFooters: memoizedHelperFooters,
238
+ }),
239
+ [
240
+ errorMessage,
241
+ isFormValid,
242
+ updateFormValidation,
243
+ fetchHelperFooters,
244
+ memoizedHelperFooters,
245
+ ]
246
+ )
247
+
248
+ return returnValue
249
+ }
250
+
251
+ export default useHelperFooter
@@ -1,30 +1,32 @@
1
- 'use client'
2
-
3
- import React, { useEffect, useState, useCallback } from 'react'
1
+ import React, { useMemo, useCallback } from 'react'
4
2
  import { Button, Box, ButtonProps } from '@mui/material'
5
3
  import StarIcon from '@mui/icons-material/Star'
6
4
  import Typography from '../Typography'
7
- import { get, JSONValue } from 'goobs-cache'
8
5
  import { red } from '../../styles/palette'
6
+ import useHelperFooter from './hook/useHelperFooter'
9
7
 
8
+ /**
9
+ * Defines the possible alignment options for the button text.
10
+ */
10
11
  export type ButtonAlignment = 'left' | 'center' | 'right'
11
12
 
12
- export interface HelperFooterMessage {
13
- status: 'error' | 'success'
14
- statusMessage: string
15
- spreadMessage: string
16
- spreadMessagePriority: number
17
- formname: string
18
- required: boolean
19
- }
20
-
13
+ /**
14
+ * Interface for the CustomButton component props.
15
+ * Extends ButtonProps from Material-UI, omitting 'color' and 'variant'.
16
+ */
21
17
  export interface CustomButtonProps
22
18
  extends Omit<ButtonProps, 'color' | 'variant'> {
19
+ /** The text to display on the button */
23
20
  text?: string
21
+ /** The background color of the button */
24
22
  backgroundcolor?: string
23
+ /** The outline color of the button */
25
24
  outlinecolor?: string
25
+ /** The font color of the button text */
26
26
  fontcolor?: string
27
+ /** The alignment of the button text */
27
28
  fontlocation?: ButtonAlignment
29
+ /** The variant of the font to use for the button text */
28
30
  fontvariant?:
29
31
  | 'arapeyh1'
30
32
  | 'arapeyh2'
@@ -53,18 +55,32 @@ export interface CustomButtonProps
53
55
  | 'merriparagraph'
54
56
  | 'merrihelperheader'
55
57
  | 'merrihelperfooter'
58
+ /** The icon to display on the button */
56
59
  icon?: React.ReactNode | false
60
+ /** The color of the icon */
57
61
  iconcolor?: string
62
+ /** The size of the icon */
58
63
  iconsize?: string
64
+ /** The location of the icon relative to the text */
59
65
  iconlocation?: 'left' | 'top' | 'right'
66
+ /** The variant of the button */
60
67
  variant?: 'text' | 'outlined' | 'contained'
68
+ /** The function to call when the button is clicked */
61
69
  onClick?: () => void
62
- helperfooter?: HelperFooterMessage
70
+ /** The width of the button */
63
71
  width?: string
72
+ /** The name of the form associated with this button */
64
73
  formname?: string
74
+ /** The name attribute of the button */
65
75
  name?: string
66
76
  }
67
77
 
78
+ /**
79
+ * CustomButton component that extends Material-UI's Button with additional styling and functionality.
80
+ *
81
+ * @param props - The props for the CustomButton component
82
+ * @returns A React functional component
83
+ */
68
84
  const CustomButton: React.FC<CustomButtonProps> = props => {
69
85
  const {
70
86
  text,
@@ -85,78 +101,15 @@ const CustomButton: React.FC<CustomButtonProps> = props => {
85
101
  width,
86
102
  } = props
87
103
 
88
- const [errorMessage, setErrorMessage] = useState<string | undefined>(
89
- undefined
90
- )
91
- const [isFormValid, setIsFormValid] = useState<boolean>(true)
92
- const [helperFooterValue, setHelperFooterValue] = useState<
93
- Record<string, HelperFooterMessage>
94
- >({})
95
-
96
- const updateFormValidation = useCallback(() => {
97
- if (formname && helperFooterValue) {
98
- const relevantFooters = Object.values(helperFooterValue).filter(
99
- footer => footer?.formname === formname
100
- )
101
-
102
- const errorFooters = relevantFooters.filter(
103
- footer => footer?.status === 'error'
104
- )
104
+ const { errorMessage, isFormValid, updateFormValidation } =
105
+ useHelperFooter(formname)
105
106
 
106
- const emptyRequiredFields = relevantFooters.filter(
107
- footer =>
108
- footer?.required && (!footer.status || footer.status === 'error')
109
- )
110
-
111
- if (errorFooters.length > 0) {
112
- const highestPriorityError = errorFooters.reduce((prev, current) =>
113
- (prev.spreadMessagePriority || Infinity) <
114
- (current.spreadMessagePriority || Infinity)
115
- ? prev
116
- : current
117
- )
118
- setErrorMessage(highestPriorityError.spreadMessage)
119
- setIsFormValid(false)
120
- } else if (emptyRequiredFields.length > 0) {
121
- setErrorMessage('Please fill in all required fields.')
122
- setIsFormValid(false)
123
- } else {
124
- setErrorMessage(undefined)
125
- setIsFormValid(true)
126
- }
127
- } else {
128
- setErrorMessage(undefined)
129
- setIsFormValid(true)
130
- }
131
- }, [formname, helperFooterValue])
132
-
133
- useEffect(() => {
134
- const fetchHelperFooter = async () => {
135
- const helperFooterResult = await get('helperFooter', 'client')
136
- if (
137
- helperFooterResult &&
138
- typeof helperFooterResult === 'object' &&
139
- 'value' in helperFooterResult
140
- ) {
141
- setHelperFooterValue(
142
- (helperFooterResult as JSONValue).value as Record<
143
- string,
144
- HelperFooterMessage
145
- >
146
- )
147
- } else {
148
- setHelperFooterValue({})
149
- }
150
- }
151
-
152
- fetchHelperFooter()
153
- }, [formname])
154
-
155
- useEffect(() => {
156
- updateFormValidation()
157
- }, [updateFormValidation])
158
-
159
- const renderIcon = () => {
107
+ /**
108
+ * Renders the icon for the button based on the provided props.
109
+ *
110
+ * @returns {React.ReactNode} The rendered icon or null
111
+ */
112
+ const renderIcon = useCallback((): React.ReactNode => {
160
113
  if (icon === false) {
161
114
  return null
162
115
  }
@@ -166,17 +119,80 @@ const CustomButton: React.FC<CustomButtonProps> = props => {
166
119
  })
167
120
  }
168
121
  return <StarIcon style={{ fontSize: iconsize }} />
169
- }
122
+ }, [icon, iconsize])
123
+
124
+ /**
125
+ * Handles the button click event. Prevents default behavior, validates the form,
126
+ * and calls the onClick prop if the form is valid.
127
+ *
128
+ * @param event - The mouse event from clicking the button
129
+ */
130
+ const handleButtonClick = useCallback(
131
+ async (event: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
132
+ event.preventDefault()
133
+ const validationResult = await updateFormValidation(formname)
134
+ if (validationResult && onClick) {
135
+ onClick()
136
+ }
137
+ },
138
+ [updateFormValidation, onClick, formname]
139
+ )
170
140
 
171
- const handleButtonClick = async () => {
172
- if (!isFormValid) {
173
- return
174
- }
141
+ /**
142
+ * Memoized style object for the button.
143
+ */
144
+ const buttonStyle = useMemo(
145
+ () => ({
146
+ minWidth: text ? 'auto' : 'fit-content',
147
+ paddingLeft: text ? '8px' : '0',
148
+ paddingRight: text ? '8px' : '0',
149
+ justifyContent: fontlocation || 'center',
150
+ backgroundColor: backgroundcolor,
151
+ border: outlinecolor ? `1px solid ${outlinecolor}` : undefined,
152
+ color: iconcolor,
153
+ width: width,
154
+ }),
155
+ [text, fontlocation, backgroundcolor, outlinecolor, iconcolor, width]
156
+ )
175
157
 
176
- if (onClick) {
177
- onClick()
178
- }
179
- }
158
+ /**
159
+ * Memoized content for the button, including icon and text.
160
+ */
161
+ const buttonContent = useMemo(
162
+ () => (
163
+ <Box display="flex" alignItems="center">
164
+ {iconlocation === 'left' && renderIcon()}
165
+ {text && (
166
+ <Typography
167
+ fontvariant={fontvariant}
168
+ fontcolor={fontcolor}
169
+ text={text}
170
+ />
171
+ )}
172
+ {iconlocation === 'right' && renderIcon()}
173
+ </Box>
174
+ ),
175
+ [iconlocation, renderIcon, text, fontvariant, fontcolor]
176
+ )
177
+
178
+ /**
179
+ * Memoized error message component that displays when the form is invalid.
180
+ */
181
+ const errorMessageComponent = useMemo(
182
+ () =>
183
+ !isFormValid && errorMessage ? (
184
+ <Typography
185
+ fontvariant="merrihelperfooter"
186
+ fontcolor={red.main}
187
+ text={errorMessage}
188
+ marginTop={0.5}
189
+ marginBottom={0}
190
+ align="center"
191
+ width="100%"
192
+ />
193
+ ) : null,
194
+ [errorMessage, isFormValid]
195
+ )
180
196
 
181
197
  return (
182
198
  <Box
@@ -193,42 +209,13 @@ const CustomButton: React.FC<CustomButtonProps> = props => {
193
209
  type={type}
194
210
  name={name}
195
211
  onClick={handleButtonClick}
196
- style={{
197
- minWidth: text ? 'auto' : 'fit-content',
198
- paddingLeft: text ? '8px' : '0',
199
- paddingRight: text ? '8px' : '0',
200
- justifyContent: fontlocation || 'center',
201
- backgroundColor: backgroundcolor,
202
- border: outlinecolor ? `1px solid ${outlinecolor}` : undefined,
203
- color: iconcolor,
204
- width: width,
205
- }}
212
+ style={buttonStyle}
206
213
  >
207
- <Box display="flex" alignItems="center">
208
- {iconlocation === 'left' && renderIcon()}
209
- {text && (
210
- <Typography
211
- fontvariant={fontvariant}
212
- fontcolor={fontcolor}
213
- text={text}
214
- />
215
- )}
216
- {iconlocation === 'right' && renderIcon()}
217
- </Box>
214
+ {buttonContent}
218
215
  </Button>
219
- {errorMessage && (
220
- <Typography
221
- fontvariant="merrihelperfooter"
222
- fontcolor={red.main}
223
- text={errorMessage}
224
- marginTop={0.5}
225
- marginBottom={0}
226
- align="center"
227
- width="100%"
228
- />
229
- )}
216
+ {errorMessageComponent}
230
217
  </Box>
231
218
  )
232
219
  }
233
220
 
234
- export default CustomButton
221
+ export default React.memo(CustomButton)
@@ -80,8 +80,8 @@ const ConfirmationCodeInputs: React.FC<ConfirmationCodeInputsProps> = ({
80
80
  if (isValid) {
81
81
  set(
82
82
  'verificationCode',
83
- combinedCode,
84
- new Date(Date.now() + 3600000),
83
+ 'codeStore',
84
+ { type: 'string', value: combinedCode },
85
85
  'client'
86
86
  )
87
87
  }
@@ -33,7 +33,6 @@ const useButton = (grid: {
33
33
  iconlocation,
34
34
  variant,
35
35
  onClick,
36
- helperfooter,
37
36
  columnconfig: itemColumnConfig,
38
37
  cellconfig,
39
38
  ...restProps
@@ -72,7 +71,6 @@ const useButton = (grid: {
72
71
  iconlocation={iconlocation}
73
72
  variant={variant}
74
73
  onClick={onClick}
75
- helperfooter={helperfooter}
76
74
  {...restProps}
77
75
  />
78
76
  ),
@@ -65,16 +65,16 @@ export interface ContentSectionProps {
65
65
  card?: ExtendedCardProps | ExtendedCardProps[]
66
66
  codecopy?: ExtendedCodeCopyProps | ExtendedCodeCopyProps[]
67
67
  }>
68
+ width?: number
68
69
  }
69
70
 
70
71
  /**
71
72
  * RenderContent component handles the rendering of various content elements
72
73
  * based on the provided configuration.
73
74
  */
74
- const RenderContent: React.FC<ContentSectionProps['grids'][0]> = ({
75
- grid,
76
- ...props
77
- }) => {
75
+ const RenderContent: React.FC<
76
+ ContentSectionProps['grids'][0] & { width?: number }
77
+ > = ({ grid, width, ...props }) => {
78
78
  let columnConfigs: columnconfig[] = []
79
79
 
80
80
  // Helper function to add configurations to columnConfigs
@@ -104,20 +104,26 @@ const RenderContent: React.FC<ContentSectionProps['grids'][0]> = ({
104
104
  addToColumnConfigs(useCard(props))
105
105
  addToColumnConfigs(useCodeCopy(props))
106
106
 
107
+ const updatedGridConfig: gridconfig = {
108
+ ...grid.gridconfig,
109
+ gridwidth: width ? `${width}px` : grid.gridconfig?.gridwidth,
110
+ }
111
+
107
112
  return (
108
- <CustomGrid gridconfig={grid.gridconfig} columnconfig={columnConfigs} />
113
+ <CustomGrid gridconfig={updatedGridConfig} columnconfig={columnConfigs} />
109
114
  )
110
115
  }
111
116
 
112
117
  /**
113
118
  * ContentSection component renders multiple grids based on the provided configuration.
114
119
  * @param grids An array of ContentSectionProps, each representing a grid to be rendered.
120
+ * @param width Optional width for the content section, defaults to 450px if not provided.
115
121
  */
116
- export default function ContentSection({ grids }: ContentSectionProps) {
122
+ export default function ContentSection({ grids, width }: ContentSectionProps) {
117
123
  return (
118
124
  <>
119
125
  {grids.map((gridProps, index) => (
120
- <RenderContent key={index} {...gridProps} />
126
+ <RenderContent key={index} {...gridProps} width={width} />
121
127
  ))}
122
128
  </>
123
129
  )