goobs-frontend 0.7.67 → 0.7.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +14 -16
  2. package/src/app/_app.tsx +8 -8
  3. package/src/components/Button/index.tsx +95 -203
  4. package/src/components/ConfirmationCodeInput/index.tsx +4 -6
  5. package/src/components/Content/Structure/button/useButton.tsx +1 -35
  6. package/src/components/Content/Structure/datefield/useDateField.tsx +55 -0
  7. package/src/components/Content/Structure/dropdown/useDropdown.tsx +55 -0
  8. package/src/components/Content/Structure/incremementNumberField/useIncremementNumberField.tsx +65 -0
  9. package/src/components/Content/Structure/numberField/useNumberField.tsx +55 -0
  10. package/src/components/Content/Structure/passwordField/usePasswordField.tsx +57 -0
  11. package/src/components/Content/Structure/phoneNumber/usePhoneNumber.tsx +0 -0
  12. package/src/components/Content/Structure/qrcode/useQRCode.tsx +69 -0
  13. package/src/components/Content/Structure/searchbar/useSearchbar.tsx +55 -0
  14. package/src/components/Content/Structure/textfield/useTextField.tsx +84 -0
  15. package/src/components/Content/index.tsx +44 -7
  16. package/src/components/DateField/index.tsx +112 -0
  17. package/src/components/Dropdown/index.tsx +91 -0
  18. package/src/components/Form/Popup/index.tsx +1 -1
  19. package/src/components/IncrementNumberField/index.tsx +123 -0
  20. package/src/components/Nav/HorizontalVariant/index.tsx +18 -12
  21. package/src/components/Nav/VerticalVariant/index.tsx +14 -10
  22. package/src/components/NumberField/index.tsx +95 -0
  23. package/src/components/PasswordField/index.tsx +105 -0
  24. package/src/components/PhoneNumberField/index.tsx +102 -0
  25. package/src/components/PricingTable/index.tsx +10 -8
  26. package/src/components/QRCode/index.tsx +105 -0
  27. package/src/components/Searchbar/index.tsx +77 -0
  28. package/src/components/TextField/index.tsx +130 -0
  29. package/src/components/Toolbar/index.tsx +11 -10
  30. package/src/index.ts +54 -9
  31. package/src/components/Button/hook/useHelperFooter.tsx +0 -214
  32. package/src/components/Content/Structure/styledcomponent/useStyledComponent.tsx +0 -104
  33. package/src/components/StyledComponent/adornment/index.tsx +0 -125
  34. package/src/components/StyledComponent/hooks/useDropdown.tsx +0 -150
  35. package/src/components/StyledComponent/hooks/useInputHelperFooter.tsx +0 -524
  36. package/src/components/StyledComponent/hooks/usePhoneNumber.tsx +0 -99
  37. package/src/components/StyledComponent/hooks/useRequiredFieldsValidator.tsx +0 -190
  38. package/src/components/StyledComponent/hooks/useSearchbar.tsx +0 -46
  39. package/src/components/StyledComponent/hooks/useSplitButton.tsx +0 -70
  40. package/src/components/StyledComponent/index.tsx +0 -473
  41. package/src/components/StyledComponent/useCallbacks/index.tsx +0 -46
  42. package/src/styles/StyledComponent/Label/index.ts +0 -76
