@taruvi/navkit 0.0.37 → 0.0.39
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/.claude/settings.local.json +12 -0
- package/package.json +6 -6
- package/src/App.styles.ts +4 -1
- package/src/App.tsx +16 -8
- package/src/NavkitContext.tsx +2 -1
- package/src/components/AppLauncher/AppLauncher.tsx +6 -1
- package/src/components/DraggableResizable.tsx +1 -1
- package/src/components/MattermostChat/MattermostChat.tsx +84 -53
- package/src/components/Profile/Profile.tsx +10 -16
- package/src/components/Profile/ProfileMenu.tsx +4 -1
- package/src/components/Shortucts/Shortcuts.styles.ts +2 -2
- package/src/components/Shortucts/Shortcuts.tsx +8 -7
- package/src/types.ts +4 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taruvi/navkit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.39",
|
|
4
4
|
"main": "src/App.tsx",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"peerDependencies": {
|
|
14
14
|
"@emotion/react": ">=11 <12",
|
|
15
15
|
"@emotion/styled": ">=11 <12",
|
|
16
|
-
"@fortawesome/fontawesome-svg-core": ">=6 <
|
|
17
|
-
"@fortawesome/free-regular-svg-icons": ">=6 <
|
|
18
|
-
"@fortawesome/free-solid-svg-icons": ">=6 <
|
|
19
|
-
"@fortawesome/react-fontawesome": ">=0.2 <
|
|
16
|
+
"@fortawesome/fontawesome-svg-core": ">=6 <8",
|
|
17
|
+
"@fortawesome/free-regular-svg-icons": ">=6 <8",
|
|
18
|
+
"@fortawesome/free-solid-svg-icons": ">=6 <8",
|
|
19
|
+
"@fortawesome/react-fontawesome": ">=0.2 <4",
|
|
20
20
|
"@mui/icons-material": ">=5 <8",
|
|
21
21
|
"@mui/material": ">=5 <8",
|
|
22
22
|
"react": ">=18 <20"
|
|
@@ -48,6 +48,6 @@
|
|
|
48
48
|
"vitest": "^4.0.18"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@taruvi/sdk": "
|
|
51
|
+
"@taruvi/sdk": "latest"
|
|
52
52
|
}
|
|
53
53
|
}
|
package/src/App.styles.ts
CHANGED
|
@@ -77,7 +77,7 @@ export const getAppStyles = (mode: ThemeMode) => {
|
|
|
77
77
|
overflow: 'hidden',
|
|
78
78
|
},
|
|
79
79
|
toolbar: {
|
|
80
|
-
backgroundColor: '#
|
|
80
|
+
backgroundColor: '#2B97FF',
|
|
81
81
|
boxShadow: ' 0px 0px 10px 2px rgba(0, 0, 0, 0.3)',
|
|
82
82
|
justifyContent: 'space-between',
|
|
83
83
|
minHeight: dimensions.navHeight,
|
|
@@ -106,6 +106,9 @@ export const getAppStyles = (mode: ThemeMode) => {
|
|
|
106
106
|
fontWeight: 600,
|
|
107
107
|
cursor: 'pointer',
|
|
108
108
|
},
|
|
109
|
+
taruviSpaceLogo: {
|
|
110
|
+
height: '22px',
|
|
111
|
+
},
|
|
109
112
|
rightSection: {
|
|
110
113
|
display: 'flex',
|
|
111
114
|
alignItems: 'center',
|
package/src/App.tsx
CHANGED
|
@@ -9,10 +9,12 @@ import DraggableResizable from "./components/DraggableResizable";
|
|
|
9
9
|
import { getAppStyles } from "./App.styles"
|
|
10
10
|
import { NavkitProvider, useNavigation } from "./NavkitContext"
|
|
11
11
|
import taruviLogo from "./assets/logo.svg";
|
|
12
|
+
import taruviLogoWhite from "./assets/taruvi-logo-white.png";
|
|
12
13
|
import type { Client } from "@taruvi/sdk"
|
|
13
14
|
|
|
14
15
|
const NavkitContent = () => {
|
|
15
16
|
const { isUserAuthenticated, siteSettings, appSettings, appName, userData, jwtToken, themeMode } = useNavigation();
|
|
17
|
+
const showTaruviLogo = !appName
|
|
16
18
|
const styles = getAppStyles(themeMode)
|
|
17
19
|
const [showAppLauncher, setShowAppLauncher] = useState<boolean>(false)
|
|
18
20
|
const [showProfileMenu, setShowProfileMenu] = useState<boolean>(false)
|
|
@@ -35,12 +37,18 @@ const NavkitContent = () => {
|
|
|
35
37
|
<AppBar position="static" sx={styles.appBar}>
|
|
36
38
|
<Toolbar sx={styles.toolbar}>
|
|
37
39
|
<Box component={"a"} href="/" sx={styles.leftSection}>
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
{showTaruviLogo ? (
|
|
41
|
+
<Box component="img" src={taruviLogoWhite} sx={styles.taruviSpaceLogo} />
|
|
42
|
+
) : (
|
|
43
|
+
<>
|
|
44
|
+
{appSettings?.icon ? <Box component="img" src={appSettings?.icon} sx={styles.logo} /> : <></>}
|
|
45
|
+
{(appName || appSettings?.displayName) &&
|
|
46
|
+
<Typography sx={styles.appName} onClick={() => (window.location.href = "/")}>
|
|
47
|
+
{(() => { const name = appName || appSettings!.displayName!; return name.length > 40 ? `${name.substring(0, 40)}...` : name })()}
|
|
48
|
+
</Typography>
|
|
49
|
+
}
|
|
50
|
+
</>
|
|
51
|
+
)}
|
|
44
52
|
</Box>
|
|
45
53
|
<Box component={"div"} sx={styles.rightSection}>
|
|
46
54
|
<Shortcuts showChat={setShowChat} onMenuToggle={() => setShowShortcutsMenu(!showShortcutsMenu)} />
|
|
@@ -70,7 +78,7 @@ const NavkitContent = () => {
|
|
|
70
78
|
<Box component="img" src={taruviLogo} sx={styles.appLauncherLogo} />
|
|
71
79
|
)}
|
|
72
80
|
</Box>
|
|
73
|
-
{isUserAuthenticated && userData && <Profile userData={userData} onClick={() => setShowProfileMenu(!showProfileMenu)}/>}
|
|
81
|
+
{isUserAuthenticated && userData && <Profile userData={userData} onClick={() => setShowProfileMenu(!showProfileMenu)} />}
|
|
74
82
|
</Box>
|
|
75
83
|
</Toolbar>
|
|
76
84
|
</AppBar>
|
|
@@ -101,7 +109,7 @@ const NavkitContent = () => {
|
|
|
101
109
|
<FontAwesomeIcon icon={["fas", "external-link-alt"]} style={{ fontSize: "10px" }} />
|
|
102
110
|
</IconButton>
|
|
103
111
|
</Box>
|
|
104
|
-
<MattermostChat jwtToken={jwtToken} mattermostUrl={siteSettings['chat-url']} width="100%" height="100%" />
|
|
112
|
+
<MattermostChat jwtToken={jwtToken} mattermostUrl={siteSettings['chat-url']} loginId={userData?.email || ''} width="100%" height="100%" />
|
|
105
113
|
</DraggableResizable>
|
|
106
114
|
)}
|
|
107
115
|
</>
|
package/src/NavkitContext.tsx
CHANGED
|
@@ -12,7 +12,6 @@ const getInitialTheme = (): ThemeMode => {
|
|
|
12
12
|
if (typeof window !== 'undefined') {
|
|
13
13
|
const stored = localStorage.getItem(THEME_STORAGE_KEY)
|
|
14
14
|
if (stored === 'dark' || stored === 'light') return stored
|
|
15
|
-
if (window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark'
|
|
16
15
|
}
|
|
17
16
|
return 'light'
|
|
18
17
|
}
|
|
@@ -30,6 +29,7 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName }: Nav
|
|
|
30
29
|
'show-chat': false,
|
|
31
30
|
'chat-url': '',
|
|
32
31
|
frontendUrl: '',
|
|
32
|
+
enableDarkMode: false,
|
|
33
33
|
})
|
|
34
34
|
const appSettings = useRef<AppSettings>({})
|
|
35
35
|
|
|
@@ -104,6 +104,7 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName }: Nav
|
|
|
104
104
|
'chat-url': rawSettings['navkit.chat-url'] ?? '',
|
|
105
105
|
frontendUrl: rawSettings['navkit.frontend-url'] ?? '',
|
|
106
106
|
logo: rawSettings['navkit.logo'] ?? '',
|
|
107
|
+
enableDarkMode: rawSettings['navkit.enable-dark-mode'] ?? false,
|
|
107
108
|
}
|
|
108
109
|
if (isUserAuthenticated) {
|
|
109
110
|
const userDataResponse = await auth.getCurrentUser()
|
|
@@ -6,6 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
|
6
6
|
import { findIconDefinition, type IconName } from "@fortawesome/fontawesome-svg-core"
|
|
7
7
|
import { useNavigation } from "../../NavkitContext"
|
|
8
8
|
import { getAppLauncherStyles } from "./AppLauncher.styles"
|
|
9
|
+
import { typography } from "../../styles/variables"
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
const isIconUrl = (icon: string): boolean => {
|
|
@@ -37,7 +38,11 @@ const AppLauncher = () => {
|
|
|
37
38
|
return (
|
|
38
39
|
<Card sx={styles.container}>
|
|
39
40
|
<Search appsList={appsList} onSearchChange={handleSearchChange} />
|
|
40
|
-
{filteredApps
|
|
41
|
+
{filteredApps.length === 0 ? (
|
|
42
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
|
|
43
|
+
<Typography sx={{ color: styles.appName.color, fontSize: typography.sizes.sm }}>No apps available</Typography>
|
|
44
|
+
</Box>
|
|
45
|
+
) : (
|
|
41
46
|
<Box sx={styles.scrollContainer}>
|
|
42
47
|
<Grid container spacing={0}>
|
|
43
48
|
{filteredApps.map((app) => {
|
|
@@ -41,7 +41,7 @@ const DraggableResizable = ({
|
|
|
41
41
|
y: Math.max(0, Math.min(e.clientY - dragOffset.current.y, window.innerHeight - 50)),
|
|
42
42
|
})
|
|
43
43
|
} else if (action.current === 'resize') {
|
|
44
|
-
setSize(
|
|
44
|
+
setSize(() => ({
|
|
45
45
|
w: Math.max(minWidth, e.clientX - dragOffset.current.x),
|
|
46
46
|
h: Math.max(minHeight, e.clientY - dragOffset.current.y),
|
|
47
47
|
}))
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { Box, Badge } from '@mui/material'
|
|
2
|
+
import { Box, Badge, CircularProgress, Typography } from '@mui/material'
|
|
3
3
|
|
|
4
4
|
interface MattermostChatProps {
|
|
5
5
|
jwtToken: string
|
|
6
|
-
mattermostUrl: string
|
|
6
|
+
mattermostUrl: string
|
|
7
|
+
loginId: string
|
|
7
8
|
onNotification?: (count: number) => void
|
|
8
9
|
onUrlClick?: (url: string) => void
|
|
9
10
|
width?: string | number
|
|
@@ -24,6 +25,7 @@ interface NotifyMessage {
|
|
|
24
25
|
const MattermostChat = ({
|
|
25
26
|
jwtToken,
|
|
26
27
|
mattermostUrl,
|
|
28
|
+
loginId,
|
|
27
29
|
onNotification,
|
|
28
30
|
onUrlClick,
|
|
29
31
|
width = '100%',
|
|
@@ -32,17 +34,65 @@ const MattermostChat = ({
|
|
|
32
34
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
|
33
35
|
const [unreadCount, setUnreadCount] = useState(0)
|
|
34
36
|
const [isWindowFocused, setIsWindowFocused] = useState(true)
|
|
37
|
+
const [authToken, setAuthToken] = useState<string | null>(null)
|
|
38
|
+
const [authError, setAuthError] = useState<string | null>(null)
|
|
39
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
40
|
+
|
|
41
|
+
// Authenticate with Mattermost on mount
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const login = async () => {
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(`${mattermostUrl.replace(/\/$/, '')}/api/v4/users/login`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
|
48
|
+
credentials: 'omit',
|
|
49
|
+
body: JSON.stringify({ login_id: loginId, password: jwtToken, token: '', deviceId: '' })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
setAuthError(`Login failed (${res.status})`)
|
|
54
|
+
setIsLoading(false)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const token = res.headers.get('Token')
|
|
59
|
+
if (token) {
|
|
60
|
+
setAuthToken(token)
|
|
61
|
+
} else {
|
|
62
|
+
setAuthError('No token in response')
|
|
63
|
+
}
|
|
64
|
+
} catch (e: any) {
|
|
65
|
+
setAuthError(e.message || 'Login failed')
|
|
66
|
+
}
|
|
67
|
+
setIsLoading(false)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (loginId && jwtToken) login()
|
|
71
|
+
else { setAuthError('Missing credentials'); setIsLoading(false) }
|
|
72
|
+
}, [mattermostUrl, loginId, jwtToken])
|
|
73
|
+
|
|
74
|
+
// Inject auth token into iframe via postMessage once loaded
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!authToken || !iframeRef.current) return
|
|
77
|
+
|
|
78
|
+
const handleIframeLoad = () => {
|
|
79
|
+
// Set the auth cookie/token in the iframe context
|
|
80
|
+
iframeRef.current?.contentWindow?.postMessage(
|
|
81
|
+
{ type: 'token', data: authToken },
|
|
82
|
+
new URL(mattermostUrl).origin
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const iframe = iframeRef.current
|
|
87
|
+
iframe.addEventListener('load', handleIframeLoad)
|
|
88
|
+
return () => iframe.removeEventListener('load', handleIframeLoad)
|
|
89
|
+
}, [authToken, mattermostUrl])
|
|
90
|
+
|
|
35
91
|
useEffect(() => {
|
|
36
|
-
// Handle postMessage events from Mattermost iframe
|
|
37
92
|
const handleMessage = (event: MessageEvent<NotifyMessage>) => {
|
|
38
|
-
// Validate origin for security
|
|
39
93
|
const mattermostOrigin = new URL(mattermostUrl).origin
|
|
40
|
-
if (event.origin !== mattermostOrigin)
|
|
41
|
-
console.warn('Received message from untrusted origin:', event.origin)
|
|
42
|
-
return
|
|
43
|
-
}
|
|
94
|
+
if (event.origin !== mattermostOrigin) return
|
|
44
95
|
|
|
45
|
-
// Handle Notify events
|
|
46
96
|
if (event.data?.event === 'Notify') {
|
|
47
97
|
if (!isWindowFocused) {
|
|
48
98
|
setUnreadCount(prev => {
|
|
@@ -51,31 +101,16 @@ const MattermostChat = ({
|
|
|
51
101
|
return newCount
|
|
52
102
|
})
|
|
53
103
|
}
|
|
54
|
-
|
|
55
|
-
// Handle URL clicks if provided
|
|
56
|
-
if (event.data.data?.url && onUrlClick) {
|
|
57
|
-
onUrlClick(event.data.data.url)
|
|
58
|
-
}
|
|
104
|
+
if (event.data.data?.url && onUrlClick) onUrlClick(event.data.data.url)
|
|
59
105
|
}
|
|
60
106
|
}
|
|
61
107
|
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
setIsWindowFocused(true)
|
|
65
|
-
setUnreadCount(0)
|
|
66
|
-
onNotification?.(0)
|
|
67
|
-
}
|
|
108
|
+
const handleFocus = () => { setIsWindowFocused(true); setUnreadCount(0); onNotification?.(0) }
|
|
109
|
+
const handleBlur = () => setIsWindowFocused(false)
|
|
68
110
|
|
|
69
|
-
const handleBlur = () => {
|
|
70
|
-
setIsWindowFocused(false)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Add event listeners
|
|
74
111
|
window.addEventListener('message', handleMessage)
|
|
75
112
|
window.addEventListener('focus', handleFocus)
|
|
76
113
|
window.addEventListener('blur', handleBlur)
|
|
77
|
-
|
|
78
|
-
// Cleanup event listeners on unmount
|
|
79
114
|
return () => {
|
|
80
115
|
window.removeEventListener('message', handleMessage)
|
|
81
116
|
window.removeEventListener('focus', handleFocus)
|
|
@@ -83,33 +118,34 @@ const MattermostChat = ({
|
|
|
83
118
|
}
|
|
84
119
|
}, [mattermostUrl, isWindowFocused, onNotification, onUrlClick])
|
|
85
120
|
|
|
86
|
-
|
|
87
|
-
|
|
121
|
+
if (isLoading) {
|
|
122
|
+
return (
|
|
123
|
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width, height }}>
|
|
124
|
+
<CircularProgress size={32} />
|
|
125
|
+
</Box>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (authError) {
|
|
130
|
+
return (
|
|
131
|
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width, height }}>
|
|
132
|
+
<Typography color="error">{authError}</Typography>
|
|
133
|
+
</Box>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Load Mattermost with the auth token as a cookie header via the access_token param
|
|
138
|
+
const iframeUrl = `${mattermostUrl.replace(/\/$/, '')}?access_token=${authToken}`
|
|
88
139
|
|
|
89
140
|
return (
|
|
90
|
-
<Box
|
|
91
|
-
sx={{
|
|
92
|
-
position: 'relative',
|
|
93
|
-
width,
|
|
94
|
-
height,
|
|
95
|
-
overflow: 'hidden'
|
|
96
|
-
}}
|
|
97
|
-
>
|
|
141
|
+
<Box sx={{ position: 'relative', width, height, overflow: 'hidden' }}>
|
|
98
142
|
{unreadCount > 0 && (
|
|
99
143
|
<Badge
|
|
100
144
|
badgeContent={unreadCount}
|
|
101
145
|
color="error"
|
|
102
146
|
sx={{
|
|
103
|
-
position: 'absolute',
|
|
104
|
-
|
|
105
|
-
right: 16,
|
|
106
|
-
zIndex: 1000,
|
|
107
|
-
'& .MuiBadge-badge': {
|
|
108
|
-
fontSize: '1rem',
|
|
109
|
-
height: '28px',
|
|
110
|
-
minWidth: '28px',
|
|
111
|
-
borderRadius: '14px'
|
|
112
|
-
}
|
|
147
|
+
position: 'absolute', top: 16, right: 16, zIndex: 1000,
|
|
148
|
+
'& .MuiBadge-badge': { fontSize: '1rem', height: '28px', minWidth: '28px', borderRadius: '14px' }
|
|
113
149
|
}}
|
|
114
150
|
/>
|
|
115
151
|
)}
|
|
@@ -118,12 +154,7 @@ const MattermostChat = ({
|
|
|
118
154
|
src={iframeUrl}
|
|
119
155
|
title="Mattermost Chat"
|
|
120
156
|
allow="clipboard-read; clipboard-write"
|
|
121
|
-
style={{
|
|
122
|
-
width: '100%',
|
|
123
|
-
height: '100%',
|
|
124
|
-
border: 'none',
|
|
125
|
-
display: 'block'
|
|
126
|
-
}}
|
|
157
|
+
style={{ width: '100%', height: '100%', border: 'none', display: 'block' }}
|
|
127
158
|
/>
|
|
128
159
|
</Box>
|
|
129
160
|
)
|
|
@@ -2,39 +2,33 @@ import { Box, SvgIcon } from '@mui/material'
|
|
|
2
2
|
import { getProfileStyles } from './Profile.styles'
|
|
3
3
|
import { useNavigation } from '../../NavkitContext'
|
|
4
4
|
|
|
5
|
-
const ProfileIcon = () => (
|
|
5
|
+
const ProfileIcon = ({ color }: { color?: string }) => (
|
|
6
6
|
<SvgIcon viewBox="0 0 22 22" sx={{ fontSize: 23, display: 'block' }}>
|
|
7
|
-
<path d="M10.9921 20.9849C16.511 20.9849 20.9849 16.511 20.9849 10.9921C20.9849 5.4732 16.511 0.999268 10.9921 0.999268C5.47322 0.999268 0.999283 5.4732 0.999283 10.9921C0.999283 16.511 5.47322 20.9849 10.9921 20.9849Z" stroke="#
|
|
8
|
-
<path d="M10.9921 11.9914C12.6478 11.9914 13.9899 10.6492 13.9899 8.99357C13.9899 7.33791 12.6478 5.99573 10.9921 5.99573C9.33644 5.99573 7.99426 7.33791 7.99426 8.99357C7.99426 10.6492 9.33644 11.9914 10.9921 11.9914Z" stroke="#
|
|
9
|
-
<path d="M5.99569 19.6479V17.9871C5.99569 17.457 6.20625 16.9487 6.58106 16.5739C6.95586 16.1991 7.4642 15.9885 7.99425 15.9885H13.9899C14.52 15.9885 15.0283 16.1991 15.4031 16.5739C15.7779 16.9487 15.9885 17.457 15.9885 17.9871V19.6479" stroke="#
|
|
7
|
+
<path d="M10.9921 20.9849C16.511 20.9849 20.9849 16.511 20.9849 10.9921C20.9849 5.4732 16.511 0.999268 10.9921 0.999268C5.47322 0.999268 0.999283 5.4732 0.999283 10.9921C0.999283 16.511 5.47322 20.9849 10.9921 20.9849Z" stroke={color || "#ffffff"} fill="none" strokeWidth="1.99856" strokeLinecap="round" strokeLinejoin="round"/>
|
|
8
|
+
<path d="M10.9921 11.9914C12.6478 11.9914 13.9899 10.6492 13.9899 8.99357C13.9899 7.33791 12.6478 5.99573 10.9921 5.99573C9.33644 5.99573 7.99426 7.33791 7.99426 8.99357C7.99426 10.6492 9.33644 11.9914 10.9921 11.9914Z" stroke={color || "#ffffff"} fill="none" strokeWidth="1.99856" strokeLinecap="round" strokeLinejoin="round"/>
|
|
9
|
+
<path d="M5.99569 19.6479V17.9871C5.99569 17.457 6.20625 16.9487 6.58106 16.5739C6.95586 16.1991 7.4642 15.9885 7.99425 15.9885H13.9899C14.52 15.9885 15.0283 16.1991 15.4031 16.5739C15.7779 16.9487 15.9885 17.457 15.9885 17.9871V19.6479" stroke={color || "#ffffff"} fill="none" strokeWidth="1.99856" strokeLinecap="round" strokeLinejoin="round"/>
|
|
10
10
|
</SvgIcon>
|
|
11
11
|
)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
full_name?: string
|
|
15
|
-
first_name?: string
|
|
16
|
-
last_name?: string
|
|
17
|
-
pfp?: string
|
|
18
|
-
username?: string
|
|
19
|
-
email?: string
|
|
20
|
-
}
|
|
13
|
+
import type { UserData } from '../../types'
|
|
21
14
|
|
|
22
15
|
interface ProfileProps {
|
|
23
16
|
userData: UserData
|
|
24
17
|
onClick: () => void
|
|
18
|
+
iconColor?: string
|
|
25
19
|
}
|
|
26
20
|
|
|
27
21
|
const Profile = (props: ProfileProps) => {
|
|
28
|
-
const { userData, onClick } = props
|
|
22
|
+
const { userData, onClick, iconColor } = props
|
|
29
23
|
const { themeMode } = useNavigation()
|
|
30
24
|
const styles = getProfileStyles(themeMode)
|
|
31
25
|
|
|
32
26
|
return (
|
|
33
27
|
<Box sx={styles.trigger} onClick={onClick}>
|
|
34
|
-
{userData.
|
|
35
|
-
<Box component="img" src={userData.
|
|
28
|
+
{userData.icon_url ? (
|
|
29
|
+
<Box component="img" src={userData.icon_url} sx={{ width: 23, height: 23, borderRadius: '50%' }} />
|
|
36
30
|
) : (
|
|
37
|
-
<ProfileIcon />
|
|
31
|
+
<ProfileIcon color={iconColor} />
|
|
38
32
|
)}
|
|
39
33
|
</Box>
|
|
40
34
|
)
|
|
@@ -6,7 +6,7 @@ import { getProfileStyles } from './Profile.styles'
|
|
|
6
6
|
import { useNavigation } from '../../NavkitContext'
|
|
7
7
|
|
|
8
8
|
const ProfileMenu = () => {
|
|
9
|
-
const { auth, themeMode, toggleTheme, userData } = useNavigation()
|
|
9
|
+
const { auth, themeMode, toggleTheme, userData, siteSettings } = useNavigation()
|
|
10
10
|
const styles = getProfileStyles(themeMode)
|
|
11
11
|
|
|
12
12
|
// const handlePreferences = () => {
|
|
@@ -14,6 +14,7 @@ const ProfileMenu = () => {
|
|
|
14
14
|
// }
|
|
15
15
|
|
|
16
16
|
const handleLogout = async () => {
|
|
17
|
+
localStorage.removeItem('navkit-theme-mode')
|
|
17
18
|
await auth.logout()
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -35,6 +36,7 @@ const ProfileMenu = () => {
|
|
|
35
36
|
</Typography>
|
|
36
37
|
</MenuItem> */}
|
|
37
38
|
|
|
39
|
+
{siteSettings.enableDarkMode && (
|
|
38
40
|
<MenuItem onClick={toggleTheme} sx={styles.menuItem}>
|
|
39
41
|
{themeMode === 'dark' ? (
|
|
40
42
|
<LightModeOutlined sx={styles.iconStyle} />
|
|
@@ -45,6 +47,7 @@ const ProfileMenu = () => {
|
|
|
45
47
|
{themeMode === 'dark' ? 'Light Mode' : 'Dark Mode'}
|
|
46
48
|
</Typography>
|
|
47
49
|
</MenuItem>
|
|
50
|
+
)}
|
|
48
51
|
|
|
49
52
|
<MenuItem onClick={handleLogout} sx={styles.menuItem}>
|
|
50
53
|
<FontAwesomeIcon
|
|
@@ -10,7 +10,7 @@ export const shortcutsStyles = {
|
|
|
10
10
|
mobileTrigger: {
|
|
11
11
|
display: { xs: 'flex', md: 'none' },
|
|
12
12
|
ml: spacing.sm,
|
|
13
|
-
color: '#
|
|
13
|
+
color: '#ffffff',
|
|
14
14
|
},
|
|
15
15
|
shortcut: {
|
|
16
16
|
display: 'flex',
|
|
@@ -25,6 +25,6 @@ export const shortcutsStyles = {
|
|
|
25
25
|
},
|
|
26
26
|
iconStyle: {
|
|
27
27
|
fontSize: '23px',
|
|
28
|
-
color:
|
|
28
|
+
color: '#ffffff',
|
|
29
29
|
},
|
|
30
30
|
}
|
|
@@ -4,9 +4,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
|
4
4
|
import { shortcutsStyles } from './Shortcuts.styles'
|
|
5
5
|
import { useNavigation } from '../../NavkitContext'
|
|
6
6
|
|
|
7
|
-
const ChatIcon = () => (
|
|
7
|
+
const ChatIcon = ({ color }: { color?: string }) => (
|
|
8
8
|
<SvgIcon viewBox="-2 -2 24 24" sx={{ fontSize: 23, display: 'block', position: 'relative', top: 2 }}>
|
|
9
|
-
<path d="M18 0H2C0.9 0 0 0.9 0 2V20L4 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0Z" fill="#
|
|
9
|
+
<path d="M18 0H2C0.9 0 0 0.9 0 2V20L4 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0Z" fill={color || "#ffffff"}/>
|
|
10
10
|
</SvgIcon>
|
|
11
11
|
)
|
|
12
12
|
|
|
@@ -14,11 +14,12 @@ interface ShortcutsProps {
|
|
|
14
14
|
showChat?: (showChat: boolean) => void
|
|
15
15
|
onMenuToggle?: () => void
|
|
16
16
|
triggerRef?: React.RefObject<HTMLButtonElement | null>
|
|
17
|
+
iconColor?: string
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const Shortcuts = (props: ShortcutsProps) => {
|
|
20
21
|
|
|
21
|
-
const { showChat, onMenuToggle, triggerRef } = props
|
|
22
|
+
const { showChat, onMenuToggle, triggerRef, iconColor } = props
|
|
22
23
|
const { navigateToUrl, siteSettings, isUserAuthenticated } = useNavigation()
|
|
23
24
|
const shortcuts = siteSettings.shortcuts
|
|
24
25
|
|
|
@@ -40,7 +41,7 @@ const Shortcuts = (props: ShortcutsProps) => {
|
|
|
40
41
|
>
|
|
41
42
|
<FontAwesomeIcon
|
|
42
43
|
icon={["far", shortcut.icon] as any}
|
|
43
|
-
style={shortcutsStyles.iconStyle}
|
|
44
|
+
style={{ ...shortcutsStyles.iconStyle, ...(iconColor && { color: iconColor }) }}
|
|
44
45
|
/>
|
|
45
46
|
</Link>
|
|
46
47
|
))}
|
|
@@ -50,7 +51,7 @@ const Shortcuts = (props: ShortcutsProps) => {
|
|
|
50
51
|
onClick={() => showChat?.(true)}
|
|
51
52
|
sx={shortcutsStyles.shortcut}
|
|
52
53
|
>
|
|
53
|
-
<ChatIcon />
|
|
54
|
+
<ChatIcon color={iconColor} />
|
|
54
55
|
</Link>
|
|
55
56
|
}
|
|
56
57
|
</Box>
|
|
@@ -60,11 +61,11 @@ const Shortcuts = (props: ShortcutsProps) => {
|
|
|
60
61
|
<IconButton
|
|
61
62
|
ref={triggerRef}
|
|
62
63
|
onClick={onMenuToggle}
|
|
63
|
-
sx={shortcutsStyles.mobileTrigger}
|
|
64
|
+
sx={{ ...shortcutsStyles.mobileTrigger, ...(iconColor && { color: iconColor }) }}
|
|
64
65
|
>
|
|
65
66
|
<FontAwesomeIcon
|
|
66
67
|
icon={["fas", "ellipsis-v"]}
|
|
67
|
-
style={{ ...shortcutsStyles.iconStyle, color: '#
|
|
68
|
+
style={{ ...shortcutsStyles.iconStyle, color: iconColor || '#ffffff' }}
|
|
68
69
|
/>
|
|
69
70
|
</IconButton>
|
|
70
71
|
)}
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from "react"
|
|
2
|
-
import type { Client, Auth } from "@taruvi/sdk"
|
|
2
|
+
import type { Client, Auth, UserData, UserRole } from "@taruvi/sdk"
|
|
3
3
|
import type { ThemeMode } from "./styles/variables"
|
|
4
4
|
|
|
5
|
+
export type { UserData, UserRole }
|
|
6
|
+
|
|
5
7
|
export interface AppData {
|
|
6
8
|
appname: string,
|
|
7
9
|
icon: string,
|
|
@@ -9,41 +11,13 @@ export interface AppData {
|
|
|
9
11
|
id: string
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export interface UserRole {
|
|
13
|
-
name: string
|
|
14
|
-
slug: string
|
|
15
|
-
type: string
|
|
16
|
-
app_slug?: string
|
|
17
|
-
source?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface UserData {
|
|
21
|
-
id?: number
|
|
22
|
-
username?: string
|
|
23
|
-
email?: string
|
|
24
|
-
first_name?: string
|
|
25
|
-
last_name?: string
|
|
26
|
-
full_name?: string
|
|
27
|
-
pfp?: string
|
|
28
|
-
is_active?: boolean
|
|
29
|
-
is_staff?: boolean
|
|
30
|
-
is_superuser?: boolean
|
|
31
|
-
is_deleted?: boolean
|
|
32
|
-
date_joined?: string
|
|
33
|
-
last_login?: string
|
|
34
|
-
groups?: { id: number; name: string }[]
|
|
35
|
-
user_permissions?: { id: number; name: string; codename: string; content_type: string }[]
|
|
36
|
-
attributes?: Record<string, unknown>
|
|
37
|
-
missing_attributes?: string[]
|
|
38
|
-
roles?: UserRole[]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
14
|
export interface SiteSettings {
|
|
42
15
|
shortcuts?: AppData[]
|
|
43
16
|
logo?: string
|
|
44
17
|
'show-chat': boolean
|
|
45
18
|
'chat-url': string
|
|
46
19
|
frontendUrl: string
|
|
20
|
+
enableDarkMode: boolean
|
|
47
21
|
}
|
|
48
22
|
|
|
49
23
|
export interface AppSettings {
|