goobs-frontend 0.8.13 → 0.8.15

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.
@@ -1,149 +1,343 @@
1
1
  'use client'
2
- import React, { useState, useMemo } from 'react'
3
- import type { JSX } from 'react'
4
- import HorizontalVariant from './HorizontalVariant'
5
- import VerticalVariant from './VerticalVariant'
2
+ import React, { useState, useCallback, FC } from 'react'
3
+ import { useRouter } from 'next/navigation'
4
+ import { Drawer, Box, Stack, Divider } from '@mui/material'
5
+ import Link from 'next/link'
6
+ import { Typography } from '../Typography'
7
+ import SearchableDropdown from '../SearchableDropdown'
8
+ import { white, ocean, semiTransparentWhite } from '../../styles/palette'
6
9
 
7
- // Type definition for alignment options
8
- type Alignment = 'left' | 'center' | 'right' | 'inherit' | 'justify'
10
+ // Sub-components for mainNav, subNav, viewNav
11
+ import MainNav from './VerticalVariant/mainNav'
12
+ import SubNavComponent from './VerticalVariant/subNav'
13
+ import ViewNav from './VerticalVariant/viewNav'
9
14
 
10
- /**
11
- * Interface for the props of the Nav component and its sub-components
12
- */
13
- export interface NavProps {
14
- items?: (NavProps | SubNav | View)[] // Array of navigation items
15
- showSearchableNav?: boolean // Flag to show/hide searchable navigation
16
- showTitle?: boolean // Flag to show/hide title
17
- showLine?: boolean // Flag to show/hide divider line
18
- verticalNavTitle?: string // Title for vertical navigation
19
- searchableNavLabel?: string // Label for searchable navigation
20
- anchor?: 'left' | 'right' // Position of vertical navigation
21
- orientation?: 'vertical' | 'horizontal' // Orientation of navigation
22
- height?: string // Height of navigation (for horizontal)
23
- alignment?: Alignment // Alignment of items (for horizontal)
24
- navname?: string // Name of the navigation
25
- title?: string // Title of the navigation item
26
- route?: string // Route for the navigation item
27
- subnavs?: SubNav[] // Sub-navigation items
28
- onClick?: () => void // Click handler for the item
29
- hasleftborder?: string // Flag for left border
30
- hasrightborder?: string // Flag for right border
31
- trigger?: 'route' | 'onClick' | 'routeonhorizontal' // Trigger type for the item
32
- backgroundcolor?: string
33
- shrunkfontcolor?: string // Color of the label when shrunk
34
- unshrunkfontcolor?: string // Color of the label when not shrunk
35
- titleUrl?: string // Changed from url to titleUrl
36
- mobileOpen?: boolean // Controls mobile drawer open state
37
- onClose?: () => void // Handler for closing mobile drawer
38
- variant?: 'temporary' | 'permanent' // Drawer variant for mobile/desktop
39
- spacingfromtopofscreen?: string // Spacing from top of screen
40
- }
15
+ /* -------------------------------------------------------------------------- */
16
+ /* INTERFACES */
17
+ /* -------------------------------------------------------------------------- */
41
18
 
42
19
  /**
43
- * Type definition for sub-navigation items
20
+ * A single interface that covers all vertical nav items:
21
+ * - navType = 'mainNav' => can have subnavs
22
+ * - navType = 'subNav' => can have views
23
+ * - navType = 'viewNav' => no children
44
24
  */