@@ -0,0 +1,123 @@
1
+ 'use client'
2
+ import React, { useState, useCallback } from 'react'
3
+ import { TextField, Button, Box, TextFieldProps } from '@mui/material'
4
+ import { styled } from '@mui/material/styles'
5
+
6
+ export interface IncrementNumberFieldProps
7
+ extends Omit<TextFieldProps, 'onChange'> {
8
+ initialValue?: string
9
+ onChange?: () => void
10
+ backgroundcolor?: string
11
+ outlinecolor?: string
12
+ fontcolor?: string
13
+ label?: string
14
+ }
15
+
16
+ const StyledTextField = styled(TextField)<{
17
+ backgroundcolor?: string
18
+ outlinecolor?: string
19
+ fontcolor?: string
20
+ }>(({ theme, backgroundcolor, outlinecolor, fontcolor }) => ({
21
+ '& .MuiOutlinedInput-root': {
22
+ backgroundColor: backgroundcolor || theme.palette.background.paper,
23
+ '& fieldset': {
24
+ borderColor: outlinecolor || theme.palette.primary.main,
25
+ },
26
+ '&:hover fieldset': {
27
+ borderColor: outlinecolor || theme.palette.primary.main,
28
+ },
29
+ '&.Mui-focused fieldset': {
30
+ borderColor: outlinecolor || theme.palette.primary.main,
31
+ },
32
+ },
33
+ '& .MuiInputLabel-root': {
34
+ color: fontcolor || theme.palette.text.primary,
35
+ '&.Mui-focused': {
36
+ color: fontcolor || theme.palette.primary.main,
37
+ },
38
+ },
39
+ '& .MuiInputBase-input': {
40
+ color: fontcolor || theme.palette.text.primary,
41
+ textAlign: 'center',
42
+ padding: '8px 0',
43
+ },
44
+ }))
45
+
46
+ const StyledButton = styled(Button)(({ theme }) => ({
47
+ minWidth: '36px',
48
+ padding: 0,
49
+ height: '100%',
50
+ borderRadius: '4px',
51
+ backgroundColor: theme.palette.grey[300],
52
+ color: theme.palette.text.primary,
53
+ '&:hover': {
54
+ backgroundColor: theme.palette.grey[400],
55
+ },
56
+ }))
57
+
58
+ const IncrementNumberField: React.FC<IncrementNumberFieldProps> = ({
59
+ initialValue = '0',
60
+ onChange,
61
+ backgroundcolor,
62
+ outlinecolor,
63
+ fontcolor,
64
+ label,
65
+ ...rest
66
+ }) => {
67
+ const [value, setValue] = useState(initialValue)
68
+
69
+ const handleIncrement = useCallback(() => {
70
+ setValue(prev => {
71
+ const num = parseInt(prev)
72
+ if (isNaN(num)) {
73
+ return '0'
74
+ }
75
+ const newValue = (num + 1).toString()
76
+ onChange?.()
77
+ return newValue
78
+ })
79
+ }, [onChange])
80
+
81
+ const handleDecrement = useCallback(() => {
82
+ setValue(prev => {
83
+ const num = parseInt(prev)
84
+ if (isNaN(num)) {
85
+ return '0'
86
+ }
87
+ const newValue = Math.max(0, num - 1).toString()
88
+ onChange?.()
89
+ return newValue
90
+ })
91
+ }, [onChange])
92
+
93
+ const handleChange = useCallback(
94
+ (event: React.ChangeEvent<HTMLInputElement>) => {
95
+ const numValue = event.target.value.replace(/[^0-9]/g, '')
96
+ const newValue = numValue === '' ? '0' : numValue
97
+ setValue(newValue)
98
+ onChange?.()
99
+ },
100
+ [onChange]
101
+ )
102
+
103
+ return (
104
+ <Box display="flex" alignItems="center">
105
+ <StyledButton onClick={handleDecrement}>-</StyledButton>
106
+ <StyledTextField
107
+ value={value}
108
+ onChange={handleChange}
109
+ backgroundcolor={backgroundcolor}
110
+ outlinecolor={outlinecolor}
111
+ fontcolor={fontcolor}
112
+ label={label}
113
+ variant="outlined"
114
+ size="small"
115
+ inputProps={{ style: { width: '40px' } }}
116
+ {...rest}
117
+ />
118
+ <StyledButton onClick={handleIncrement}>+</StyledButton>
119
+ </Box>
120
+ )
121
+ }
122
+
123
+ export default IncrementNumberField
@@ -1,7 +1,6 @@
1
1
  'use client'
2
- import React, { useEffect } from 'react'
2
+ import React, { useState, useEffect } from 'react'
3
3
  import { Box, Tabs, Tab } from '@mui/material'
4
- import { session } from 'goobs-cache'
5
4
  import { NavProps, SubNav, View } from '../index'
