Skip to content

Commit c7db6dc

Browse files
committed
Adds user profile menu and updates auth flow
Enables avatar-based dropdown for improved user engagement Fetches profile data from GraphQL and updates context for personalization Replaces default icon to enhance brand identity
1 parent c704e82 commit c7db6dc

File tree

6 files changed

+231
-10
lines changed

6 files changed

+231
-10
lines changed

editor/public/images/app-icon.svg

Lines changed: 1 addition & 0 deletions
Loading

editor/src/components/TopBar.tsx

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useRef, useEffect } from 'react';
22
import { FacilitySelectorPortal } from './FacilitySelectorPortal';
33
import { BuildVersion } from './BuildVersion';
44
import { useAuth } from '../lib/AuthProvider';
@@ -22,7 +22,27 @@ export const TopBar: React.FC<TopBarProps> = ({
2222
selectedFacilityId,
2323
onFacilitySelect
2424
}) => {
25-
const { isAuthenticated, isLoading, logout } = useAuth();
25+
const { isAuthenticated, isLoading, logout, profile } = useAuth();
26+
const [menuOpen, setMenuOpen] = useState(false);
27+
const avatarRef = useRef<HTMLDivElement>(null);
28+
29+
// Close menu on outside click for avatar menu
30+
useEffect(() => {
31+
if (!menuOpen) return;
32+
const handleClick = (e: MouseEvent) => {
33+
if (avatarRef.current && !avatarRef.current.contains(e.target as Node)) {
34+
setMenuOpen(false);
35+
}
36+
};
37+
document.addEventListener('mousedown', handleClick);
38+
return () => document.removeEventListener('mousedown', handleClick);
39+
}, [menuOpen]);
40+
41+
const handleLogoutClick = () => {
42+
console.log('Logging out user');
43+
logout();
44+
setMenuOpen(false);
45+
};
2646

2747
const handleFacilitySelect = (facility: Facility | null) => {
2848
onFacilitySelect(facility);
@@ -55,13 +75,49 @@ export const TopBar: React.FC<TopBarProps> = ({
5575
<div className="top-bar-auth">
5676
{isLoading ? (
5777
<span className="auth-loading">🔄</span>
58-
) : isAuthenticated ? (
59-
<button onClick={logout} className="auth-button logout">
60-
🔓 Logout
61-
</button>
6278
) : null}
6379
</div>
6480
<BuildVersion />
81+
{profile && (
82+
<>
83+
<div
84+
ref={avatarRef}
85+
className="top-bar-user-image top-bar-user-image-clickable"
86+
onClick={() => {
87+
setMenuOpen((v) => !v);
88+
}}
89+
style={{ display: 'inline-block', position: 'relative' }}
90+
>
91+
{profile.picture && (
92+
<img src={profile.picture} alt={profile.fullName || ''} />
93+
)}
94+
</div>
95+
{menuOpen && (
96+
<div
97+
className="top-bar-user-menu"
98+
style={{
99+
position: 'absolute',
100+
right: 0,
101+
top: '56px',
102+
zIndex: 1000
103+
}}
104+
>
105+
<div className="top-bar-user-menu-title">{profile.fullName || ''}</div>
106+
<hr className="top-bar-user-menu-divider" />
107+
<button
108+
className="top-bar-user-menu-item top-bar-user-menu-link"
109+
onMouseDown={handleLogoutClick}
110+
>
111+
<span className="top-bar-user-menu-row">
112+
{/* Logout SVG */}
113+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M1.404 1.985a1 1 0 0 1 1-1H8.9a1 1 0 0 1 1 1v3.143a.5.5 0 0 1-1 0V1.985H2.404v12.03H8.9v-3.003a.5.5 0 0 1 1 0v3.003a1 1 0 0 1-1 1H2.404a1 1 0 0 1-1-1V1.985Z" fill="#414141"/><path fillRule="evenodd" clipRule="evenodd" d="M11.721 5.533a.5.5 0 0 1 .707.024l1.967 2.102a.6.6 0 0 1-.007.827l-1.966 2.033a.5.5 0 0 1-.719-.695l1.213-1.254H5.897a.5.5 0 1 1 0-1h7.045l-1.244-1.33a.5.5 0 0 1 .023-.707Z" fill="#414141"/></svg>
114+
Log Out
115+
</span>
116+
</button>
117+
</div>
118+
)}
119+
</>
120+
)}
65121
</div>
66122
</div>
67123
</div>

editor/src/graphql/queries.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
// Query to get current user profile after login
2+
export const GET_ME = gql`
3+
query getMe {
4+
me {
5+
profile {
6+
id
7+
dbId
8+
fullName
9+
picture
10+
}
11+
}
12+
}
13+
`;
114
import { gql } from 'urql';
215

316
// =============================================================================

editor/src/lib/AuthProvider.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
22
import { authService } from './auth-service';
3+
import { GET_ME } from '../graphql/queries';
4+
import { useQuery } from 'urql';
5+
6+
// Dummy usage to trigger codegen
7+
const _dummy = GET_ME;
38

49
interface AuthContextType {
510
isAuthenticated: boolean;
@@ -10,6 +15,7 @@ interface AuthContextType {
1015
logout: () => Promise<void>;
1116
refreshAuth: () => Promise<void>;
1217
tokenInfo: { isValid: boolean; expiresAt?: Date; scope?: string };
18+
profile: Profile | null;
1319
}
1420

1521
const AuthContext = createContext<AuthContextType | undefined>(undefined);
@@ -18,10 +24,17 @@ interface AuthProviderProps {
1824
children: ReactNode;
1925
}
2026

27+
interface Profile {
28+
id: string;
29+
dbId: number;
30+
fullName: string;
31+
picture?: string;
32+
}
2133
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
2234
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
2335
const [isLoading, setIsLoading] = useState<boolean>(true);
2436
const [error, setError] = useState<string | null>(null);
37+
const [profile, setProfile] = useState<Profile | null>(null);
2538

2639
// Check authentication status on mount
2740
useEffect(() => {
@@ -97,6 +110,13 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
97110
};
98111

99112
const tokenInfo = authService.getTokenInfo();
113+
// Fetch user profile after authentication
114+
const [{ data, fetching }] = useQuery({ query: GET_ME, pause: !isAuthenticated });
115+
React.useEffect(() => {
116+
if (data?.me?.profile) {
117+
setProfile(data.me.profile);
118+
}
119+
}, [data]);
100120

101121
const value: AuthContextType = {
102122
isAuthenticated,
@@ -107,6 +127,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
107127
logout,
108128
refreshAuth,
109129
tokenInfo,
130+
profile,
110131
};
111132

112133
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;

editor/src/main.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { AuthProvider } from './lib/AuthProvider';
66

77
ReactDOM.createRoot(document.getElementById('root')!).render(
88
<React.StrictMode>
9-
<AuthProvider>
10-
<GraphQLProvider>
9+
<GraphQLProvider>
10+
<AuthProvider>
1111
<App />
12-
</GraphQLProvider>
13-
</AuthProvider>
12+
</AuthProvider>
13+
</GraphQLProvider>
1414
</React.StrictMode>
1515
);

editor/src/treeview.css

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,131 @@
1+
/* Menu item icon + label row */
2+
.top-bar-user-menu-row {
3+
display: flex;
4+
align-items: center;
5+
gap: 12px;
6+
}
7+
8+
/* Menu link styling */
9+
.top-bar-user-menu-link {
10+
text-decoration: none;
11+
background: none;
12+
border: none;
13+
width: 100%;
14+
text-align: left;
15+
color: inherit;
16+
padding: 0;
17+
}
18+
/* Top bar app icon styling */
19+
.top-bar-app-icon {
20+
height: 32px;
21+
width: 32px;
22+
display: inline-block;
23+
margin-right: 8px;
24+
}
25+
.top-bar-user-menu {
26+
position: absolute;
27+
top: 56px;
28+
right: 0;
29+
margin-top: -16px;
30+
background-color: rgb(255, 255, 255);
31+
color: rgba(0, 0, 0, 0.87);
32+
box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 5px -3px, rgba(0, 0, 0, 0.14) 0px 8px 10px 1px, rgba(0, 0, 0, 0.12) 0px 3px 14px 2px;
33+
overflow: visible;
34+
min-width: 240px;
35+
max-width: 320px;
36+
border-radius: 18px;
37+
z-index: 2000;
38+
display: flex;
39+
flex-direction: column;
40+
align-items: center;
41+
border: none;
42+
}
43+
44+
45+
.top-bar-user-menu-title {
46+
margin: 0px;
47+
--base: 1rem;
48+
/* font-family removed */
49+
font-size: calc(var(--base) * 0.8125);
50+
font-weight: 700;
51+
line-height: 1.4;
52+
padding: 10px;
53+
font-size: 14px;
54+
text-align: left;
55+
}
56+
.top-bar-user-menu-divider {
57+
display: block;
58+
margin-block-start: 0.5em;
59+
margin-block-end: 0.5em;
60+
margin-inline-start: auto;
61+
margin-inline-end: auto;
62+
color: gray;
63+
unicode-bidi: isolate;
64+
overflow: hidden;
65+
border-style: inset;
66+
border-width: 1px;
67+
width: -webkit-fill-available;
68+
}
69+
.top-bar-user-menu-item {
70+
margin: 0px;
71+
color: rgb(65, 65, 65);
72+
--base: 1rem;
73+
/* font-family removed */
74+
font-size: calc(var(--base) * 0.8125);
75+
font-weight: 400;
76+
line-height: 1.4;
77+
background: none;
78+
border: none;
79+
text-align: left;
80+
padding: 10px;
81+
cursor: pointer;
82+
border-radius: 6px;
83+
transition: background 0.15s;
84+
display: flex;
85+
align-items: center;
86+
}
87+
.top-bar-user-menu-item:hover {
88+
background: #f8f8f8;
89+
}
90+
.top-bar-user-image img {
91+
width: 100%;
92+
height: 100%;
93+
object-fit: cover;
94+
}
95+
/* Top bar user image styling */
96+
.top-bar-user-image {
97+
position: relative;
98+
display: flex;
99+
align-items: center;
100+
justify-content: center;
101+
flex-shrink: 0;
102+
/* font-family removed */
103+
font-size: 1.25rem;
104+
line-height: 1;
105+
border-radius: 50%;
106+
overflow: hidden;
107+
width: 32px;
108+
height: 32px;
109+
-webkit-user-select: none;
110+
user-select: none;
111+
}
112+
/* Remove incorrect width/height from menu, keep only for avatar */
113+
.top-bar-user-menu {
114+
overflow: visible;
115+
}
116+
.top-bar-user-image-clickable {
117+
cursor: pointer;
118+
width: 32px;
119+
height: 32px;
120+
background-color: #fff;
121+
margin-left: 12px;
122+
}
123+
/* Top bar user name styling */
124+
.top-bar-user {
125+
margin-left: 16px;
126+
font-weight: 500;
127+
color: #3b82f6;
128+
}
1129
.node-editor {
2130
display: flex;
3131
flex-direction: column;
@@ -49,6 +177,8 @@ html, body, #root { height:100%; font-family: system-ui, 'Segoe UI', Roboto, Oxy
49177
display: flex;
50178
align-items: center;
51179
gap: 12px;
180+
position: relative;
181+
overflow: visible;
52182
}
53183

54184
.top-bar-auth {

0 commit comments

Comments
 (0)