45
- export type SubNav = {
46
- title?: string // Title of the sub-nav
47
- route?: string // Route for the sub-nav
48
- subtitle?: string // Subtitle for the sub-nav
49
- views?: View[] // Views within the sub-nav
50
- navname?: string // Name of the sub-nav
25
+ export interface NavItem {
26
+ navType: 'mainNav' | 'subNav' | 'viewNav'
27
+ title: string
28
+ route?: string
29
+ trigger?: 'route' | 'onClick'
30
+ onClick?: () => void
31
+ // For mainNav items only:
32
+ subnavs?: NavItem[]
33
+ // For subNav items only:
34
+ views?: NavItem[]
51
35
  }
52
36
 
53
37
  /**
54
- * Type definition for view items
38
+ * NavProps for the vertical nav component.
55
39
  */
56
- export type View = {
57
- route?: string // Route for the view
58
- title?: string // Title of the view
59
- subtitle?: string // Subtitle for the view
60
- navname?: string // Name of the view
40
+ export interface NavProps {
41
+ /** The entire nav data array (mainNav, subNav, viewNav items). */
42
+ items?: NavItem[]
43
+
44
+ /** Whether to show the search box. */
45
+ showSearchableNav?: boolean
46
+
47
+ /** Whether to show the nav title. */
48
+ showTitle?: boolean
49
+
50
+ /** Whether to show a horizontal divider line. */
51
+ showLine?: boolean
52
+
53
+ /** Title text for the nav. */
54
+ verticalNavTitle?: string
55
+
56
+ /** Label for the search box. */
57
+ searchableNavLabel?: string
58
+
59
+ /** Side on which the Drawer anchors. */
60
+ anchor?: 'left' | 'right'
61
+
62
+ /** Background color (drawer or items). */
63
+ backgroundcolor?: string
64
+
65
+ /** Label color when shrunk (search box). */
66
+ shrunkfontcolor?: string
67
+
68
+ /** Label color when not shrunk (search box). */
69
+ unshrunkfontcolor?: string
70
+
71
+ /** Destination route if user clicks the nav title. */
72
+ titleUrl?: string
73
+
74
+ /** Controls mobile drawer open state. */
75
+ mobileOpen?: boolean
76
+
77
+ /** Handler for closing the mobile drawer. */
78
+ onClose?: () => void
79
+
80
+ /** MUI Drawer variant: 'temporary' or 'permanent'. */
81
+ variant?: 'temporary' | 'permanent'
82
+
83
+ /** Spacing from the top of the screen. */
84
+ spacingfromtopofscreen?: string
85
+
86
+ /** Margin above the nav title. */
87
+ marginabovetitle?: string
88
+
89
+ /** Margin below the nav title. */
90
+ marginbelowtitle?: string
61
91
  }
62
92
 
63
- /**
64
- * Nav component that renders either a vertical or horizontal navigation
65
- * @param {NavProps} props - The props for the component
66
- * @returns {JSX.Element} The rendered Nav component
67
- */
68
- function Nav({
93
+ /* -------------------------------------------------------------------------- */
94
+ /* SINGLE CONST NAV COMPONENT */
95
+ /* -------------------------------------------------------------------------- */
96
+
97
+ const Nav: FC<NavProps> = ({
69
98
  items = [],
70
- showSearchableNav = true, // Combined showSearchbar and showDropdown
99
+ showSearchableNav = true,
71
100
  showTitle = true,
72
101
  showLine = true,
73
102
  verticalNavTitle = 'Navigation',
74
- searchableNavLabel = 'Search or select a nav', // Combined label
103
+ searchableNavLabel = 'Search or select a nav',
75
104
  anchor = 'left',
76
- orientation,
77
- height = '80px',
78
- alignment = 'left',
79
- navname,
80
105
  shrunkfontcolor = 'black',
81
106
  unshrunkfontcolor = 'black',
82
107
  backgroundcolor,
83
- titleUrl, // Changed from url to titleUrl
108
+ titleUrl,
84
109
  mobileOpen = false,
85
110
  onClose,
86
111
  variant = 'permanent',
87
112
  spacingfromtopofscreen,
88
- }: NavProps): JSX.Element {
89
- // State for expanded navigation items
113
+ marginabovetitle = '0px',
114
+ marginbelowtitle = '0px',
115
+ }) => {
116
+ // States for expanded mainNavs and subNavs
90
117
  const [expandedNavs, setExpandedNavs] = useState<string[]>([])
91
118
  const [expandedSubnavs, setExpandedSubnavs] = useState<string[]>([])
92
- const [verticalNavWidth] = useState<number>(250) // Default width
93
-
94
- // Memoized navigation items
95
- const navs = useMemo(() => {
96
- const navs: NavProps[] = []
97
- const subnavs: SubNav[] = []
98
- const views: View[] = []
99
- items.forEach((item: NavProps | SubNav | View) => {
100
- if ('title' in item && 'subnavs' in item) {
101
- navs.push(item as NavProps)
102
- } else if ('title' in item && 'views' in item) {
103
- subnavs.push(item as SubNav)
104
- } else if ('title' in item && 'route' in item) {
105
- views.push(item as View)
119
+
120
+ // Default width for the vertical nav
121
+ const [verticalNavWidth] = useState<string>('250px')
122
+
123
+ // For route triggers
124
+ const router = useRouter()
125
+
126
+ // For search dropdown
127
+ const [selectedNav, setSelectedNav] = useState<string | null>(null)
128
+
129
+ // Build search dropdown options from mainNav items
130
+ const navOptions = items
131
+ .filter(item => item.navType === 'mainNav')
132
+ .map(item => ({ value: item.title }))
133
+
134
+ /**
135
+ * Handle route or onClick triggers for mainNav/subNav/viewNav
136
+ */
137
+ const handleNavClick = useCallback(
138
+ (item: NavItem) => {
139
+ if (item.trigger === 'route' && item.route) {
140
+ router.push(item.route)
141
+ if (variant === 'temporary' && onClose) {
142
+ onClose()
143
+ }
144
+ } else if (item.trigger === 'onClick' && item.onClick) {
145
+ item.onClick()
146
+ if (variant === 'temporary' && onClose) {
147
+ onClose()
148
+ }
106
149
  }
107
- })
108
- return navs
109
- }, [items])
110
-
111
- // Render vertical or horizontal variant based on orientation
112
- if (orientation === 'vertical') {
113
- return (
114
- <VerticalVariant
115
- items={navs}
116
- showSearchableNav={showSearchableNav}
117
- showTitle={showTitle}
118
- showLine={showLine}
119
- verticalNavTitle={verticalNavTitle}
120
- searchableNavLabel={searchableNavLabel}
121
- anchor={anchor}
122
- expandedNavs={expandedNavs}
123
- setExpandedNavs={setExpandedNavs}
124
- expandedSubnavs={expandedSubnavs}
125
- setExpandedSubnavs={setExpandedSubnavs}
126
- verticalNavWidth={`${verticalNavWidth}px`}
127
- shrunkfontcolor={shrunkfontcolor}
128
- unshrunkfontcolor={unshrunkfontcolor}
129
- backgroundcolor={backgroundcolor}
130
- titleUrl={titleUrl}
131
- mobileOpen={mobileOpen}
132
- onClose={onClose}
133
- variant={variant}
134
- spacingfromtopofscreen={spacingfromtopofscreen}
135
- />
136
- )
137
- } else {
138
- return (
139
- <HorizontalVariant
140
- items={items}
141
- height={height}
142
- alignment={alignment}
143
- navname={navname}
144
- />
145
- )
146
- }
150
+ },
151
+ [router, variant, onClose]
152
+ )
153
+
154
+ /**
155
+ * When user selects a mainNav from the search dropdown
156
+ */
157
+ const handleSearchableNavChange = useCallback(
158
+ (newValue: { value: string } | null) => {
159
+ setSelectedNav(newValue?.value || null)
160
+ },
161
+ []
162
+ )
163
+
164
+ /**
165
+ * Recursively render mainNav -> subNav -> viewNav
166
+ */
167
+ const renderItem = useCallback(
168
+ (
169
+ item: NavItem,
170
+ level: number,
171
+ activeAndHoverColor = semiTransparentWhite.main
172
+ ) => {
173
+ switch (item.navType) {
174
+ case 'mainNav': {
175
+ const hasChildren = !!item.subnavs?.length
176
+ return (
177
+ <MainNav
178
+ key={item.title}
179
+ title={item.title}
180
+ hasChildren={hasChildren}
181
+ expandedNavs={expandedNavs}
182
+ setExpandedNavs={setExpandedNavs}
183
+ onClick={() => handleNavClick(item)}
184
+ level={level}
185
+ activeAndHoverColor={activeAndHoverColor}
186
+ >
187
+ {item.subnavs?.map(subItem =>
188
+ renderItem(subItem, level + 1, activeAndHoverColor)
189
+ )}
190
+ </MainNav>
191
+ )
192
+ }
193
+ case 'subNav': {
194
+ const hasChildren = !!item.views?.length
195
+ return (
196
+ <SubNavComponent
197
+ key={item.title}
198
+ title={item.title}
199
+ route={item.route}
200
+ trigger={item.trigger}
201
+ expandedSubnavs={expandedSubnavs}
202
+ setExpandedSubnavs={setExpandedSubnavs}
203
+ activeAndHoverColor={activeAndHoverColor}
204
+ onClose={onClose}
205
+ variant={variant}
206
+ hasChildren={hasChildren}
207
+ >
208
+ {item.views?.map(view =>
209
+ renderItem(view, level + 2, activeAndHoverColor)
210
+ )}
211
+ </SubNavComponent>
212
+ )
213
+ }
214
+ case 'viewNav': {
215
+ return (
216
+ <ViewNav
217
+ key={item.title}
218
+ title={item.title}
219
+ route={item.route}
220
+ trigger={item.trigger}
221
+ onClick={item.onClick}
222
+ level={level}
223
+ activeAndHoverColor={activeAndHoverColor}
224
+ onClose={onClose}
225
+ variant={variant}
226
+ />
227
+ )
228
+ }
229
+ default:
230
+ return null
231
+ }
232
+ },
233
+ [
234
+ expandedNavs,
235
+ setExpandedNavs,
236
+ expandedSubnavs,
237
+ setExpandedSubnavs,
238
+ handleNavClick,
239
+ onClose,
240
+ variant,
241
+ ]
242
+ )
243
+
244
+ // Drawer Content: Title, optional search, optional divider, then items
245
+ const drawerContent = (
246
+ <>
247
+ <Box px="15px" sx={{ whiteSpace: 'nowrap' /* no text wrapping */ }}>
248
+ {showTitle && (
249
+ <Box mt={marginabovetitle} mb={marginbelowtitle}>
250
+ <Link
251
+ href={titleUrl || '/'}
252
+ passHref
253
+ style={{ textDecoration: 'none' }}
254
+ onClick={variant === 'temporary' ? onClose : undefined}
255
+ >
256
+ <Typography
257
+ fontvariant="merrih4"
258
+ fontcolor={white.main}
259
+ text={verticalNavTitle}
260
+ />
261
+ </Link>
262
+ </Box>
263
+ )}
264
+
265
+ {showSearchableNav && (
266
+ <Stack mt={{ xs: '10px', md: '10px', lg: 0 }} spacing={0}>
267
+ <Box
268
+ sx={{
269
+ position: 'relative',
270
+ zIndex: theme => theme.zIndex.drawer + 1,
271
+ width: '100%',
272
+ minHeight: '40px',
273
+ whiteSpace: 'nowrap',
274
+ }}
275
+ >
276
+ <SearchableDropdown
277
+ label={searchableNavLabel}
278
+ options={navOptions}
279
+ backgroundcolor={backgroundcolor || semiTransparentWhite.main}
280
+ outlinecolor="none"
281
+ fontcolor={white.main}
282
+ shrunkfontcolor={shrunkfontcolor}
283
+ unshrunkfontcolor={unshrunkfontcolor}
284
+ shrunklabelposition="aboveNotch"
285
+ onChange={handleSearchableNavChange}
286
+ placeholder="Search..."
287
+ />
288
+ </Box>
289
+ </Stack>
290
+ )}
291
+ </Box>
292
+
293
+ {showLine && (
294
+ <Divider
295
+ sx={{
296
+ width: '100%',
297
+ backgroundColor: white.main,
298
+ mt: 2.5,
299
+ }}
300
+ />
301
+ )}
302
+
303
+ {selectedNav
304
+ ? items.filter(i => i.title === selectedNav).map(i => renderItem(i, 0))
305
+ : items.map(i => renderItem(i, 0))}
306
+ </>
307
+ )
308
+
309
+ // Render the Drawer
310
+ return (
311
+ <Drawer
312
+ variant={variant}
313
+ anchor={anchor}
314
+ open={variant === 'temporary' ? mobileOpen : true}
315
+ onClose={onClose}
316
+ elevation={0}
317
+ sx={{
318
+ width: 'auto',
319
+ height: '100%',
320
+ flexShrink: 0,
321
+ '& .MuiDrawer-paper': {
322
+ minWidth: verticalNavWidth,
323
+ width: 'auto',
324
+ whiteSpace: 'nowrap',
325
+ overflowX: 'auto',
326
+ border: 0,
327
+ zIndex: theme =>
328
+ variant === 'temporary'
329
+ ? theme.zIndex.drawer + 2
330
+ : theme.zIndex.drawer - 1,
331
+ backgroundColor: ocean.main,
332
+ pt: '17px',
333
+ boxSizing: 'border-box',
334
+ marginTop: spacingfromtopofscreen,
335
+ },
336
+ }}
337
+ >
338
+ {drawerContent}
339
+ </Drawer>
340
+ )
147
341
  }
148
342
 
149
343
  export default Nav
@@ -307,6 +307,15 @@ const defaultConfig: PricingProps = {
307
307
  row: 7,
308
308
  column: 1,
309
309
  mobilewidth: '100%',
310
+ margintop: {
311
+ xs: 2,
312
+ sm: 2,
313
+ md: 2,
314
+ ms: 2,
315
+ ml: 2,
316
+ lg: 2,
317
+ xl: 2,
318
+ },
310
319
  marginbottom: {
311
320
  xs: 1,
312
321
  sm: 1,
@@ -0,0 +1,195 @@
1
+ 'use client'
2
+ import React, { useState, useEffect } from 'react'
3
+ import { AppBar, Toolbar, Box, Tabs as MuiTabs, Tab } from '@mui/material'
4
+ import { usePathname } from 'next/navigation'
5
+
6
+ /** Represents the shape of each tab item in the horizontal nav. */
7
+ export interface TabsItem {
8
+ /** The text/title displayed on the tab */
9
+ title?: string
10
+
11
+ /** The URL route */
12
+ route?: string
13
+
14
+ /** The trigger type: 'route' | 'onClick' */
15
+ trigger?: 'route' | 'onClick'
16
+
17
+ /** OnClick callback */
18
+ onClick?: () => void
19
+
20
+ /** Optional left border */
21
+ hasleftborder?: string
22
+
23
+ /** Optional right border */
24
+ hasrightborder?: string
25
+ }
26
+
27
+ /** Represents the currently active/selected tab */
28
+ export interface ActiveTabValue {
29
+ tabId: string | false
30
+ }
31
+
32
+ /** Props for the horizontal Tabs component. */
33
+ export interface TabsProps {
34
+ /** Array of horizontal items (TabsItem). */
35
+ items: TabsItem[]
36
+
37
+ /** Height of the tabs bar. */
38
+ height?: string
39
+
40
+ /** MUI alignment. */
41
+ alignment?: 'left' | 'center' | 'right' | 'inherit' | 'justify'
42
+
43
+ /** Unique name for this tab set (for managing active tab). */
44
+ navname?: string
45
+ }
46
+
47
+ /**
48
+ * A horizontal navigation component, built with MUI Tabs.
49
+ */
50
+ function Tabs({
51
+ items,
52
+ height = '48px',
53
+ alignment = 'left',
54
+ navname = '',
55
+ }: TabsProps) {
56
+ const [activeTabValues, setActiveTabValues] = useState<
57
+ Record<string, ActiveTabValue>
58
+ >({})
59
+ const pathname = usePathname()
60
+
61
+ useEffect(() => {
62
+ /**
63
+ * Find the item whose route matches the current path
64
+ */
65
+ const currentTab = items.find(item => item.route === pathname)
66
+
67
+ setActiveTabValues(prev => ({
68
+ ...prev,
69
+ [navname]: { tabId: currentTab?.title || false },
70
+ }))
71
+ }, [items, navname, pathname])
72
+
73
+ /**
74
+ * When user changes tab via click,
75
+ * update the activeTabValues record.
76
+ */
77
+ const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
78
+ setActiveTabValues(prev => ({
79
+ ...prev,
80
+ [navname]: { tabId: newValue },
81
+ }))
82
+ }
83
+
84
+ /**
85
+ * Called when a tab is clicked:
86
+ * - if trigger='route', navigate to the route
87
+ * - if trigger='onClick', call onClick
88
+ */
89
+ const handleTabClick = (tab: TabsItem) => {
90
+ if (tab.trigger === 'route' && tab.route) {
91
+ window.location.href = tab.route
92
+ } else if (tab.trigger === 'onClick' && tab.onClick) {
93
+ tab.onClick()
94
+ }
95
+ }
96
+
97
+ return (
98
+ <AppBar
99
+ position="sticky"
100
+ sx={{
101
+ backgroundColor: 'black',
102
+ color: 'white',
103
+ }}
104
+ >
105
+ <Toolbar
106
+ disableGutters
107
+ sx={{
108
+ paddingLeft: 0,
109
+ paddingRight: 0,
110
+ height: `${height} !important`,
111
+ minHeight: `${height} !important`,
112
+ borderTop: '1px solid white',
113
+ }}
114
+ >
115
+ <Box
116
+ sx={{
117
+ width: '100%',
118
+ display: 'flex',
119
+ flexDirection: 'row',
120
+ justifyContent: alignment === 'left' ? 'flex-start' : alignment,
121
+ }}
122
+ >
123
+ <Box
124
+ sx={{
125
+ flexGrow: 1,
126
+ display: 'flex',
127
+ height: height,
128
+ justifyContent: 'flex-start',
129
+ paddingLeft: 0,
130
+ paddingRight: 0,
131
+ }}
132
+ >
133
+ <MuiTabs
134
+ value={activeTabValues[navname]?.tabId || false}
135
+ onChange={handleTabChange}
136
+ aria-label="nav tabs"
137
+ variant="fullWidth"
138
+ sx={{
139
+ height: height,
140
+ '& .MuiTabs-flexContainer': {
141
+ height: '100%',
142
+ },
143
+ '& .MuiTab-root': {
144
+ height: '100%',
145
+ minHeight: 'unset',
146
+ },
147
+ }}
148
+ >
149
+ {items.map(item => (
150
+ <Tab
151
+ key={item.title}
152
+ value={item.title || ''}
153
+ label={item.title || ''}
154
+ onClick={() => handleTabClick(item)}
155
+ sx={{
156
+ flex: 1,
157
+ textTransform: 'none',
158
+ boxSizing: 'border-box',
159
+ backgroundColor: 'black',
160
+ color: '#fff',
161
+ fontWeight: 500,
162
+ fontFamily: 'Merriweather',
163
+ fontSize: 16,
164
+ height: height,
165
+ '&:hover': {
166
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
167
+ },
168
+ '& .MuiTouchRipple-root': {
169
+ color: '#fff',
170
+ },
171
+ '&.Mui-selected': {
172
+ color: '#fff',
173
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
174
+ },
175
+ '& .MuiSvgIcon-root': {
176
+ color: '#fff',
177
+ },
178
+ ...(item.hasleftborder === 'true' && {
179
+ borderLeft: '1px solid white',
180
+ }),
181
+ ...(item.hasrightborder === 'true' && {
182
+ borderRight: '1px solid white',
183
+ }),
184
+ }}
185
+ />
186
+ ))}
187
+ </MuiTabs>
188
+ </Box>
189
+ </Box>
190
+ </Toolbar>
191
+ </AppBar>
192
+ )
193
+ }
194
+
195
+ export default Tabs