6
5
 
7
6
  /**
@@ -42,24 +41,31 @@ function HorizontalVariant({
42
41
  items,
43
42
  height = '80px',
44
43
  alignment = 'left',
45
- navname,
44
+ navname = '',
46
45
  }: HorizontalVariantProps) {
47
46
  /**
48
47
  * State to keep track of active tab values for different navigation components.
49
48
  */
50
- const activeTabValuesAtom = session.atom<
51
- Record<string, ActiveTabValue | null>
49
+ const [activeTabValues, setActiveTabValues] = useState<
50
+ Record<string, ActiveTabValue>
52
51
  >({})
53
- const [activeTabValues, setActiveTabValues] =
54
- session.useAtom(activeTabValuesAtom)
55
52
 
56
53
  /**
57
54
  * Effect hook to initialize the active tab values when the component mounts.
58
55
  */
59
56
  useEffect(() => {
60
- // The initialization is now handled by the session atom
61
- // No need to fetch from cache as it's automatically handled by goobs-cache
62
- }, [])
57
+ if (!activeTabValues[navname]) {
58
+ const firstTab = items.find(item => 'orientation' in item) as
59
+ | NavProps
60
+ | undefined
61
+ if (firstTab && firstTab.title) {
62
+ setActiveTabValues(prev => ({
63
+ ...prev,
64
+ [navname]: { tabId: firstTab.title as string },
65
+ }))
66
+ }
67
+ }
68
+ }, [items, navname, activeTabValues])
63
69
 
