goobs-frontend 0.8.4 → 0.8.6
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 +18 -16
- package/src/components/Button/index.tsx +0 -15
- package/src/components/Card/variants/product/index.tsx +2 -0
- package/src/components/Card/variants/productsummary/index.tsx +2 -0
- package/src/components/CodeCopy/index.tsx +2 -0
- package/src/components/Content/Structure/qrcode/useQRCode.tsx +7 -3
- package/src/components/Dropdown/index.tsx +146 -47
- package/src/components/Grid/index.tsx +3 -1
- package/src/components/Icons/FavoriteIcon.tsx +2 -0
- package/src/components/Nav/HorizontalVariant/index.tsx +15 -56
- package/src/components/PricingTable/index.tsx +60 -4
- package/src/components/QRCode/index.tsx +47 -19
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goobs-frontend",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"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
6
|
"license": "MIT",
|
|
6
7
|
"main": "./src/index.ts",
|
|
@@ -21,31 +22,32 @@
|
|
|
21
22
|
"@emotion/cache": "^11.13.1",
|
|
22
23
|
"@emotion/react": "^11.13.3",
|
|
23
24
|
"@emotion/styled": "^11.13.0",
|
|
24
|
-
"@mui/icons-material": "^6.1.
|
|
25
|
-
"@mui/material": "^6.1.
|
|
26
|
-
"@types/lodash": "^4.17.
|
|
25
|
+
"@mui/icons-material": "^6.1.4",
|
|
26
|
+
"@mui/material": "^6.1.4",
|
|
27
|
+
"@types/lodash": "^4.17.12",
|
|
27
28
|
"highlight.js": "^11.10.0",
|
|
28
|
-
"jotai": "^2.10.
|
|
29
|
+
"jotai": "^2.10.1",
|
|
29
30
|
"lodash": "^4.17.21",
|
|
30
|
-
"next": "
|
|
31
|
-
"
|
|
31
|
+
"next": "15.0.0",
|
|
32
|
+
"otplib": "^12.0.1",
|
|
33
|
+
"react-datepicker": "^7.5.0",
|
|
32
34
|
"react-qr-code": "^2.0.15"
|
|
33
35
|
},
|
|
34
36
|
"devDependencies": {
|
|
35
|
-
"@next/eslint-plugin-next": "^
|
|
36
|
-
"@types/node": "^22.7.
|
|
37
|
-
"@types/react": "18.3.
|
|
38
|
-
"@types/react-dom": "^18.3.
|
|
39
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
40
|
-
"@typescript-eslint/parser": "^8.
|
|
41
|
-
"eslint": "^9.
|
|
42
|
-
"eslint-config-next": "^
|
|
37
|
+
"@next/eslint-plugin-next": "^15.0.0",
|
|
38
|
+
"@types/node": "^22.7.8",
|
|
39
|
+
"@types/react": "18.3.11",
|
|
40
|
+
"@types/react-dom": "^18.3.1",
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^8.11.0",
|
|
42
|
+
"@typescript-eslint/parser": "^8.11.0",
|
|
43
|
+
"eslint": "^9.13.0",
|
|
44
|
+
"eslint-config-next": "^15.0.0",
|
|
43
45
|
"eslint-config-prettier": "^9.1.0",
|
|
44
46
|
"eslint-plugin-prettier": "^5.2.1",
|
|
45
47
|
"prettier": "^3.3.3",
|
|
46
48
|
"react": "^18.3.1",
|
|
47
49
|
"react-dom": "^18.3.1",
|
|
48
|
-
"typescript": "^5.6.
|
|
50
|
+
"typescript": "^5.6.3"
|
|
49
51
|
},
|
|
50
52
|
"files": [
|
|
51
53
|
"src"
|
|
@@ -19,7 +19,6 @@ export interface CustomButtonProps extends ButtonProps {
|
|
|
19
19
|
|
|
20
20
|
const CustomButton: React.FC<CustomButtonProps> = React.memo(
|
|
21
21
|
props => {
|
|
22
|
-
console.log('[trace-button] CustomButton: Rendering component', { props })
|
|
23
22
|
const {
|
|
24
23
|
text,
|
|
25
24
|
variant,
|
|
@@ -40,7 +39,6 @@ const CustomButton: React.FC<CustomButtonProps> = React.memo(
|
|
|
40
39
|
const handleButtonClick = (
|
|
41
40
|
event: React.MouseEvent<HTMLButtonElement>
|
|
42
41
|
): void => {
|
|
43
|
-
console.log('[trace-button] CustomButton: Button clicked')
|
|
44
42
|
event.preventDefault()
|
|
45
43
|
onClick?.(event)
|
|
46
44
|
}
|
|
@@ -63,13 +61,6 @@ const CustomButton: React.FC<CustomButtonProps> = React.memo(
|
|
|
63
61
|
})
|
|
64
62
|
: null
|
|
65
63
|
|
|
66
|
-
console.log('[trace-button] CustomButton: Rendering button', {
|
|
67
|
-
variant,
|
|
68
|
-
style: buttonStyle,
|
|
69
|
-
disableButton,
|
|
70
|
-
isDisabled,
|
|
71
|
-
})
|
|
72
|
-
|
|
73
64
|
const buttonContent = (
|
|
74
65
|
<>
|
|
75
66
|
{iconlocation === 'above' && IconComponent}
|
|
@@ -140,15 +131,9 @@ const CustomButton: React.FC<CustomButtonProps> = React.memo(
|
|
|
140
131
|
prevProps.iconlocation === nextProps.iconlocation &&
|
|
141
132
|
prevProps.fontlocation === nextProps.fontlocation
|
|
142
133
|
|
|
143
|
-
console.log('[trace-button] CustomButton: Props comparison', {
|
|
144
|
-
propsAreEqual,
|
|
145
|
-
prevProps: Object.keys(prevProps),
|
|
146
|
-
nextProps: Object.keys(nextProps),
|
|
147
|
-
})
|
|
148
134
|
return propsAreEqual
|
|
149
135
|
}
|
|
150
136
|
)
|
|
151
137
|
|
|
152
138
|
CustomButton.displayName = 'CustomButton'
|
|
153
|
-
console.log('[trace-button] CustomButton: Component defined')
|
|
154
139
|
export default CustomButton
|
|
@@ -6,7 +6,7 @@ type ExtendedColumnConfig = Omit<columnconfig, 'component'> & {
|
|
|
6
6
|
component?: columnconfig['component']
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export interface ExtendedQRCodeProps extends
|
|
9
|
+
export interface ExtendedQRCodeProps extends QRCodeProps {
|
|
10
10
|
columnconfig?: ExtendedColumnConfig
|
|
11
11
|
cellconfig?: cellconfig
|
|
12
12
|
}
|
|
@@ -21,9 +21,11 @@ const useQRCode = (grid: {
|
|
|
21
21
|
index: number
|
|
22
22
|
): columnconfig => {
|
|
23
23
|
const {
|
|
24
|
-
|
|
24
|
+
username,
|
|
25
|
+
appName,
|
|
25
26
|
size,
|
|
26
27
|
title,
|
|
28
|
+
onSecretGenerated,
|
|
27
29
|
columnconfig: itemColumnConfig,
|
|
28
30
|
cellconfig,
|
|
29
31
|
...restProps
|
|
@@ -48,9 +50,11 @@ const useQRCode = (grid: {
|
|
|
48
50
|
component: (
|
|
49
51
|
<QRCodeComponent
|
|
50
52
|
key={`qrcode-${index}`}
|
|
51
|
-
|
|
53
|
+
username={username}
|
|
54
|
+
appName={appName}
|
|
52
55
|
size={size}
|
|
53
56
|
title={title}
|
|
57
|
+
onSecretGenerated={onSecretGenerated}
|
|
54
58
|
{...restProps}
|
|
55
59
|
/>
|
|
56
60
|
),
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react'
|
|
2
4
|
import {
|
|
3
5
|
Select,
|
|
4
6
|
MenuItem,
|
|
@@ -6,9 +8,12 @@ import {
|
|
|
6
8
|
InputLabel,
|
|
7
9
|
SelectProps,
|
|
8
10
|
FormHelperText,
|
|
9
|
-
|
|
11
|
+
Box,
|
|
12
|
+
CircularProgress,
|
|
10
13
|
} from '@mui/material'
|
|
11
14
|
import { styled } from '@mui/material/styles'
|
|
15
|
+
import { black, white } from '../../styles/palette'
|
|
16
|
+
import Typography from '../Typography'
|
|
12
17
|
|
|
13
18
|
export interface SimpleDropdownOption {
|
|
14
19
|
value: string
|
|
@@ -38,37 +43,62 @@ export interface DropdownProps extends Omit<SelectProps, 'onChange'> {
|
|
|
38
43
|
onFocus?: SelectProps['onFocus']
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
const StyledBox = styled(Box)(() => ({
|
|
47
|
+
position: 'relative',
|
|
48
|
+
width: '100%',
|
|
49
|
+
height: '50px',
|
|
50
|
+
marginTop: '5px',
|
|
51
|
+
}))
|
|
52
|
+
|
|
41
53
|
const StyledFormControl = styled(FormControl)<{
|
|
42
54
|
backgroundcolor?: string
|
|
43
55
|
outlinecolor?: string
|
|
44
|
-
}>(({
|
|
45
|
-
|
|
56
|
+
}>(({ outlinecolor }) => ({
|
|
57
|
+
position: 'absolute',
|
|
58
|
+
top: 0,
|
|
59
|
+
left: 0,
|
|
60
|
+
right: 0,
|
|
61
|
+
bottom: 0,
|
|
46
62
|
'& .MuiOutlinedInput-root': {
|
|
47
|
-
backgroundColor:
|
|
63
|
+
backgroundColor: white.main,
|
|
64
|
+
height: '45px',
|
|
48
65
|
'& fieldset': {
|
|
49
|
-
borderColor: outlinecolor ||
|
|
66
|
+
borderColor: outlinecolor || black.main,
|
|
50
67
|
},
|
|
51
68
|
'&:hover fieldset': {
|
|
52
|
-
borderColor: outlinecolor ||
|
|
69
|
+
borderColor: outlinecolor || black.main,
|
|
53
70
|
},
|
|
54
71
|
'&.Mui-focused fieldset': {
|
|
55
|
-
borderColor: outlinecolor ||
|
|
72
|
+
borderColor: outlinecolor || black.main,
|
|
56
73
|
},
|
|
57
74
|
},
|
|
58
75
|
}))
|
|
59
76
|
|
|
60
77
|
const StyledInputLabel = styled(InputLabel)<{ shrunkfontcolor?: string }>(
|
|
61
|
-
(
|
|
78
|
+
() => ({
|
|
79
|
+
color: black.main,
|
|
62
80
|
'&.Mui-focused': {
|
|
63
|
-
color:
|
|
81
|
+
color: black.main,
|
|
82
|
+
},
|
|
83
|
+
'&.MuiInputLabel-shrink': {
|
|
84
|
+
transform: 'translate(13px, -7px) scale(0.75)',
|
|
85
|
+
color: black.main,
|
|
64
86
|
},
|
|
65
87
|
})
|
|
66
88
|
)
|
|
67
89
|
|
|
68
|
-
const StyledMenuItem = styled(MenuItem)((
|
|
90
|
+
const StyledMenuItem = styled(MenuItem)(() => ({
|
|
69
91
|
display: 'flex',
|
|
70
92
|
flexDirection: 'column',
|
|
71
93
|
alignItems: 'flex-start',
|
|
94
|
+
backgroundColor: white.main,
|
|
95
|
+
}))
|
|
96
|
+
|
|
97
|
+
const LoadingContainer = styled(Box)(() => ({
|
|
98
|
+
display: 'flex',
|
|
99
|
+
justifyContent: 'center',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
height: '50px',
|
|
72
102
|
}))
|
|
73
103
|
|
|
74
104
|
const capitalizeFirstLetter = (string: string) => {
|
|
@@ -92,23 +122,33 @@ const Dropdown: React.FC<DropdownProps> = ({
|
|
|
92
122
|
onFocus,
|
|
93
123
|
...rest
|
|
94
124
|
}) => {
|
|
95
|
-
const [selectedValue, setSelectedValue] = useState(
|
|
125
|
+
const [selectedValue, setSelectedValue] = useState<string>('')
|
|
126
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
96
129
|
const defaultOption = options.find(option => option.value === defaultValue)
|
|
97
|
-
|
|
98
|
-
|
|
130
|
+
setSelectedValue(defaultOption ? defaultOption.value : '')
|
|
131
|
+
setIsLoading(false)
|
|
132
|
+
}, [defaultValue, options])
|
|
99
133
|
|
|
100
134
|
const handleChange: SelectProps['onChange'] = (event, child) => {
|
|
101
135
|
const newValue = event.target.value as string
|
|
102
136
|
setSelectedValue(newValue)
|
|
103
|
-
onChange
|
|
137
|
+
if (onChange) {
|
|
138
|
+
onChange(event, child)
|
|
139
|
+
}
|
|
104
140
|
}
|
|
105
141
|
|
|
106
142
|
const handleBlur: SelectProps['onBlur'] = event => {
|
|
107
|
-
onBlur
|
|
143
|
+
if (onBlur) {
|
|
144
|
+
onBlur(event)
|
|
145
|
+
}
|
|
108
146
|
}
|
|
109
147
|
|
|
110
148
|
const handleFocus: SelectProps['onFocus'] = event => {
|
|
111
|
-
onFocus
|
|
149
|
+
if (onFocus) {
|
|
150
|
+
onFocus(event)
|
|
151
|
+
}
|
|
112
152
|
}
|
|
113
153
|
|
|
114
154
|
const renderMenuItem = (option: DropdownOption) => {
|
|
@@ -116,48 +156,107 @@ const Dropdown: React.FC<DropdownProps> = ({
|
|
|
116
156
|
if (!('attribute1' in option)) {
|
|
117
157
|
return (
|
|
118
158
|
<MenuItem key={option.value} value={option.value}>
|
|
119
|
-
{label}
|
|
159
|
+
<Typography fontvariant="merriparagraph" text={label} />
|
|
120
160
|
</MenuItem>
|
|
121
161
|
)
|
|
122
162
|
} else {
|
|
123
163
|
return (
|
|
124
164
|
<StyledMenuItem key={option.value} value={option.value}>
|
|
125
|
-
<Typography
|
|
126
|
-
<Typography
|
|
127
|
-
|
|
128
|
-
{option.attribute2
|
|
129
|
-
|
|
165
|
+
<Typography fontvariant="merriparagraph" text={label} />
|
|
166
|
+
<Typography
|
|
167
|
+
fontvariant="merriparagraph"
|
|
168
|
+
text={`${option.attribute1}${option.attribute2 ? ` | ${option.attribute2}` : ''}`}
|
|
169
|
+
fontcolor="textSecondary"
|
|
170
|
+
/>
|
|
130
171
|
</StyledMenuItem>
|
|
131
172
|
)
|
|
132
173
|
}
|
|
133
174
|
}
|
|
134
175
|
|
|
176
|
+
if (isLoading) {
|
|
177
|
+
return (
|
|
178
|
+
<LoadingContainer>
|
|
179
|
+
<CircularProgress size={24} />
|
|
180
|
+
</LoadingContainer>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
135
184
|
return (
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
{label}
|
|
144
|
-
</StyledInputLabel>
|
|
145
|
-
<Select
|
|
146
|
-
labelId={`${name}-label`}
|
|
147
|
-
value={selectedValue}
|
|
148
|
-
onChange={handleChange}
|
|
149
|
-
onBlur={handleBlur}
|
|
150
|
-
onFocus={handleFocus}
|
|
151
|
-
label={label}
|
|
152
|
-
sx={{ color: fontcolor }}
|
|
153
|
-
name={name}
|
|
154
|
-
aria-labelledby={`${name}-label`}
|
|
155
|
-
{...rest}
|
|
185
|
+
<StyledBox>
|
|
186
|
+
<StyledFormControl
|
|
187
|
+
backgroundcolor={backgroundcolor}
|
|
188
|
+
outlinecolor={outlinecolor}
|
|
189
|
+
error={error}
|
|
190
|
+
required={required}
|
|
191
|
+
fullWidth
|
|
156
192
|
>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
193
|
+
<StyledInputLabel
|
|
194
|
+
id={`${name}-label`}
|
|
195
|
+
shrunkfontcolor={shrunkfontcolor}
|
|
196
|
+
>
|
|
197
|
+
{label}
|
|
198
|
+
</StyledInputLabel>
|
|
199
|
+
<Select
|
|
200
|
+
labelId={`${name}-label`}
|
|
201
|
+
value={selectedValue}
|
|
202
|
+
onChange={handleChange}
|
|
203
|
+
onBlur={handleBlur}
|
|
204
|
+
onFocus={handleFocus}
|
|
205
|
+
label={label}
|
|
206
|
+
sx={{
|
|
207
|
+
color: fontcolor || black.main,
|
|
208
|
+
height: '45px',
|
|
209
|
+
backgroundColor: white.main,
|
|
210
|
+
'& .MuiSelect-select': {
|
|
211
|
+
paddingTop: '10px',
|
|
212
|
+
paddingBottom: '10px',
|
|
213
|
+
},
|
|
214
|
+
'& .MuiSvgIcon-root': {
|
|
215
|
+
color: black.main,
|
|
216
|
+
},
|
|
217
|
+
'& .MuiOutlinedInput-notchedOutline': {
|
|
218
|
+
borderColor: black.main,
|
|
219
|
+
},
|
|
220
|
+
'&:hover .MuiOutlinedInput-notchedOutline': {
|
|
221
|
+
borderColor: black.main,
|
|
222
|
+
},
|
|
223
|
+
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
|
224
|
+
borderColor: black.main,
|
|
225
|
+
},
|
|
226
|
+
'& .MuiInputBase-input': {
|
|
227
|
+
color: black.main,
|
|
228
|
+
},
|
|
229
|
+
'& .MuiInputBase-input::placeholder': {
|
|
230
|
+
color: black.main,
|
|
231
|
+
opacity: 1,
|
|
232
|
+
},
|
|
233
|
+
'& .MuiPaper-root': {
|
|
234
|
+
backgroundColor: white.main,
|
|
235
|
+
},
|
|
236
|
+
'& .MuiMenu-list': {
|
|
237
|
+
backgroundColor: white.main,
|
|
238
|
+
},
|
|
239
|
+
}}
|
|
240
|
+
MenuProps={{
|
|
241
|
+
PaperProps: {
|
|
242
|
+
sx: {
|
|
243
|
+
backgroundColor: white.main,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
}}
|
|
247
|
+
name={name}
|
|
248
|
+
aria-labelledby={`${name}-label`}
|
|
249
|
+
{...rest}
|
|
250
|
+
>
|
|
251
|
+
{options.map(renderMenuItem)}
|
|
252
|
+
</Select>
|
|
253
|
+
{helperText && (
|
|
254
|
+
<FormHelperText>
|
|
255
|
+
<Typography fontvariant="merriparagraph" text={helperText} />
|
|
256
|
+
</FormHelperText>
|
|
257
|
+
)}
|
|
258
|
+
</StyledFormControl>
|
|
259
|
+
</StyledBox>
|
|
161
260
|
)
|
|
162
261
|
}
|
|
163
262
|
|
|
@@ -247,7 +247,9 @@ const CustomGrid: React.FC<CustomGridProps> = ({
|
|
|
247
247
|
: '100%',
|
|
248
248
|
}}
|
|
249
249
|
>
|
|
250
|
-
{currentColumnConfig?.component
|
|
250
|
+
{React.isValidElement(currentColumnConfig?.component)
|
|
251
|
+
? currentColumnConfig?.component
|
|
252
|
+
: null}
|
|
251
253
|
</Box>
|
|
252
254
|
</Grid2>
|
|
253
255
|
)
|
|
@@ -2,78 +2,43 @@
|
|
|
2
2
|
import React, { useState, useEffect } from 'react'
|
|
3
3
|
import { Box, Tabs, Tab } from '@mui/material'
|
|
4
4
|
import { NavProps, SubNav, View } from '../index'
|
|
5
|
+
import { usePathname } from 'next/navigation'
|
|
5
6
|
|
|
6
|
-
/**
|
|
7
|
-
* Represents the possible alignment options for the horizontal navigation.
|
|
8
|
-
*/
|
|
9
7
|
type Alignment = 'left' | 'center' | 'right' | 'inherit' | 'justify'
|
|
10
8
|
|
|
11
|
-
/**
|
|
12
|
-
* Represents the structure of an active tab value.
|
|
13
|
-
*/
|
|
14
9
|
export interface ActiveTabValue {
|
|
15
|
-
|
|
16
|
-
tabId: string
|
|
10
|
+
tabId: string | false
|
|
17
11
|
}
|
|
18
12
|
|
|
19
|
-
/**
|
|
20
|
-
* Props for the HorizontalVariant component.
|
|
21
|
-
*/
|
|
22
13
|
export interface HorizontalVariantProps {
|
|
23
|
-
/** An array of navigation items, sub-navigation items, or views. */
|
|
24
14
|
items: (NavProps | SubNav | View)[]
|
|
25
|
-
/** The height of the navigation bar. Defaults to '80px'. */
|
|
26
15
|
height?: string
|
|
27
|
-
/** The alignment of the navigation items. Defaults to 'left'. */
|
|
28
16
|
alignment?: Alignment
|
|
29
|
-
/** A unique name for this navigation component. Used for state management. */
|
|
30
17
|
navname?: string
|
|
31
18
|
}
|
|
32
19
|
|
|
33
|
-
/**
|
|
34
|
-
* HorizontalVariant component that renders a horizontal navigation bar.
|
|
35
|
-
* It supports dynamic tab management, routing, and custom click handlers.
|
|
36
|
-
*
|
|
37
|
-
* @param {HorizontalVariantProps} props - The props for the HorizontalVariant component.
|
|
38
|
-
* @returns {JSX.Element} The rendered HorizontalVariant component.
|
|
39
|
-
*/
|
|
40
20
|
function HorizontalVariant({
|
|
41
21
|
items,
|
|
42
22
|
height = '80px',
|
|
43
23
|
alignment = 'left',
|
|
44
24
|
navname = '',
|
|
45
25
|
}: HorizontalVariantProps) {
|
|
46
|
-
/**
|
|
47
|
-
* State to keep track of active tab values for different navigation components.
|
|
48
|
-
*/
|
|
49
26
|
const [activeTabValues, setActiveTabValues] = useState<
|
|
50
27
|
Record<string, ActiveTabValue>
|
|
51
28
|
>({})
|
|
29
|
+
const pathname = usePathname()
|
|
52
30
|
|
|
53
|
-
/**
|
|
54
|
-
* Effect hook to initialize the active tab values when the component mounts.
|
|
55
|
-
*/
|
|
56
31
|
useEffect(() => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}, [items, navname, activeTabValues])
|
|
32
|
+
const currentTab = items.find(
|
|
33
|
+
item => 'orientation' in item && item.route === pathname
|
|
34
|
+
) as NavProps | undefined
|
|
35
|
+
|
|
36
|
+
setActiveTabValues(prev => ({
|
|
37
|
+
...prev,
|
|
38
|
+
[navname]: { tabId: currentTab?.title || false },
|
|
39
|
+
}))
|
|
40
|
+
}, [items, navname, pathname])
|
|
69
41
|
|
|
70
|
-
/**
|
|
71
|
-
* Handles tab change events.
|
|
72
|
-
* Updates the active tab values in the state.
|
|
73
|
-
*
|
|
74
|
-
* @param {React.SyntheticEvent} event - The event object.
|
|
75
|
-
* @param {string} newValue - The new value of the selected tab.
|
|
76
|
-
*/
|
|
77
42
|
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
|
|
78
43
|
setActiveTabValues(prev => ({
|
|
79
44
|
...prev,
|
|
@@ -81,12 +46,6 @@ function HorizontalVariant({
|
|
|
81
46
|
}))
|
|
82
47
|
}
|
|
83
48
|
|
|
84
|
-
/**
|
|
85
|
-
* Handles click events on individual tabs.
|
|
86
|
-
* Supports different trigger types: route, onClick, and routeonhorizontal.
|
|
87
|
-
*
|
|
88
|
-
* @param {NavProps} tab - The tab object that was clicked.
|
|
89
|
-
*/
|
|
90
49
|
const handleTabClick = (tab: NavProps) => {
|
|
91
50
|
if (tab.trigger === 'route') {
|
|
92
51
|
if (tab.route) {
|
|
@@ -116,7 +75,7 @@ function HorizontalVariant({
|
|
|
116
75
|
}}
|
|
117
76
|
>
|
|
118
77
|
<Tabs
|
|
119
|
-
value={activeTabValues[navname]?.tabId ||
|
|
78
|
+
value={activeTabValues[navname]?.tabId || false}
|
|
120
79
|
onChange={handleTabChange}
|
|
121
80
|
aria-label="nav tabs"
|
|
122
81
|
sx={{
|
|
@@ -136,8 +95,8 @@ function HorizontalVariant({
|
|
|
136
95
|
return (
|
|
137
96
|
<Tab
|
|
138
97
|
key={tab.title}
|
|
139
|
-
value={tab.title}
|
|
140
|
-
label={tab.title}
|
|
98
|
+
value={tab.title || ''}
|
|
99
|
+
label={tab.title || ''}
|
|
141
100
|
onClick={() => handleTabClick(tab)}
|
|
142
101
|
sx={{
|
|
143
102
|
minHeight: 0,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
|
-
import React, { useState, useCallback, useMemo } from 'react'
|
|
3
|
-
import { Box, Paper, SelectChangeEvent } from '@mui/material'
|
|
2
|
+
import React, { useState, useCallback, useMemo, useEffect } from 'react'
|
|
3
|
+
import { Box, Paper, SelectChangeEvent, CircularProgress } from '@mui/material'
|
|
4
4
|
import InfoIcon from '@mui/icons-material/Info'
|
|
5
5
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
|
6
6
|
import { Typography } from '../Typography'
|
|
@@ -72,17 +72,37 @@ export interface PricingProps {
|
|
|
72
72
|
const PricingTable: React.FC<PricingProps> = props => {
|
|
73
73
|
const router = useRouter()
|
|
74
74
|
const [selectedPackageIndex, setSelectedPackageIndex] = useState(0)
|
|
75
|
+
const [selectedPackage, setSelectedPackage] = useState('')
|
|
76
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
75
77
|
|
|
76
78
|
const config = useMemo(() => {
|
|
77
79
|
return { ...defaultConfig, ...props }
|
|
78
80
|
}, [props])
|
|
79
81
|
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const timer = setTimeout(() => {
|
|
84
|
+
setIsLoading(false)
|
|
85
|
+
}, 100) // Simulating a 100ms loading time
|
|
86
|
+
|
|
87
|
+
return () => clearTimeout(timer)
|
|
88
|
+
}, [])
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (
|
|
92
|
+
config.packagecolumns?.packagenames &&
|
|
93
|
+
config.packagecolumns.packagenames.length > 0
|
|
94
|
+
) {
|
|
95
|
+
setSelectedPackage(config.packagecolumns.packagenames[0])
|
|
96
|
+
}
|
|
97
|
+
}, [config.packagecolumns?.packagenames])
|
|
98
|
+
|
|
80
99
|
const handlePackageChange = useCallback(
|
|
81
100
|
(event: SelectChangeEvent<unknown>) => {
|
|
82
101
|
const newValue = event.target.value as string
|
|
83
102
|
const newIndex =
|
|
84
103
|
config.packagecolumns?.packagenames?.indexOf(newValue) ?? 0
|
|
85
104
|
setSelectedPackageIndex(newIndex)
|
|
105
|
+
setSelectedPackage(newValue)
|
|
86
106
|
console.log('Package selection changed to:', newValue)
|
|
87
107
|
},
|
|
88
108
|
[config.packagecolumns?.packagenames]
|
|
@@ -116,7 +136,7 @@ const PricingTable: React.FC<PricingProps> = props => {
|
|
|
116
136
|
value: name,
|
|
117
137
|
label: name,
|
|
118
138
|
}))}
|
|
119
|
-
defaultValue={
|
|
139
|
+
defaultValue={selectedPackage}
|
|
120
140
|
backgroundcolor={semiTransparentBlack.main}
|
|
121
141
|
outlinecolor={black.main}
|
|
122
142
|
fontcolor={black.main}
|
|
@@ -276,10 +296,46 @@ const PricingTable: React.FC<PricingProps> = props => {
|
|
|
276
296
|
}
|
|
277
297
|
|
|
278
298
|
return { headerColumnConfigs, featureColumnConfigs }
|
|
279
|
-
}, [
|
|
299
|
+
}, [
|
|
300
|
+
config,
|
|
301
|
+
selectedPackageIndex,
|
|
302
|
+
selectedPackage,
|
|
303
|
+
router,
|
|
304
|
+
handlePackageChange,
|
|
305
|
+
])
|
|
280
306
|
|
|
281
307
|
const { headerColumnConfigs, featureColumnConfigs } = renderColumnConfigs()
|
|
282
308
|
|
|
309
|
+
if (isLoading) {
|
|
310
|
+
return (
|
|
311
|
+
<CustomGrid
|
|
312
|
+
gridconfig={{
|
|
313
|
+
gridwidth: '100%',
|
|
314
|
+
alignment: 'center',
|
|
315
|
+
}}
|
|
316
|
+
columnconfig={[
|
|
317
|
+
{
|
|
318
|
+
row: 1,
|
|
319
|
+
column: 1,
|
|
320
|
+
alignment: 'center',
|
|
321
|
+
component: (
|
|
322
|
+
<Box
|
|
323
|
+
display="flex"
|
|
324
|
+
justifyContent="center"
|
|
325
|
+
alignItems="center"
|
|
326
|
+
height="350px"
|
|
327
|
+
width="100%"
|
|
328
|
+
overflow="auto"
|
|
329
|
+
>
|
|
330
|
+
<CircularProgress size={240} thickness={2} />
|
|
331
|
+
</Box>
|
|
332
|
+
),
|
|
333
|
+
},
|
|
334
|
+
]}
|
|
335
|
+
/>
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
|
|
283
339
|
return (
|
|
284
340
|
<Paper
|
|
285
341
|
elevation={1}
|
|
@@ -2,52 +2,58 @@ import React, { useMemo } from 'react'
|
|
|
2
2
|
import QRCode from 'react-qr-code'
|
|
3
3
|
import { Box, Typography, Paper, Theme, CircularProgress } from '@mui/material'
|
|
4
4
|
import { SxProps } from '@mui/system'
|
|
5
|
+
import { authenticator } from 'otplib'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Props for the QRCodeComponent
|
|
8
9
|
* @typedef {Object} QRCodeProps
|
|
9
|
-
* @property {string}
|
|
10
|
+
* @property {string} username - The username for the MFA setup
|
|
11
|
+
* @property {string} appName - The name of the application for MFA
|
|
10
12
|
* @property {number} [size] - The size of the QR code in pixels
|
|
11
13
|
* @property {string} [title] - An optional title to display above the QR code
|
|
12
14
|
* @property {SxProps<Theme>} [sx] - Custom styles to apply to the component
|
|
15
|
+
* @property {(secret: string) => void} [onSecretGenerated] - Callback function to receive the generated secret
|
|
13
16
|
*/
|
|
14
17
|
export interface QRCodeProps {
|
|
15
|
-
|
|
18
|
+
username: string
|
|
19
|
+
appName: string
|
|
16
20
|
size?: number
|
|
17
21
|
title?: string
|
|
18
22
|
sx?: SxProps<Theme>
|
|
23
|
+
onSecretGenerated?: (secret: string) => void
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
/**
|
|
22
|
-
* A component that displays a QR code with Material-UI styling
|
|
27
|
+
* A component that displays a QR code for MFA setup with Material-UI styling
|
|
23
28
|
* @param {QRCodeProps} props - The props for the component
|
|
24
29
|
* @returns {React.ReactElement} The rendered QR code component
|
|
25
30
|
*/
|
|
26
31
|
const QRCodeComponent: React.FC<QRCodeProps> = React.memo(
|
|
27
|
-
({
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
({ username, appName, size = 256, title, sx, onSecretGenerated }) => {
|
|
33
|
+
// Generate the secret and OTP auth URL
|
|
34
|
+
const { secret, otpAuth } = useMemo(() => {
|
|
35
|
+
const generatedSecret = authenticator.generateSecret()
|
|
36
|
+
const otpAuthUrl = authenticator.keyuri(
|
|
37
|
+
encodeURIComponent(username),
|
|
38
|
+
encodeURIComponent(appName),
|
|
39
|
+
generatedSecret
|
|
40
|
+
)
|
|
41
|
+
if (onSecretGenerated) {
|
|
42
|
+
onSecretGenerated(generatedSecret)
|
|
38
43
|
}
|
|
39
|
-
|
|
44
|
+
return { secret: generatedSecret, otpAuth: otpAuthUrl }
|
|
45
|
+
}, [username, appName, onSecretGenerated])
|
|
40
46
|
|
|
41
47
|
// Calculate responsive size
|
|
42
48
|
const responsiveSize = useMemo(() => {
|
|
43
49
|
return Math.min(size, window.innerWidth - 32) // 32px for padding
|
|
44
50
|
}, [size])
|
|
45
51
|
|
|
46
|
-
if (!
|
|
52
|
+
if (!otpAuth) {
|
|
47
53
|
return (
|
|
48
54
|
<Box sx={{ ...sx, p: 2 }} role="alert">
|
|
49
55
|
<Typography color="error">
|
|
50
|
-
Error:
|
|
56
|
+
Error: Failed to generate QR code
|
|
51
57
|
</Typography>
|
|
52
58
|
</Box>
|
|
53
59
|
)
|
|
@@ -88,13 +94,16 @@ const QRCodeComponent: React.FC<QRCodeProps> = React.memo(
|
|
|
88
94
|
}
|
|
89
95
|
>
|
|
90
96
|
<QRCode
|
|
91
|
-
value={
|
|
97
|
+
value={otpAuth}
|
|
92
98
|
size={responsiveSize}
|
|
93
99
|
style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
|
|
94
|
-
aria-label={`QR Code for ${title ||
|
|
100
|
+
aria-label={`QR Code for ${title || 'MFA Setup'}`}
|
|
95
101
|
/>
|
|
96
102
|
</React.Suspense>
|
|
97
103
|
</Box>
|
|
104
|
+
<Typography variant="body2" align="center" sx={{ mt: 2 }}>
|
|
105
|
+
Secret: {secret}
|
|
106
|
+
</Typography>
|
|
98
107
|
</Paper>
|
|
99
108
|
)
|
|
100
109
|
}
|
|
@@ -103,3 +112,22 @@ const QRCodeComponent: React.FC<QRCodeProps> = React.memo(
|
|
|
103
112
|
QRCodeComponent.displayName = 'QRCodeComponent'
|
|
104
113
|
|
|
105
114
|
export default QRCodeComponent
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Verifies a MFA token against a secret.
|
|
118
|
+
*
|
|
119
|
+
* @param token - The token to verify.
|
|
120
|
+
* @param secret - The secret key to verify against.
|
|
121
|
+
* @returns A boolean indicating whether the token is valid.
|
|
122
|
+
* @throws Error if inputs are invalid.
|
|
123
|
+
*/
|
|
124
|
+
export function verifyMFAToken(token: string, secret: string): boolean {
|
|
125
|
+
if (!token || typeof token !== 'string') {
|
|
126
|
+
throw new Error('Invalid token')
|
|
127
|
+
}
|
|
128
|
+
if (!secret || typeof secret !== 'string') {
|
|
129
|
+
throw new Error('Invalid secret')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return authenticator.verify({ token, secret })
|
|
133
|
+
}
|