goobs-frontend 0.8.7 → 0.8.8

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.8.7",
3
+ "version": "0.8.8",
4
4
  "type": "module",
5
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.",
6
6
  "license": "MIT",
@@ -1,8 +1,7 @@
1
1
  'use client'
2
- import React, { ChangeEvent, KeyboardEvent, useState } from 'react'
2
+ import React, { ChangeEvent, KeyboardEvent, useState, useCallback } from 'react'
3
3
  import { Input, Box } from '@mui/material'
4
- import { useCodeConfirmation } from './utils/useCodeConfirmation'
5
- import { columnconfig } from '../../components/Grid'
4
+ import { columnconfig } from '../Grid'
6
5
  import { red, green } from '../../styles/palette'
7
6
 
8
7
  export interface ConfirmationCodeInputsProps {
@@ -13,59 +12,138 @@ export interface ConfirmationCodeInputsProps {
13
12
  'aria-label'?: string
14
13
  'aria-required'?: boolean
15
14
  'aria-invalid'?: boolean
15
+ onChange?: (value: string) => void
16
+ value?: string
17
+ }
18
+
19
+ interface UseCodeConfirmationProps {
20
+ codeLength: number
21
+ onChange?: (value: string) => void
22
+ }
23
+
24
+ const useCodeConfirmation = ({
25
+ codeLength,
26
+ onChange,
27
+ }: UseCodeConfirmationProps) => {
28
+ const [code, setCode] = useState<Record<string, string>>(
29
+ Object.fromEntries(
30
+ Array.from({ length: codeLength }, (_, i) => [`code${i + 1}`, ''])
31
+ )
32
+ )
33
+
34
+ const handleCodeChange = useCallback(
35
+ (event: React.ChangeEvent<HTMLInputElement>, index: number) => {
36
+ const value = event.target.value.replace(/\D/g, '') // Only keep digits
37
+ if (value.length <= 1) {
38
+ // Only process if it's a single digit or empty
39
+ setCode(prevCode => {
40
+ const newCode = {
41
+ ...prevCode,
42
+ [`code${index + 1}`]: value,
43
+ }
44
+ const combinedValue = Object.values(newCode).join('')
45
+ onChange?.(combinedValue)
46
+ return newCode
47
+ })
48
+ }
49
+ },
50
+ [onChange]
51
+ )
52
+
53
+ const handleKeyDown = useCallback(
54
+ (event: React.KeyboardEvent<HTMLInputElement>, index: number) => {
55
+ // Allow only numeric keys, navigation keys, and backspace
56
+ const allowedKeys = [
57
+ 'Backspace',
58
+ 'ArrowLeft',
59
+ 'ArrowRight',
60
+ 'Tab',
61
+ 'Delete',
62
+ 'Home',
63
+ 'End',
64
+ ]
65
+
66
+ if (!allowedKeys.includes(event.key) && !/^\d$/.test(event.key)) {
67
+ event.preventDefault()
68
+ return
69
+ }
70
+
71
+ if (event.key === 'Backspace' && !code[`code${index + 1}`] && index > 0) {
72
+ setCode(prevCode => {
73
+ const newCode = {
74
+ ...prevCode,
75
+ [`code${index}`]: '',
76
+ }
77
+ const combinedValue = Object.values(newCode).join('')
78
+ onChange?.(combinedValue)
79
+ return newCode
80
+ })
81
+
82
+ const prevInput = document.querySelector(
83
+ `input[name=code${index}]`
84
+ ) as HTMLInputElement | null
85
+ if (prevInput) {
86
+ prevInput.focus()
87
+ }
88
+ } else if (event.key === 'ArrowLeft' && index > 0) {
89
+ const prevInput = document.querySelector(
90
+ `input[name=code${index}]`
91
+ ) as HTMLInputElement | null
92
+ if (prevInput) {
93
+ prevInput.focus()
94
+ }
95
+ } else if (event.key === 'ArrowRight' && index < codeLength - 1) {
96
+ const nextInput = document.querySelector(
97
+ `input[name=code${index + 2}]`
98
+ ) as HTMLInputElement | null
99
+ if (nextInput) {
100
+ nextInput.focus()
101
+ }
102
+ }
103
+ },
104
+ [code, codeLength, onChange]
105
+ )
106
+
107
+ return {
108
+ handleCodeChange,
109
+ handleKeyDown,
110
+ }
16
111
  }
17
112
 
18
- /**
19
- * ConfirmationCodeInputs component renders a set of input fields for entering a confirmation code.
20
- * It uses the useCodeConfirmation hook to handle code changes and key events.
21
- * @param props The props for the ConfirmationCodeInputs component.
22
- * @returns The rendered ConfirmationCodeInputs component.
23
- */
24
113
  const ConfirmationCodeInputs: React.FC<ConfirmationCodeInputsProps> = ({
25
114
  codeLength = 6,
26
115
  isValid,
116
+ onChange,
117
+ value,
27
118
  'aria-label': ariaLabel,
28
119
  'aria-required': ariaRequired,
29
120
  'aria-invalid': ariaInvalid,
30
121
  ...props
31
122
  }) => {
32
- const [, setVerificationCode] = useState('')
123
+ const { handleCodeChange, handleKeyDown } = useCodeConfirmation({
124
+ codeLength,
125
+ onChange,
126
+ })
33
127
 
34
- const { handleCodeChange, handleKeyDown, combinedCode } = useCodeConfirmation(
35
- {
36
- codeLength,
37
- isValid,
38
- }
39
- )
40
-
41
- /**
42
- * handleChange function is called when the value of an input field changes.
43
- * It updates the code state using the handleCodeChange function from the useCodeConfirmation hook.
44
- * If the input field has a value, it focuses on the next input field.
45
- * @param event The change event triggered by the input field.
46
- * @param index The index of the input field.
47
- */
48
128
  const handleChange = (
49
129
  event: ChangeEvent<HTMLInputElement>,
50
130
  index: number
51
131
  ) => {
52
- handleCodeChange(event, index)
53
- if (event.target.value) {
54
- const nextInput = document.querySelector(
55
- `input[name=code${index + 2}]`
56
- ) as HTMLInputElement | null
57
- if (nextInput) {
58
- nextInput.focus()
132
+ const inputValue = event.target.value.replace(/\D/g, '') // Only keep digits
133
+ if (inputValue.length <= 1) {
134
+ // Only process if it's a single digit or empty
135
+ handleCodeChange(event, index)
136
+ if (inputValue) {
137
+ const nextInput = document.querySelector(
138
+ `input[name=code${index + 2}]`
139
+ ) as HTMLInputElement | null
140
+ if (nextInput) {
141
+ nextInput.focus()
142
+ }
59
143
  }
60
144
  }
61
145
  }
62
146
 
63
- /**
64
- * handleKeyDownWrapper function is a wrapper for the handleKeyDown function from the useCodeConfirmation hook.
65
- * It is called when a key is pressed while an input field is focused.
66
- * @param event The keyboard event triggered by the input field.
67
- * @param index The index of the input field.
68
- */
69
147
  const handleKeyDownWrapper = (
70
148
  event: KeyboardEvent<HTMLInputElement>,
71
149
  index: number
@@ -73,15 +151,8 @@ const ConfirmationCodeInputs: React.FC<ConfirmationCodeInputsProps> = ({
73
151
  handleKeyDown(event, index)
74
152
  }
75
153
 
76
- /**
77
- * useEffect hook is used to set the verification code into the state using useState.
78
- * It sets the code whenever the combinedCode changes and the code is valid.
79
- */
80
- React.useEffect(() => {
81
- if (isValid) {
82
- setVerificationCode(combinedCode)
83
- }
84
- }, [combinedCode, isValid])
154
+ // Split the value into individual digits
155
+ const digits = value?.split('') || Array(codeLength).fill('')
85
156
 
86
157
  return (
87
158
  <Box
@@ -96,8 +167,11 @@ const ConfirmationCodeInputs: React.FC<ConfirmationCodeInputsProps> = ({
96
167
  <Input
97
168
  key={index}
98
169
  name={`code${index + 1}`}
170
+ value={digits[index] || ''}
99
171
  inputProps={{
100
172
  maxLength: 1,
173
+ pattern: '[0-9]*',
174
+ inputMode: 'numeric',
101
175
  'aria-label': `Code Digit ${index + 1}`,
102
176
  'aria-required': ariaRequired,
103
177
  'aria-invalid': ariaInvalid,
@@ -0,0 +1,79 @@
1
+ 'use client'
2
+
3
+ import { Checkbox } from '@mui/material'
4
+ import React from 'react'
5
+ import * as palette from '../../../styles/palette'
6
+
7
+ interface DataGridCheckboxProps {
8
+ onClick?: (event: React.MouseEvent) => void
9
+ checked?: boolean
10
+ indeterminate?: boolean
11
+ onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
12
+ disabled?: boolean
13
+ }
14
+
15
+ function DataGridCheckbox({
16
+ onClick,
17
+ checked,
18
+ indeterminate,
19
+ onChange,
20
+ disabled,
21
+ ...props
22
+ }: DataGridCheckboxProps) {
23
+ console.log('DataGridCheckbox render:', { checked, indeterminate, disabled })
24
+
25
+ const handleClick = (event: React.MouseEvent) => {
26
+ console.log('Checkbox clicked:', {
27
+ checked,
28
+ indeterminate,
29
+ eventTarget: event.target,
30
+ })
31
+
32
+ if (onClick) {
33
+ onClick(event)
34
+ }
35
+ }
36
+
37
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
38
+ event.stopPropagation()
39
+
40
+ console.log('Checkbox changed:', {
41
+ newChecked: event.target.checked,
42
+ previousChecked: checked,
43
+ indeterminate,
44
+ })
45
+
46
+ if (onChange) {
47
+ onChange(event)
48
+ }
49
+ }
50
+
51
+ return (
52
+ <Checkbox
53
+ sx={{
54
+ color: palette.marine.main,
55
+ '&.Mui-checked': {
56
+ color: palette.marine.main,
57
+ },
58
+ '&.Mui-indeterminate': {
59
+ color: palette.marine.main,
60
+ },
61
+ '&.Mui-disabled': {
62
+ color: palette.greyborder.main,
63
+ },
64
+ '&:hover': {
65
+ backgroundColor: palette.marine.light,
66
+ opacity: 0.1,
67
+ },
68
+ }}
69
+ checked={checked}
70
+ indeterminate={indeterminate}
71
+ onClick={handleClick}
72
+ onChange={handleChange}
73
+ disabled={disabled}
74
+ {...props}
75
+ />
76
+ )
77
+ }
78
+
79
+ export default DataGridCheckbox
@@ -0,0 +1,169 @@
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import { Box } from '@mui/material'
5
+ import { useAtomValue } from 'jotai'
6
+ import { columnVisibilityAtom } from '../Jotai/atom'
7
+ import { VerticalDivider } from '../VerticalDivider'
8
+ import TablePagination from '@mui/material/TablePagination'
9
+ import ManageColumn from '../ManageColumn'
10
+ import { ColumnDef } from '../Table'
11
+ import CustomButton from '../../Button'
12
+ import ShowHideEyeIcon from '@/components/Icons/ShowHideEye'
13
+
14
+ export interface CustomFooterProps {
15
+ page: number
16
+ pageSize: number
17
+ rowCount: number
18
+ onPageChange: (newPage: number) => void
19
+ onPageSizeChange: (newPageSize: number) => void
20
+ columns: ColumnDef[]
21
+ }
22
+
23
+ function CustomFooter({
24
+ page,
25
+ pageSize,
26
+ rowCount,
27
+ onPageChange,
28
+ onPageSizeChange,
29
+ columns,
30
+ }: CustomFooterProps) {
31
+ const [isOpen, setIsOpen] = useState(false)
32
+ const [checkboxWidth] = useState(45)
33
+ const columnVisibility = useAtomValue(columnVisibilityAtom)
34
+
35
+ const handleOpen = () => {
36
+ console.log('Footer handleOpen')
37
+ setIsOpen(true)
38
+ }
39
+
40
+ const handleClose = () => {
41
+ console.log('Footer handleClose')
42
+ setIsOpen(false)
43
+ }
44
+
45
+ // Calculate total width based on visible columns only
46
+ const totalWidth = columns.reduce((sum, col) => {
47
+ if (columnVisibility[col.field] !== false) {
48
+ return sum + (col.width || 150)
49
+ }
50
+ return sum
51
+ }, 0)
52
+
53
+ const totalPages = Math.ceil(rowCount / pageSize)
54
+
55
+ return (
56
+ <Box
57
+ className="custom-footer-container"
58
+ sx={{
59
+ width: `${totalWidth}px`,
60
+ minWidth: '100%',
61
+ height: '56px',
62
+ position: 'sticky',
63
+ left: 0,
64
+ marginLeft: `${checkboxWidth}px`,
65
+ }}
66
+ >
67
+ <Box
68
+ sx={{
69
+ display: 'flex',
70
+ justifyContent: 'space-between',
71
+ alignItems: 'center',
72
+ flexWrap: 'nowrap',
73
+ width: '100%',
74
+ height: '100%',
75
+ px: 2,
76
+ }}
77
+ >
78
+ <Box
79
+ sx={{
80
+ display: 'flex',
81
+ alignItems: 'center',
82
+ }}
83
+ className="left-box"
84
+ >
85
+ <Box sx={{ display: 'flex', alignItems: 'center', mr: '10px' }}>
86
+ <VerticalDivider />
87
+ </Box>
88
+ <Box
89
+ sx={{
90
+ display: 'flex',
91
+ alignItems: 'center',
92
+ gap: '8px',
93
+ pr: '8px',
94
+ }}
95
+ >
96
+ <CustomButton
97
+ onClick={handleOpen}
98
+ text="Manage Columns"
99
+ fontvariant="merriparagraph"
100
+ fontcolor="black"
101
+ icon={<ShowHideEyeIcon visible={true} />}
102
+ iconcolor="black"
103
+ iconlocation="left"
104
+ disableButton="false"
105
+ sx={{
106
+ minWidth: 'unset',
107
+ padding: '8px',
108
+ '& .MuiTypography-root': {
109
+ marginLeft: '16px',
110
+ },
111
+ }}
112
+ />
113
+ </Box>
114
+ <Box
115
+ sx={{
116
+ display: 'flex',
117
+ alignItems: 'center',
118
+ ml: '10px',
119
+ mr: '10px',
120
+ }}
121
+ >
122
+ <VerticalDivider />
123
+ </Box>
124
+ </Box>
125
+ <Box
126
+ sx={{
127
+ display: 'flex',
128
+ alignItems: 'center',
129
+ ml: 'auto',
130
+ }}
131
+ className="right-box"
132
+ >
133
+ <TablePagination
134
+ component="div"
135
+ count={rowCount}
136
+ page={page}
137
+ onPageChange={(_, newPage) => {
138
+ onPageChange(newPage)
139
+ }}
140
+ rowsPerPage={pageSize}
141
+ onRowsPerPageChange={event => {
142
+ const newPageSize = parseInt(event.target.value, 10)
143
+ onPageSizeChange(newPageSize)
144
+ }}
145
+ rowsPerPageOptions={[10, 25, 50, 100]}
146
+ slotProps={{
147
+ actions: {
148
+ previousButton: {
149
+ disabled: page === 0,
150
+ },
151
+ nextButton: {
152
+ disabled: page >= totalPages - 1,
153
+ },
154
+ },
155
+ }}
156
+ showFirstButton
157
+ showLastButton
158
+ labelDisplayedRows={({ from, to, count }) =>
159
+ `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
160
+ }
161
+ />
162
+ </Box>
163
+ </Box>
164
+ <ManageColumn open={isOpen} handleClose={handleClose} columns={columns} />
165
+ </Box>
166
+ )
167
+ }
168
+
169
+ export default CustomFooter
@@ -0,0 +1,91 @@
1
+ import { atom } from 'jotai'
2
+ import { atomWithStorage } from 'jotai/utils'
3
+
4
+ interface ColumnVisibility {
5
+ [key: string]: boolean
6
+ }
7
+
8
+ // Create a persistent atom that saves to localStorage
9
+ export const columnVisibilityAtom = atomWithStorage<ColumnVisibility>(
10
+ 'columnVisibility',
11
+ {}
12
+ )
13
+
14
+ // Atom for managing initial column setup
15
+ export const columnsAtom = atom<string[]>([])
16
+
17
+ // Actions atom for updating visibility
18
+ export const columnVisibilityActions = atom(
19
+ null,
20
+ (
21
+ get,
22
+ set,
23
+ update: {
24
+ type: 'toggle' | 'setAll' | 'reset' | 'save'
25
+ field?: string
26
+ value?: boolean
27
+ newState?: ColumnVisibility
28
+ }
29
+ ) => {
30
+ const currentVisibility = get(columnVisibilityAtom)
31
+ const columns = get(columnsAtom)
32
+ let newVisibility: ColumnVisibility = {}
33
+
34
+ console.log('columnVisibilityActions - before:', {
35
+ type: update.type,
36
+ currentVisibility,
37
+ })
38
+
39
+ switch (update.type) {
40
+ case 'toggle': {
41
+ if (update.field) {
42
+ newVisibility = {
43
+ ...currentVisibility,
44
+ [update.field]: !currentVisibility[update.field],
45
+ }
46
+ console.log('columnVisibilityActions - toggle:', {
47
+ field: update.field,
48
+ before: currentVisibility[update.field],
49
+ after: newVisibility[update.field],
50
+ })
51
+ set(columnVisibilityAtom, newVisibility)
52
+ }
53
+ break
54
+ }
55
+
56
+ case 'setAll': {
57
+ columns.forEach(column => {
58
+ newVisibility[column] = !!update.value
59
+ })
60
+ console.log('columnVisibilityActions - setAll:', {
61
+ value: update.value,
62
+ newState: newVisibility,
63
+ })
64
+ set(columnVisibilityAtom, newVisibility)
65
+ break
66
+ }
67
+
68
+ case 'save': {
69
+ if (update.newState) {
70
+ console.log('columnVisibilityActions - save:', {
71
+ before: currentVisibility,
72
+ after: update.newState,
73
+ })
74
+ set(columnVisibilityAtom, update.newState)
75
+ }
76
+ break
77
+ }
78
+
79
+ case 'reset': {
80
+ columns.forEach(column => {
81
+ newVisibility[column] = true
82
+ })
83
+ console.log('columnVisibilityActions - reset:', newVisibility)
84
+ set(columnVisibilityAtom, newVisibility)
85
+ break
86
+ }
87
+ }
88
+
89
+ console.log('columnVisibilityActions - after:', get(columnVisibilityAtom))
90
+ }
91
+ )