@campxdev/shared 1.11.29 → 1.11.30

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": "@campxdev/shared",
3
- "version": "1.11.29",
3
+ "version": "1.11.30",
4
4
  "main": "./exports.ts",
5
5
  "scripts": {
6
6
  "start": "react-scripts start",
@@ -13,7 +13,7 @@
13
13
  "lint:fix": "eslint --fix \"./src/**/*.{js,jsx,ts,tsx,json}\"",
14
14
  "format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,css,md,json}\" --config ./.prettierrc",
15
15
  "storybook": "start-storybook -p 6006 -s public",
16
- "build-storybook": "build-storybook -s public"
16
+ "build-storybook": "cross-env NODE_OPTIONS=--openssl-legacy-provider build-storybook"
17
17
  },
18
18
  "browserslist": {
19
19
  "production": [
@@ -91,6 +91,7 @@
91
91
  "storybook-addon-react-router-v6": "^0.2.1"
92
92
  },
93
93
  "resolutions": {
94
- "react-dev-utils/fork-ts-checker-webpack-plugin": "^6.5.3"
94
+ "react-dev-utils/fork-ts-checker-webpack-plugin": "^6.5.3",
95
+ "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.cd77847.0"
95
96
  }
96
97
  }
@@ -1,145 +1,209 @@
1
1
  import {
2
2
  Timeline,
3
- TimelineConnector,
4
3
  TimelineContent,
5
4
  TimelineItem,
6
5
  TimelineSeparator,
7
6
  } from '@mui/lab'
8
7
  import { Box, SxProps, Typography } from '@mui/material'
9
- import { useQuery } from 'react-query'
10
- import axios from '../../config/axios'
8
+ import axios from 'axios'
9
+ import { useCallback, useRef } from 'react'
10
+ import { useInfiniteQuery } from 'react-query'
11
11
  import { useErrorModal } from '../ErrorModalWrapper/ErrorModalWrapper'
12
- import { PageContent } from '../PageContent'
13
- import PageHeader from '../PageHeader'
14
12
  import Spinner from '../Spinner'
15
13
  import Table from '../Tables/BasicTable/Table'
16
- import { StyledAvatar, StyledCircleIcon, StyledTimeLineDot } from './Styles'
17
-
18
- export const getData = async (url: string) => {
19
- return await axios.get(url).then((res) => res.data)
20
- }
14
+ import {
15
+ StyledAvatar,
16
+ StyledCircleIcon,
17
+ StyledSpinnerBox,
18
+ StyledTimeLineDot,
19
+ } from './Styles'
21
20
 
