@@ -11,64 +11,68 @@ interface ProfileInfoTooltipProps {
11
11
className ?: string ;
12
12
}
13
13
14
- function isValidProfileId ( profileId : string ) : boolean {
15
- return typeof profileId === 'string' &&
16
- profileId . length > 0 &&
17
- profileId . length <= 64 &&
18
- / ^ [ a - z A - Z 0 - 9 _ \- ] + $ / . test ( profileId ) ;
19
- }
20
-
21
-
22
- function getPortFromAdvanced ( profile : LocalLoginProfile ) : string | null {
23
- const dbType = profile . Type ;
24
- const defaultPortItem = databaseTypeDropdownItems . find ( item => item . id === dbType ) ;
14
+ const PROFILE_ID_REGEX = / ^ [ a - z A - Z 0 - 9 _ \- ] + $ / ;
15
+ const PROFILE_ID_MAX_LENGTH = 64 ;
16
+ const TOOLTIP_OFFSET = 12 ;
17
+
18
+ const isValidProfileId = ( profileId : string ) : boolean =>
19
+ typeof profileId === 'string' &&
20
+ profileId . length > 0 &&
21
+ profileId . length <= PROFILE_ID_MAX_LENGTH &&
22
+ PROFILE_ID_REGEX . test ( profileId ) ;
23
+
24
+ const getPortFromAdvanced = ( profile : LocalLoginProfile ) : string | null => {
25
+ const defaultPortItem = databaseTypeDropdownItems . find ( item => item . id === profile . Type ) ;
26
+ const defaultPort = defaultPortItem ?. extra ?. Port ;
25
27
26
- if ( ! defaultPortItem ?. extra ?. Port ) {
27
- return null ;
28
- }
28
+ if ( ! defaultPort ) return null ;
29
29
30
- const defaultPort = defaultPortItem . extra . Port ;
31
-
32
30
if ( profile . Advanced ) {
33
31
const portObj = profile . Advanced . find ( item => item . Key === 'Port' ) ;
34
32
return portObj ?. Value || defaultPort ;
35
33
}
36
34
37
35
return defaultPort ;
38
- }
36
+ } ;
39
37
40
- function getLastAccessedTime ( profileId : string ) : string | null {
41
- if ( ! isValidProfileId ( profileId ) ) {
42
- return null ;
43
- }
38
+ const getLastAccessedTime = ( profileId : string ) : string | null => {
39
+ if ( ! isValidProfileId ( profileId ) ) return null ;
44
40
45
41
try {
46
42
const lastAccessed = localStorage . getItem ( `whodb_profile_last_accessed_${ profileId } ` ) ;
47
- if ( lastAccessed ) {
48
- const date = new Date ( lastAccessed ) ;
49
- if ( isNaN ( date . getTime ( ) ) ) {
50
- return null ;
51
- }
52
- const timeZone = Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone ;
53
- const formattedTimeZone = timeZone . replace ( / _ / g, ' ' ) . split ( '/' ) . join ( ' / ' ) ;
54
- return `${ date . toLocaleDateString ( ) } ${ date . toLocaleTimeString ( [ ] , { hour : '2-digit' , minute : '2-digit' } ) } (${ formattedTimeZone } )` ;
55
- }
56
- } catch ( error ) {
43
+ if ( ! lastAccessed ) return null ;
44
+
45
+ const date = new Date ( lastAccessed ) ;
46
+ if ( isNaN ( date . getTime ( ) ) ) return null ;
47
+
48
+ const timeZone = Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone ;
49
+ const formattedTimeZone = timeZone . replace ( / _ / g, ' ' ) . split ( '/' ) . join ( ' / ' ) ;
50
+ return `${ date . toLocaleDateString ( ) } ${ date . toLocaleTimeString ( [ ] , { hour : '2-digit' , minute : '2-digit' } ) } (${ formattedTimeZone } )` ;
51
+ } catch {
57
52
return null ;
58
53
}
59
- return null ;
60
- }
54
+ } ;
61
55
62
56
let tooltipPortalContainer : HTMLDivElement | null = null ;
63
57
64
- function getTooltipPortalContainer ( ) : HTMLDivElement {
58
+ const getTooltipPortalContainer = ( ) : HTMLDivElement => {
65
59
if ( ! tooltipPortalContainer ) {
66
60
tooltipPortalContainer = document . createElement ( 'div' ) ;
67
61
tooltipPortalContainer . id = 'whodb-tooltip-portal' ;
68
62
document . body . appendChild ( tooltipPortalContainer ) ;
69
63
}
70
64
return tooltipPortalContainer ;
71
- }
65
+ } ;
66
+
67
+ const TOOLTIP_CLASSES = {
68
+ container : classNames (
69
+ "fixed z-[9999] px-3 py-2 text-xs font-medium bg-white border border-gray-200 rounded-lg shadow-lg" ,
70
+ "dark:bg-[#2C2F33] dark:border-white/20 dark:text-gray-200" ,
71
+ "min-w-[180px]" ,
72
+ "animate-fade"
73
+ ) ,
74
+ button : "flex items-center justify-center w-4 h-4 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 focus:outline-none focus:ring-1 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900 rounded-full transition-colors"
75
+ } ;
72
76
73
77
export const ProfileInfoTooltip : FC < ProfileInfoTooltipProps > = ( { profile, className } ) => {
74
78
const [ isVisible , setIsVisible ] = useState ( false ) ;
@@ -78,33 +82,24 @@ export const ProfileInfoTooltip: FC<ProfileInfoTooltipProps> = ({ profile, class
78
82
const port = getPortFromAdvanced ( profile ) ;
79
83
const lastAccessed = getLastAccessedTime ( profile . Id ) ;
80
84
81
- const hasInfo = port !== null || lastAccessed !== null ;
82
- if ( ! hasInfo ) {
83
- return null ;
84
- }
85
+ if ( ! port && ! lastAccessed ) return null ;
85
86
86
87
const showTooltip = useCallback ( ( ) => {
87
- if ( btnRef . current ) {
88
- const rect = btnRef . current . getBoundingClientRect ( ) ;
89
- setTooltipPos ( {
90
- top : rect . top + rect . height / 2 ,
91
- left : rect . right + 12 ,
92
- } ) ;
93
- }
88
+ if ( ! btnRef . current ) return ;
89
+
90
+ const rect = btnRef . current . getBoundingClientRect ( ) ;
91
+ setTooltipPos ( {
92
+ top : rect . top + rect . height / 2 ,
93
+ left : rect . right + TOOLTIP_OFFSET ,
94
+ } ) ;
94
95
setIsVisible ( true ) ;
95
96
} , [ ] ) ;
96
97
97
- const hideTooltip = useCallback ( ( ) => {
98
- setIsVisible ( false ) ;
99
- } , [ ] ) ;
98
+ const hideTooltip = useCallback ( ( ) => setIsVisible ( false ) , [ ] ) ;
100
99
101
100
const handleClickAway = useCallback ( ( event : MouseEvent ) => {
102
- if (
103
- btnRef . current &&
104
- ! btnRef . current . contains ( event . target as Node )
105
- ) {
106
- setIsVisible ( false ) ;
107
- }
101
+ if ( btnRef . current ?. contains ( event . target as Node ) ) return ;
102
+ setIsVisible ( false ) ;
108
103
} , [ ] ) ;
109
104
110
105
const handleKeyDown = useCallback ( ( event : KeyboardEvent ) => {
@@ -123,55 +118,45 @@ export const ProfileInfoTooltip: FC<ProfileInfoTooltipProps> = ({ profile, class
123
118
} ;
124
119
} , [ isVisible , handleClickAway , handleKeyDown ] ) ;
125
120
126
- const portalContainer = useMemo ( ( ) => getTooltipPortalContainer ( ) , [ ] ) ;
127
-
128
- const tooltip = isVisible && tooltipPos
129
- ? createPortal (
130
- < div
131
- id = { `tooltip-${ profile . Id } ` }
132
- role = "tooltip"
133
- className = { classNames (
134
- "fixed z-[9999] px-3 py-2 text-xs font-medium bg-white border border-gray-200 rounded-lg shadow-lg" ,
135
- "dark:bg-[#2C2F33] dark:border-white/20 dark:text-gray-200" ,
136
- "min-w-[180px]" ,
137
- "animate-fade"
138
- ) }
139
- style = { {
140
- top : tooltipPos . top ,
141
- left : tooltipPos . left ,
142
- transform : "translateY(-50%)" ,
143
- } }
144
- >
145
- < div className = "space-y-1" >
146
- { port !== null && (
147
- < div className = "flex justify-between" >
148
- < span className = "text-gray-600 dark:text-gray-400" > Port:</ span >
149
- < span className = { ClassNames . Text } > { port } </ span >
150
- </ div >
151
- ) }
152
- { lastAccessed !== null && (
153
- < div className = "flex justify-between" >
154
- < span className = "text-gray-600 dark:text-gray-400" > Last Logged In: </ span >
155
- < span className = { ClassNames . Text } > { lastAccessed } </ span >
156
- </ div >
157
- ) }
121
+ const portalContainer = useMemo ( getTooltipPortalContainer , [ ] ) ;
122
+
123
+ const tooltip = isVisible && tooltipPos && createPortal (
124
+ < div
125
+ id = { `tooltip-${ profile . Id } ` }
126
+ role = "tooltip"
127
+ className = { TOOLTIP_CLASSES . container }
128
+ style = { {
129
+ top : tooltipPos . top ,
130
+ left : tooltipPos . left ,
131
+ transform : "translateY(-50%)" ,
132
+ } }
133
+ >
134
+ < div className = "space-y-1" >
135
+ { port && (
136
+ < div className = "flex justify-between" >
137
+ < span className = "text-gray-600 dark:text-gray-400" > Port:</ span >
138
+ < span className = { ClassNames . Text } > { port } </ span >
158
139
</ div >
159
- < div
160
- className = "absolute top-1/2 left-0 -translate-x-full -translate-y-1/2"
161
- style = { { } }
162
- >
163
- < div className = "w-0 h-0 border-t-4 border-b-4 border-r-4 border-t-transparent border-b-transparent border-r-gray-200 dark:border-r-white/20" > </ div >
140
+ ) }
141
+ { lastAccessed && (
142
+ < div className = "flex justify-between" >
143
+ < span className = "text-gray-600 dark:text-gray-400" > Last Logged In: </ span >
144
+ < span className = { ClassNames . Text } > { lastAccessed } </ span >
164
145
</ div >
165
- </ div > ,
166
- portalContainer
167
- )
168
- : null ;
146
+ ) }
147
+ </ div >
148
+ < div className = "absolute top-1/2 left-0 -translate-x-full -translate-y-1/2" >
149
+ < div className = "w-0 h-0 border-t-4 border-b-4 border-r-4 border-t-transparent border-b-transparent border-r-gray-200 dark:border-r-white/20" />
150
+ </ div >
151
+ </ div > ,
152
+ portalContainer
153
+ ) ;
169
154
170
155
return (
171
156
< div className = { classNames ( "relative" , className ) } >
172
157
< button
173
158
ref = { btnRef }
174
- className = "flex items-center justify-center w-4 h-4 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 focus:outline-none focus:ring-1 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-gray-900 rounded-full transition-colors"
159
+ className = { TOOLTIP_CLASSES . button }
175
160
onClick = { isVisible ? hideTooltip : showTooltip }
176
161
aria-label = { `Profile information for ${ profile . Id } ` }
177
162
aria-describedby = { `tooltip-${ profile . Id } ` }
@@ -185,14 +170,12 @@ export const ProfileInfoTooltip: FC<ProfileInfoTooltipProps> = ({ profile, class
185
170
) ;
186
171
} ;
187
172
188
- export function updateProfileLastAccessed ( profileId : string ) : void {
189
- if ( ! isValidProfileId ( profileId ) ) {
190
- return ;
191
- }
173
+ export const updateProfileLastAccessed = ( profileId : string ) : void => {
174
+ if ( ! isValidProfileId ( profileId ) ) return ;
192
175
193
176
try {
194
177
localStorage . setItem ( `whodb_profile_last_accessed_${ profileId } ` , new Date ( ) . toISOString ( ) ) ;
195
- } catch ( error ) {
196
-
178
+ } catch {
179
+ // Silently fail if localStorage is not available
197
180
}
198
- }
181
+ } ;
0 commit comments