64
70
  /**
65
71
  * Handles tab change events.
@@ -71,7 +77,7 @@ function HorizontalVariant({
71
77
  const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
72
78
  setActiveTabValues(prev => ({
73
79
  ...prev,
74
- [navname ?? '']: { tabId: newValue },
80
+ [navname]: { tabId: newValue },
75
81
  }))
76
82
  }
77
83
 
@@ -110,7 +116,7 @@ function HorizontalVariant({
110
116
  }}
111
117
  >
112
118
  <Tabs
113
- value={activeTabValues?.[navname ?? '']?.tabId || false}
119
+ value={activeTabValues[navname]?.tabId || ''}
114
120
  onChange={handleTabChange}
115
121
  aria-label="nav tabs"
116
122
  sx={{
@@ -12,7 +12,8 @@ import {
12
12
  AccordionDetails,
13
13
  List,
14
14
  } from '@mui/material'
15
- import StyledComponent from '../../StyledComponent'
15
+ import Dropdown from '../../Dropdown'
16
+ import Searchbar from '../../Searchbar'
16
17
  import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
17
18
  import Link from 'next/link'
18
19
  import { useRouter } from 'next/navigation'
@@ -390,26 +391,29 @@ function VerticalVariant({
390
391
  {(showDropdown || showSearchbar) && (
391
392
  <Stack mt={1} spacing={1}>
392
393
  {showDropdown && (
393
- <StyledComponent
394
+ <Dropdown
394
395
  label={dropdownLabel}
395
- componentvariant="dropdown"
396
- outlinecolor="none"
397
396
  options={navOptions}
398
397
  backgroundcolor={white.main}
399
- shrunklabellocation="above"
398
+ outlinecolor="none"
399
+ fontcolor={black.main}
400
400
  shrunkfontcolor={white.main}
401
- unshrunkfontcolor={black.main}
401
+ onChange={() => {
402
+ console.log('Dropdown selection changed')
403
+ }}
402
404
  />
403
405
  )}
404
406
  {showSearchbar && (
405
- <StyledComponent
406
- componentvariant="searchbar"
407
+ <Searchbar
407
408
  label={searchbarLabel}
408
409
  backgroundcolor={semiTransparentWhite.main}
409
410
  iconcolor={white.main}
410
411
  outlinecolor="none"
411
- shrunklabellocation="onnotch"
412
- combinedfontcolor={white.main}
412
+ fontcolor={white.main}
413
+ placeholder="Search..."
414
+ onChange={() => {
415
+ console.log('Search value changed')
416
+ }}
413
417
  />
414
418
  )}
415
419
  </Stack>
@@ -0,0 +1,95 @@
1
+ 'use client'
2
+ import React, { useState, useCallback } from 'react'
3
+ import { TextField, TextFieldProps } from '@mui/material'
4
+ import { styled } from '@mui/material/styles'
5
+
6
+ export interface NumberFieldProps extends Omit<TextFieldProps, 'onChange'> {
7
+ initialValue?: string
8
+ onChange?: () => void
9
+ backgroundcolor?: string
10
+ outlinecolor?: string
11
+ fontcolor?: string
12
+ label?: string
13
+ min?: number
14
+ max?: number
15
+ }
16
+
17
+ const StyledTextField = styled(TextField)<{
18
+ backgroundcolor?: string
19
+ outlinecolor?: string
20
+ fontcolor?: string
21
+ }>(({ theme, backgroundcolor, outlinecolor, fontcolor }) => ({
22
+ '& .MuiOutlinedInput-root': {
23
+ backgroundColor: backgroundcolor || theme.palette.background.paper,
24
+ '& fieldset': {
25
+ borderColor: outlinecolor || theme.palette.primary.main,
26
+ },
27
+ '&:hover fieldset': {
28
+ borderColor: outlinecolor || theme.palette.primary.main,
29
+ },
30
+ '&.Mui-focused fieldset': {
31
+ borderColor: outlinecolor || theme.palette.primary.main,
32
+ },
33
+ },
34
+ '& .MuiInputLabel-root': {
35
+ color: fontcolor || theme.palette.text.primary,
36
+ '&.Mui-focused': {
37
+ color: fontcolor || theme.palette.primary.main,
38
+ },
39
+ },
40
+ '& .MuiInputBase-input': {
41
+ color: fontcolor || theme.palette.text.primary,
42
+ },
43
+ }))
44
+
45
+ const NumberField: React.FC<NumberFieldProps> = ({
46
+ initialValue = '',
47
+ onChange,
48
+ backgroundcolor,
49
+ outlinecolor,
50
+ fontcolor,
51
+ label,
52
+ min,
53
+ max,
54
+ ...rest
55
+ }) => {
56
+ const [value, setValue] = useState(initialValue)
57
+
58
+ const handleChange = useCallback(
59
+ (event: React.ChangeEvent<HTMLInputElement>) => {
60
+ const newValue = event.target.value.replace(/[^0-9]/g, '')
61
+ if (newValue === '') {
62
+ setValue('')
63
+ onChange?.()
64
+ return
65
+ }
66
+ const numValue = parseInt(newValue, 10)
67
+ if (min !== undefined && numValue < min) {
68
+ setValue(min.toString())
69
+ } else if (max !== undefined && numValue > max) {
70
+ setValue(max.toString())
71
+ } else {
72
+ setValue(newValue)
73
+ }
74
+ onChange?.()
75
+ },
76
+ [onChange, min, max]
77
+ )
78
+
79
+ return (
80
+ <StyledTextField
81
+ value={value}
82
+ onChange={handleChange}
83
+ backgroundcolor={backgroundcolor}
84
+ outlinecolor={outlinecolor}
85
+ fontcolor={fontcolor}
86
+ label={label}
87
+ variant="outlined"
88
+ type="text"
89
+ inputMode="numeric"
90
+ {...rest}
91
+ />
92
+ )
93
+ }
94
+
95
+ export default NumberField
@@ -0,0 +1,105 @@
1
+ 'use client'
2
+ import React, { useState, useCallback } from 'react'
3
+ import { TextField, TextFieldProps, InputAdornment } from '@mui/material'
4
+ import { styled } from '@mui/material/styles'
5
+ import ShowHideEyeIcon from '../Icons/ShowHideEye'
6
+
7
+ export interface PasswordFieldProps extends Omit<TextFieldProps, 'type'> {
8
+ backgroundcolor?: string
9
+ outlinecolor?: string
10
+ fontcolor?: string
11
+ label?: string
12
+ }
13
+
14
+ const StyledTextField = styled(TextField)<{
15
+ backgroundcolor?: string
16
+ outlinecolor?: string
17
+ fontcolor?: string
18
+ }>(({ theme, backgroundcolor, outlinecolor, fontcolor }) => ({
19
+ '& .MuiOutlinedInput-root': {
20
+ backgroundColor: backgroundcolor || theme.palette.background.paper,
21
+ '& fieldset': {
22
+ borderColor: outlinecolor || theme.palette.primary.main,
23
+ },
24
+ '&:hover fieldset': {
25
+ borderColor: outlinecolor || theme.palette.primary.main,
26
+ },
27
+ '&.Mui-focused fieldset': {
28
+ borderColor: outlinecolor || theme.palette.primary.main,
29
+ },
30
+ },
31
+ '& .MuiInputLabel-root': {
32
+ color: fontcolor || theme.palette.text.primary,
33
+ '&.Mui-focused': {
34
+ color: fontcolor || theme.palette.primary.main,
35
+ },
36
+ },
37
+ '& .MuiInputBase-input': {
38
+ color: fontcolor || theme.palette.text.primary,
39
+ },
40
+ }))
41
+
42
+ interface AdornmentProps {
43
+ componentvariant: string
44
+ passwordVisible?: boolean
45
+ togglePasswordVisibility?: () => void
46
+ }
47
+
48
+ const EndAdornment: React.FC<AdornmentProps> = ({
49
+ componentvariant,
50
+ passwordVisible,
51
+ togglePasswordVisibility,
52
+ }) => {
53
+ if (componentvariant === 'password') {
54
+ return (
55
+ <InputAdornment
56
+ position="end"
57
+ onClick={togglePasswordVisibility}
58
+ style={{ cursor: 'pointer' }}
59
+ >
60
+ <ShowHideEyeIcon visible={passwordVisible} />
61
+ </InputAdornment>
62
+ )
63
+ }
64
+ return null
65
+ }
66
+
67
+ const PasswordField: React.FC<PasswordFieldProps> = ({
68
+ backgroundcolor,
69
+ outlinecolor,
70
+ fontcolor,
71
+ label = 'Password',
72
+ ...rest
73
+ }) => {
74
+ const [passwordVisible, setPasswordVisible] = useState(false)
75
+
76
+ const togglePasswordVisibility = useCallback(() => {
77
+ setPasswordVisible(prev => {
78
+ console.log('togglePasswordVisibility', { passwordVisible: !prev })
79
+ return !prev
80
+ })
81
+ }, [])
82
+
83
+ return (
84
+ <StyledTextField
85
+ type={passwordVisible ? 'text' : 'password'}
86
+ label={label}
87
+ backgroundcolor={backgroundcolor}
88
+ outlinecolor={outlinecolor}
89
+ fontcolor={fontcolor}
90
+ fullWidth
91
+ InputProps={{
92
+ endAdornment: (
93
+ <EndAdornment
94
+ componentvariant="password"
95
+ passwordVisible={passwordVisible}
96
+ togglePasswordVisibility={togglePasswordVisibility}
97
+ />
98
+ ),
99
+ }}
100
+ {...rest}
101
+ />
102
+ )
103
+ }
104
+
105
+ export default PasswordField
@@ -0,0 +1,102 @@
1
+ 'use client'
2
+ import React, { useState, useCallback } from 'react'
3
+ import { TextField, TextFieldProps } from '@mui/material'
4
+ import { styled } from '@mui/material/styles'
5
+
6
+ interface PhoneNumberFieldProps extends Omit<TextFieldProps, 'onChange'> {
7
+ initialValue?: string
8
+ onChange?: () => void
9
+ backgroundcolor?: string
10
+ outlinecolor?: string
11
+ fontcolor?: string
12
+ label?: string
13
+ }
14
+
15
+ const StyledTextField = styled(TextField)<{
16
+ backgroundcolor?: string
17
+ outlinecolor?: string
18
+ fontcolor?: string
19
+ }>(({ theme, backgroundcolor, outlinecolor, fontcolor }) => ({
20
+ '& .MuiOutlinedInput-root': {
21
+ backgroundColor: backgroundcolor || theme.palette.background.paper,
22
+ '& fieldset': {
23
+ borderColor: outlinecolor || theme.palette.primary.main,
24
+ },
25
+ '&:hover fieldset': {
26
+ borderColor: outlinecolor || theme.palette.primary.main,
27
+ },
28
+ '&.Mui-focused fieldset': {
29
+ borderColor: outlinecolor || theme.palette.primary.main,
30
+ },
31
+ },
32
+ '& .MuiInputLabel-root': {
33
+ color: fontcolor || theme.palette.text.primary,
34
+ '&.Mui-focused': {
35
+ color: fontcolor || theme.palette.primary.main,
36
+ },
37
+ },
38
+ '& .MuiInputBase-input': {
39
+ color: fontcolor || theme.palette.text.primary,
40
+ },
41
+ }))
42
+
43
+ const formatPhoneNumber = (value: string): string => {
44
+ const digits = value.replace(/\D/g, '')
45
+ const limitedDigits = digits.slice(0, 10)
46
+ let formattedNumber = '+1 '
47
+ if (limitedDigits.length > 0) {
48
+ formattedNumber += limitedDigits.slice(0, 3)
49
+ if (limitedDigits.length > 3) {
50
+ formattedNumber += '-' + limitedDigits.slice(3, 6)
51
+ if (limitedDigits.length > 6) {
52
+ formattedNumber += '-' + limitedDigits.slice(6)
53
+ }
54
+ }
55
+ }
56
+ return formattedNumber.trim()
57
+ }
58
+
59
+ const PhoneNumberField: React.FC<PhoneNumberFieldProps> = ({
60
+ initialValue = '',
61
+ onChange,
62
+ backgroundcolor,
63
+ outlinecolor,
64
+ fontcolor,
65
+ label = 'Phone Number',
66
+ ...rest
67
+ }) => {
68
+ const [phoneNumber, setPhoneNumber] = useState(
69
+ formatPhoneNumber(initialValue)
70
+ )
71
+
72
+ const handlePhoneNumberChange = useCallback(
73
+ (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
74
+ const input = e.target.value
75
+ let strippedInput = input.replace(/^\+1\s?/, '').replace(/\D/g, '')
76
+ strippedInput = strippedInput.slice(0, 10)
77
+ const formattedValue =
78
+ strippedInput.length > 0 ? formatPhoneNumber(strippedInput) : '+1 '
79
+ setPhoneNumber(formattedValue)
80
+ onChange?.()
81
+ },
82
+ [onChange]
83
+ )
84
+
85
+ return (
86
+ <StyledTextField
87
+ label={label}
88
+ value={phoneNumber}
89
+ onChange={handlePhoneNumberChange}
90
+ backgroundcolor={backgroundcolor}
91
+ outlinecolor={outlinecolor}
92
+ fontcolor={fontcolor}
93
+ fullWidth
94
+ inputProps={{
95
+ maxLength: 16,
96
+ }}
97
+ {...rest}
98
+ />
99
+ )
100
+ }
101
+
102
+ export default PhoneNumberField
@@ -6,7 +6,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle'
6
6
  import { Typography } from '../Typography'
7
7
  import StyledTooltip from '../Tooltip'
8
8
  import CustomButton from '../Button'
9
- import StyledComponent from '../StyledComponent'
9
+ import Dropdown from '../Dropdown'
10
10
  import CustomGrid from './../../components/Grid'
11
11
  import { columnconfig, gridconfig } from './../../components/Grid/'
12
12
  import defaultConfig from './defaultconfig'
@@ -117,15 +117,17 @@ const PricingTable: React.FC<PricingProps> = props => {
117
117
  headerColumnConfigs.push({
118
118
  ...config.packagecolumns.columnconfig,
119
119
  component: (
120
- <StyledComponent
120
+ <Dropdown
121
121
  label="Packages"
122
- shrunklabellocation="above"
123
- componentvariant="dropdown"
124
- shrunkfontcolor={black.main}
125
- outlinecolor={black.main}
126
- backgroundcolor={semiTransparentBlack.main}
127
- defaultOption="ThothOS"
128
122
  options={['ThothOS', 'ThothOS Pro', 'ThothOS Enterprise']}
123
+ defaultOption="ThothOS"
124
+ backgroundcolor={semiTransparentBlack.main}
125
+ outlinecolor={black.main}
126
+ fontcolor={black.main}
127
+ shrunkfontcolor={black.main}
128
+ onChange={() => {
129
+ console.log('Package selection changed')
130
+ }}
129
131
  />
130
132
  ),
131
133
  })
@@ -0,0 +1,105 @@
1
+ import React, { useMemo } from 'react'
2
+ import QRCode from 'react-qr-code'
3
+ import { Box, Typography, Paper, Theme, CircularProgress } from '@mui/material'
4
+ import { SxProps } from '@mui/system'
5
+
6
+ /**
7
+ * Props for the QRCodeComponent
8
+ * @typedef {Object} QRCodeProps
9
+ * @property {string} value - The value to be encoded in the QR code
10
+ * @property {number} [size] - The size of the QR code in pixels
11
+ * @property {string} [title] - An optional title to display above the QR code
12
+ * @property {SxProps<Theme>} [sx] - Custom styles to apply to the component
13
+ */
14
+ export interface QRCodeProps {
15
+ value: string
16
+ size?: number
17
+ title?: string
18
+ sx?: SxProps<Theme>
19
+ }
20
+
21
+ /**
22
+ * A component that displays a QR code with Material-UI styling
23
+ * @param {QRCodeProps} props - The props for the component
24
+ * @returns {React.ReactElement} The rendered QR code component
25
+ */
26
+ const QRCodeComponent: React.FC<QRCodeProps> = React.memo(
27
+ ({ value, size = 256, title, sx }) => {
28
+ // Validate the QR code value
29
+ const isValidValue = useMemo(() => {
30
+ if (!value) return false
31
+ try {
32
+ // Check if the value is a valid URL
33
+ new URL(value)
34
+ return true
35
+ } catch {
36
+ // If not a URL, check if it's a non-empty string
37
+ return typeof value === 'string' && value.trim().length > 0
38
+ }
39
+ }, [value])
40
+
41
+ // Calculate responsive size
42
+ const responsiveSize = useMemo(() => {
43
+ return Math.min(size, window.innerWidth - 32) // 32px for padding
44
+ }, [size])
45
+
46
+ if (!isValidValue) {
47
+ return (
48
+ <Box sx={{ ...sx, p: 2 }} role="alert">
49
+ <Typography color="error">
50
+ Error: Invalid or empty QR code value
51
+ </Typography>
52
+ </Box>
53
+ )
54
+ }
55
+
56
+ return (
57
+ <Paper
58
+ elevation={3}
59
+ sx={{
60
+ ...sx,
61
+ p: 3,
62
+ display: 'inline-block',
63
+ maxWidth: '100%',
64
+ boxSizing: 'border-box',
65
+ }}
66
+ >
67
+ {title && (
68
+ <Typography variant="h6" gutterBottom align="center">
69
+ {title}
70
+ </Typography>
71
+ )}
72
+ <Box
73
+ sx={{
74
+ display: 'flex',
75
+ justifyContent: 'center',
76
+ alignItems: 'center',
77
+ width: responsiveSize,
78
+ height: responsiveSize,
79
+ margin: 'auto',
80
+ }}
81
+ >
82
+ <React.Suspense
83
+ fallback={
84
+ <CircularProgress
85
+ size={responsiveSize / 4}
86
+ aria-label="Loading QR Code"
87
+ />
88
+ }
89
+ >
90
+ <QRCode
91
+ value={value}
92
+ size={responsiveSize}
93
+ style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
94
+ aria-label={`QR Code for ${title || value}`}
95
+ />
96
+ </React.Suspense>
97
+ </Box>
98
+ </Paper>
99
+ )
100
+ }
101
+ )
102
+
103
+ QRCodeComponent.displayName = 'QRCodeComponent'
104
+
105
+ export default QRCodeComponent