22
- export interface Activity {
23
- id: number
24
- date: string
25
- time: string
26
- message: string
27
- userName: string
21
+ interface Props {
22
+ endPoint: string
23
+ tableView: boolean
24
+ sx?: SxProps
28
25
  }
29
26
 
30
- const ActivityLog = ({
31
- activities,
32
- endPoint,
33
- tableView,
34
- title,
35
- sx,
36
- }: {
37
- activities: Activity[]
38
- sx?: SxProps
39
- endPoint?: string
40
- title?: string
41
- tableView: boolean
42
- }) => {
27
+ export default function ActivityLog({ endPoint, tableView, sx }: Props) {
43
28
  const errorModal = useErrorModal()
44
- const { data, isLoading } = useQuery('activity', () => getData(endPoint), {
45
- onError: (error) => {
29
+ const fetchActivities = async ({ pageParam = 0 }) => {
30
+ try {
31
+ const response = await axios.get(endPoint, {
32
+ params: {
33
+ limit: 4,
34
+ skip: pageParam,
35
+ },
36
+ })
37
+ return response.data.posts
38
+ } catch (error) {
46
39
  // eslint-disable-next-line no-console
47
40
  console.log(error)
48
41
  errorModal({ error: error })
49
- },
50
- })
51
-
52
- if (isLoading) {
53
- return <Spinner />
42
+ }
54
43
  }
55
44
 
45
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
46
+ useInfiniteQuery('activities', fetchActivities, {
47
+ getNextPageParam: (lastPage) => {
48
+ return lastPage.length ? lastPage[lastPage.length - 1].id : null
49
+ },
50
+ })
51
+
52
+ const activitesData = data ? data.pages.flatMap((page) => page) : []
53
+
54
+ if (isLoading) return <Spinner />
55
+
56
+ return (
57
+ <Box>
58
+ <Typography
59
+ variant="h1"
60
+ sx={{ fontSize: '18px', fontWeight: 700, margin: '20px' }}
61
+ >
62
+ Activity Log
63
+ </Typography>
64
+
65
+ {!tableView ? (
66
+ <Box
67
+ sx={{
68
+ overflowY: 'scroll',
69
+ '&::-webkit-scrollbar': {
70
+ display: 'none',
71
+ },
72
+ height: '380px',
73
+ }}
74
+ >
75
+ <TimeLineComponent
76
+ activitesData={activitesData}
77
+ fetchNextPage={fetchNextPage}
78
+ isFetchingNextPage={isFetchingNextPage}
79
+ hasNextPage={hasNextPage}
80
+ sx={sx}
81
+ />
82
+ </Box>
83
+ ) : (
84
+ <Box sx={{ margin: '0 25px' }}>
85
+ <Table
86
+ columns={columns}
87
+ dataSource={activitesData}
88
+ loading={isLoading}
89
+ />
90
+ </Box>
91
+ )}
92
+ </Box>
93
+ )
94
+ }
95
+
96
+ const columns = [
97
+ { title: 'Date', dataIndex: 'date', key: 'date' },
98
+ { title: 'Time', dataIndex: 'time', key: 'time' },
99
+ { title: 'Message', dataIndex: 'message', key: 'message' },
100
+ { title: 'User', dataIndex: 'userName', key: 'userName' },
101
+ ]
102
+
103
+ export const TimeLineComponent = ({
104
+ activitesData,
105
+ isFetchingNextPage,
106
+ hasNextPage,
107
+ fetchNextPage,
108
+ sx,
109
+ }) => {
110
+ const lastItemRef = useIntersectionObserver<HTMLElement>(() => {
111
+ if (!isFetchingNextPage && hasNextPage) fetchNextPage()
112
+ })
113
+
56
114
  return (
57
- <>
58
- <PageHeader title={title ?? 'Activity Log'} />
59
- <PageContent>
60
- {!tableView ? (
61
- <Box
115
+ <Timeline sx={{ padding: 0, margin: 0 }}>
116
+ {activitesData.map((item, index, items) => (
117
+ <Box
118
+ key={index}
119
+ ref={items.length - 1 === index ? lastItemRef : null}
120
+ sx={{ maxWidth: '550px', ...sx }}
121
+ >
122
+ <TimelineItem
62
123
  sx={{
63
- padding: '20px',
64
- maxWidth: '550px',
65
- ...sx,
124
+ margin: 2,
125
+ padding: 0,
126
+ '&:before': { display: 'none' },
66
127
  }}
67
128
  >
68
- <Timeline>
69
- {activities.map((item, index) => (
70
- <TimelineItem
71
- key={index}
129
+ <TimelineSeparator>
130
+ <StyledTimeLineDot>
131
+ <StyledCircleIcon />
132
+ </StyledTimeLineDot>
133
+ {index < activitesData.length - 1 && (
134
+ <Box
72
135
  sx={{
73
- margin: 0,
74
- padding: 0,
75
- '&:before': { flex: 'none' },
136
+ width: '1px',
137
+ height: '110px',
138
+ bgcolor: '#12121233',
139
+ position: 'absolute',
140
+ top: '25px',
141
+ }}
142
+ />
143
+ )}
144
+ </TimelineSeparator>
145
+ <TimelineContent sx={{ padding: '6px 10px' }}>
146
+ <Box>
147
+ <Typography variant="subtitle2" sx={{ fontSize: '14px' }}>
148
+ {`${Math.floor(Math.random() * 30) + 1} ${
149
+ Math.random() > 0.5 ? 'June' : 'July'
150
+ }, 2023`}{' '}
151
+ -{' '}
152
+ {`${Math.floor(Math.random() * 12) + 1}:${
153
+ Math.floor(Math.random() * 60) + 1
154
+ } ${Math.random() > 0.5 ? 'AM' : 'PM'}`}
155
+ {/* {item.date} - {item.time} */}
156
+ </Typography>
157
+ <Typography sx={{ fontSize: '16px' }} variant="body1">
158
+ {item.title}
159
+ {/* {item.message} */}
160
+ </Typography>
161
+ <Typography
162
+ style={{
163
+ display: 'flex',
164
+ alignItems: 'center',
165
+ marginTop: '8px',
76
166
  }}
77
167
  >
78
- <TimelineSeparator>
79
- <StyledTimeLineDot>
80
- <StyledCircleIcon />
81
- </StyledTimeLineDot>
82
- {index < activities.length - 1 && (
83
- <TimelineConnector
84
- sx={{ bgcolor: '#12121233', width: '1px' }}
85
- />
86
- )}
87
- </TimelineSeparator>
88
- <TimelineContent sx={{ padding: '6px 16px' }}>
89
- <Box>
90
- <Typography variant="subtitle2" sx={{ fontSize: '14px' }}>
91
- {item.date} - {item.time}
92
- </Typography>
93
- <Typography sx={{ fontSize: '16px' }} variant="body1">
94
- {item.message}
95
- </Typography>
96
- <Typography
97
- style={{
98
- display: 'flex',
99
- alignItems: 'center',
100
- marginTop: '5px',
101
- }}
102
- >
103
- <StyledAvatar />
104
- <Typography sx={{ fontSize: '13px', fontWeight: 900 }}>
105
- {item.userName}
106
- </Typography>
107
- </Typography>
108
- </Box>
109
- </TimelineContent>
110
- </TimelineItem>
111
- ))}
112
- </Timeline>
113
- </Box>
114
- ) : (
115
- <Table columns={columns} dataSource={activities ?? []} />
116
- )}
117
- </PageContent>
118
- </>
168
+ <StyledAvatar>
169
+ {'John Doe'
170
+ .split(' ')
171
+ .map((n) => n[0])
172
+ .join('')
173
+ .toUpperCase()}
174
+ </StyledAvatar>
175
+ <Typography sx={{ fontSize: '13px', fontWeight: 900 }}>
176
+ {`John Doe ${item.id}`}
177
+ {/* {item.userName} */}
178
+ </Typography>
179
+ </Typography>
180
+ </Box>
181
+ </TimelineContent>
182
+ </TimelineItem>
183
+ {isFetchingNextPage && index === items.length - 1 && hasNextPage && (
184
+ <StyledSpinnerBox>
185
+ <Spinner />
186
+ </StyledSpinnerBox>
187
+ )}
188
+ </Box>
189
+ ))}
190
+ </Timeline>
119
191
  )
120
192
  }
121
193
 
122
- export default ActivityLog
194
+ function useIntersectionObserver<T extends HTMLElement>(callback: () => void) {
195
+ const observer = useRef<IntersectionObserver | null>(null)
123
196
 
124
- const columns = [
125
- {
126
- title: 'Date',
127
- dataIndex: 'date',
128
- key: 'date',
129
- },
130
- {
131
- title: 'Time',
132
- dataIndex: 'time',
133
- key: 'time',
134
- },
135
- {
136
- title: 'Message',
137
- dataIndex: 'message',
138
- key: 'message',
139
- },
140
- {
141
- title: 'User',
142
- dataIndex: 'userName',
143
- key: 'userName',
144
- },
145
- ]
197
+ const handleObserver = useCallback(
198
+ (node: T) => {
199
+ if (observer.current) observer.current.disconnect()
200
+ observer.current = new IntersectionObserver((entries) => {
201
+ if (entries[0].isIntersecting) callback()
202
+ })
203
+ if (node) observer.current.observe(node)
204
+ },
205
+ [callback],
206
+ )
207
+
208
+ return handleObserver
209
+ }
@@ -1,6 +1,6 @@
1
1
  import CircleRoundedIcon from '@mui/icons-material/CircleRounded'
2
2
  import { TimelineDot } from '@mui/lab'
3
- import { Avatar, styled } from '@mui/material'
3
+ import { Avatar, Box, styled } from '@mui/material'
4
4
 
5
5
  export const StyledTimeLineDot = styled(TimelineDot)({
6
6
  width: '20px',
@@ -21,8 +21,17 @@ export const StyledCircleIcon = styled(CircleRoundedIcon)({
21
21
  height: '7px',
22
22
  })
23
23
 
24
- export const StyledAvatar = styled(Avatar)({
24
+ export const StyledAvatar = styled(Avatar)(({ theme }) => ({
25
25
  width: 30,
26
26
  height: 30,
27
27
  marginRight: '8px',
28
+ backgroundColor: theme.palette.primary.main,
29
+ fontSize: '14px',
30
+ }))
31
+
32
+ export const StyledSpinnerBox = styled(Box)({
33
+ display: 'flex',
34
+ justifyContent: 'center',
35
+ alignItems: 'center',
36
+ padding: '20px',
28
37
  })
@@ -87,7 +87,7 @@ export const SearchSingleSelect = ({ options, value, onChange }: Props) => {
87
87
  <TextField
88
88
  inputRef={(input) => {
89
89
  if (input && value) {
90
- input.style.width = `${value.label.length * 9}px`
90
+ input.style.width = `${value.label?.length * 9}px`
91
91
  }
92
92
  }}
93
93
  InputProps={{
@@ -38,6 +38,7 @@ interface AppHeaderProps {
38
38
  clientLogo: string
39
39
  fullName: string
40
40
  profileUrl?: string
41
+ showMenu?: boolean
41
42
  actions?: ReactNode[]
42
43
  userBoxActions: {
43
44
  label: ReactNode
@@ -45,23 +46,30 @@ interface AppHeaderProps {
45
46
  onClick: any
46
47
  }[]
47
48
  customHeaderActions?: ReactNode
49
+ headerSx?: any
50
+ imageSx?: any
51
+ profileSx?: any
48
52
  cogWheelMenu?: { label: string; path: string; openNewTab?: boolean }[]
49
53
  }
50
54
 
51
55
  export default function AppHeader({
52
56
  clientLogo = imageMap.campx,
57
+ showMenu = true,
53
58
  fullName,
54
59
  userBoxActions = [],
55
60
  cogWheelMenu = [],
56
61
  customHeaderActions,
57
62
  profileUrl,
58
63
  actions = [],
64
+ headerSx = {},
65
+ profileSx = {},
66
+ imageSx = {},
59
67
  }: AppHeaderProps) {
60
68
  return (
61
- <StyledHeader>
69
+ <StyledHeader sx={headerSx}>
62
70
  <Box sx={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
63
- <AppsMenu />
64
- <AppLogo clientLogo={clientLogo} />
71
+ {showMenu && <AppsMenu />}
72
+ <AppLogo clientLogo={clientLogo} imageSx={imageSx} />
65
73
  </Box>
66
74
  <Box className="actions">
67
75
  {customHeaderActions ? (
@@ -73,6 +81,7 @@ export default function AppHeader({
73
81
  userBoxActions={userBoxActions}
74
82
  profileUrl={profileUrl}
75
83
  actions={actions}
84
+ profileSx={profileSx}
76
85
  />
77
86
  )}
78
87
  </Box>
@@ -80,16 +89,16 @@ export default function AppHeader({
80
89
  )
81
90
  }
82
91
 
83
- const AppLogo = ({ clientLogo }) => {
92
+ const AppLogo = ({ clientLogo, imageSx }) => {
84
93
  const originSubdomain = window.location.host.split('.')?.slice(-3)[0] ?? 'ums'
85
94
  const currentApp =
86
95
  applications.find((item) => item.domainName === originSubdomain)
87
- ?.domainName ?? 'campx'
96
+ ?.domainName ?? 'admin'
88
97
 
89
98
  return (
90
99
  <StyledRouterLink to={'/'}>
91
100
  <StyledLogosWrapper>
92
- <StyledImageWrapper>
101
+ <StyledImageWrapper sx={imageSx}>
93
102
  <img src={imageMap[currentApp]} />
94
103
  </StyledImageWrapper>
95
104
  <Box
@@ -18,12 +18,14 @@ export default function HeaderActions({
18
18
  userBoxActions,
19
19
  profileUrl = '',
20
20
  actions = [],
21
+ profileSx = {},
21
22
  }: {
22
23
  cogWheelMenu?: any[]
23
24
  fullName: string
24
25
  userBoxActions: IMenuItemProps[] | []
25
26
  profileUrl: string
26
27
  actions?: ReactNode[]
28
+ profileSx?: any
27
29
  }) {
28
30
  const navigate = useNavigate()
29
31
  return (
@@ -46,6 +48,7 @@ export default function HeaderActions({
46
48
  fullName={fullName}
47
49
  actions={userBoxActions}
48
50
  profileUrl={profileUrl}
51
+ profileSx={profileSx}
49
52
  />
50
53
  </Stack>
51
54
  </Stack>
@@ -27,17 +27,19 @@ export default function UserBox({
27
27
  actions,
28
28
  customActions = [],
29
29
  profileUrl,
30
+ profileSx = {},
30
31
  }: {
31
32
  fullName: string
32
33
  actions: IMenuItemProps[] | []
33
34
  customActions?: IMenuItemProps[] | []
34
35
  profileUrl?: string
36
+ profileSx?: any
35
37
  }) {
36
38
  const navigate = useNavigate()
37
39
  return (
38
40
  <DropDownButton
39
41
  anchor={({ open }) => (
40
- <StyledAvatar src={profileUrl ?? ''} onClick={open}>
42
+ <StyledAvatar src={profileUrl ?? ''} onClick={open} sx={profileSx}>
41
43
  {getStartingLetters(fullName)}
42
44
  </StyledAvatar>
43
45
  )}