Skip to content

Commit 5c0102f

Browse files
authored
feat: add account dialog in main menu (#48)
* chore: update dependencies in package.json and yarn.lock - Added @types/crypto-js and crypto-js to package.json for enhanced type definitions and cryptographic functionality. - Updated yarn.lock to reflect the addition of new dependencies and their respective versions. * feat: add AccountDialog component with user profile display - Introduced AccountDialog component for displaying user account information. - Implemented loading and error states for user profile data retrieval. - Added styles in AccountDialog.scss for layout and visual presentation of the dialog. - Integrated Gravatar for user avatars based on email. * feat: enhance MainMenu with user account functionality - Added AccountDialog component to display user account information. - Integrated Gravatar for user avatars based on email. - Implemented account modal toggle in MainMenu for improved user experience. - Updated user profile handling to include email for avatar generation.
1 parent cb09983 commit 5c0102f

File tree

5 files changed

+256
-10
lines changed

5 files changed

+256
-10
lines changed

src/frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"@monaco-editor/react": "^4.7.0",
88
"@tanstack/react-query": "^5.74.3",
99
"@tanstack/react-query-devtools": "^5.74.3",
10+
"@types/crypto-js": "^4.2.2",
1011
"browser-fs-access": "0.29.1",
12+
"crypto-js": "^4.2.0",
1113
"lucide-react": "^0.488.0",
1214
"posthog-js": "^1.236.0",
1315
"react": "19.0.0",
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
.account-dialog {
2+
3+
&__wrapper {
4+
position: absolute;
5+
top: 0;
6+
left: 0;
7+
right: 0;
8+
bottom: 0;
9+
z-index: 1000;
10+
background-color: rgba(0, 0, 0, 0.2);
11+
backdrop-filter: blur(1px);
12+
}
13+
14+
&__title-container {
15+
display: flex;
16+
align-items: center;
17+
justify-content: space-between;
18+
}
19+
20+
&__title {
21+
margin: 0;
22+
font-size: 1.2rem;
23+
font-weight: 600;
24+
}
25+
26+
&__content {
27+
padding: 1rem;
28+
min-height: 200px;
29+
display: flex;
30+
flex-direction: column;
31+
}
32+
33+
&__loading, &__error {
34+
display: flex;
35+
align-items: center;
36+
justify-content: center;
37+
min-height: 200px;
38+
font-size: 1rem;
39+
color: var(--text-primary-color);
40+
}
41+
42+
&__error {
43+
color: #e74c3c;
44+
}
45+
46+
&__profile {
47+
display: flex;
48+
align-items: flex-start;
49+
padding: 1.25rem;
50+
border-radius: 8px;
51+
background-color: var(--dialog-bg-color);
52+
}
53+
54+
&__avatar {
55+
margin-right: 1.25rem;
56+
}
57+
58+
&__gravatar {
59+
width: 90px;
60+
height: 90px;
61+
border-radius: 50%;
62+
object-fit: cover;
63+
border: 2px solid var(--text-primary-color);
64+
}
65+
66+
&__user-info {
67+
flex: 1;
68+
}
69+
70+
&__name {
71+
margin: 0 0 0 0;
72+
font-size: 1.3rem;
73+
font-weight: 600;
74+
color: var(--text-primary-color);
75+
display: flex;
76+
align-items: center;
77+
flex-wrap: wrap;
78+
gap: 0.5rem;
79+
}
80+
81+
&__username {
82+
margin: 0 0 1.5rem 0;
83+
font-size: 0.95rem;
84+
color: var(--text-secondary-color);
85+
}
86+
87+
&__user-id {
88+
margin: 0;
89+
font-size: 0.75rem;
90+
font-family: monospace;
91+
color: var(--text-secondary-color);
92+
opacity: 0.7;
93+
}
94+
95+
&__verified {
96+
font-size: 0.75rem;
97+
color: #2ecc71;
98+
background-color: rgba(46, 204, 113, 0.1);
99+
padding: 0.15rem 0.4rem;
100+
border-radius: 4px;
101+
display: inline-flex;
102+
align-items: center;
103+
vertical-align: middle;
104+
}
105+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import React, { useState, useCallback } from "react";
2+
import { Dialog } from "@atyrode/excalidraw";
3+
import { useUserProfile } from "../api/hooks";
4+
import md5 from 'crypto-js/md5';
5+
import "./AccountDialog.scss";
6+
7+
interface AccountDialogProps {
8+
excalidrawAPI?: any;
9+
onClose?: () => void;
10+
}
11+
12+
// Function to generate gravatar URL
13+
const getGravatarUrl = (email: string, size = 100) => {
14+
const hash = md5(email.toLowerCase().trim()).toString();
15+
return `https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon`;
16+
};
17+
18+
const AccountDialog: React.FC<AccountDialogProps> = ({
19+
excalidrawAPI,
20+
onClose,
21+
}) => {
22+
const [modalIsShown, setModalIsShown] = useState(true);
23+
const { data: profile, isLoading, isError } = useUserProfile();
24+
25+
const handleClose = useCallback(() => {
26+
setModalIsShown(false);
27+
if (onClose) {
28+
onClose();
29+
}
30+
}, [onClose]);
31+
32+
// Dialog content with user profile information
33+
const dialogContent = (
34+
<div className="account-dialog__content">
35+
{isLoading && (
36+
<div className="account-dialog__loading">
37+
Loading account information...
38+
</div>
39+
)}
40+
41+
{isError && (
42+
<div className="account-dialog__error">
43+
Error loading account information. Please try again later.
44+
</div>
45+
)}
46+
47+
{profile && !isLoading && !isError && (
48+
<div className="account-dialog__profile">
49+
<div className="account-dialog__avatar">
50+
<img
51+
src={getGravatarUrl(profile.email)}
52+
alt={profile.username}
53+
className="account-dialog__gravatar"
54+
/>
55+
</div>
56+
<div className="account-dialog__user-info">
57+
<h2 className="account-dialog__name">
58+
{profile.name || profile.username}
59+
{profile.email_verified && (
60+
<span className="account-dialog__verified">✓ Verified</span>
61+
)}
62+
</h2>
63+
<p className="account-dialog__username">{profile.username}</p>
64+
<p className="account-dialog__user-id">{profile.id}</p>
65+
</div>
66+
</div>
67+
)}
68+
</div>
69+
);
70+
71+
return (
72+
<>
73+
{modalIsShown && (
74+
<div className="account-dialog__wrapper">
75+
<Dialog
76+
className="account-dialog"
77+
size="small"
78+
onCloseRequest={handleClose}
79+
title={
80+
<div className="account-dialog__title-container">
81+
<h2 className="account-dialog__title">Account</h2>
82+
</div>
83+
}
84+
closeOnClickOutside={true}
85+
children={dialogContent}
86+
/>
87+
</div>
88+
)}
89+
</>
90+
);
91+
};
92+
93+
export default AccountDialog;

src/frontend/src/ui/MainMenu.tsx

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@ import type { ExcalidrawImperativeAPI } from '@atyrode/excalidraw/types';
44
import type { MainMenu as MainMenuType } from '@atyrode/excalidraw';
55

66
import { LogOut, SquarePlus, LayoutDashboard, SquareCode, Eye, Coffee, Grid2x2, User, Text, ArchiveRestore, Settings, Terminal } from 'lucide-react';
7+
import AccountDialog from './AccountDialog';
8+
import md5 from 'crypto-js/md5';
79
import { capture } from '../utils/posthog';
810
import { ExcalidrawElementFactory, PlacementMode } from '../lib/ExcalidrawElementFactory';
911
import { useUserProfile } from "../api/hooks";
1012
import { queryClient } from "../api/queryClient";
11-
import SettingsDialog from "./SettingsDialog";
1213
import "./MainMenu.scss";
14+
15+
// Function to generate gravatar URL
16+
const getGravatarUrl = (email: string, size = 32) => {
17+
const hash = md5(email.toLowerCase().trim()).toString();
18+
return `https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon`;
19+
};
1320
interface MainMenuConfigProps {
1421
MainMenu: typeof MainMenuType;
1522
excalidrawAPI: ExcalidrawImperativeAPI | null;
@@ -22,20 +29,21 @@ interface MainMenuConfigProps {
2229
export const MainMenuConfig: React.FC<MainMenuConfigProps> = ({
2330
MainMenu,
2431
excalidrawAPI,
25-
showBackupsModal,
2632
setShowBackupsModal,
27-
showSettingsModal = false,
2833
setShowSettingsModal = (show: boolean) => {},
2934
}) => {
35+
const [showAccountModal, setShowAccountModal] = useState(false);
3036
const { data, isLoading, isError } = useUserProfile();
3137

3238
let username = "";
39+
let email = "";
3340
if (isLoading) {
3441
username = "Loading...";
3542
} else if (isError || !data?.username) {
3643
username = "Unknown";
3744
} else {
3845
username = data.username;
46+
email = data.email || "";
3947
}
4048
const handleHtmlEditorClick = () => {
4149
if (!excalidrawAPI) return;
@@ -124,6 +132,10 @@ export const MainMenuConfig: React.FC<MainMenuConfigProps> = ({
124132
const handleSettingsClick = () => {
125133
setShowSettingsModal(true);
126134
};
135+
136+
const handleAccountClick = () => {
137+
setShowAccountModal(true);
138+
};
127139

128140
const handleGridToggle = () => {
129141
if (!excalidrawAPI) return;
@@ -155,13 +167,29 @@ export const MainMenuConfig: React.FC<MainMenuConfigProps> = ({
155167
};
156168

157169
return (
158-
<MainMenu>
159-
<div className="main-menu__top-row">
160-
<span className="main-menu__label">
161-
<User width={20} height={20} />
162-
<span className="main-menu__label-username">{username}</span>
163-
</span>
164-
</div>
170+
<>
171+
{showAccountModal && (
172+
<AccountDialog
173+
excalidrawAPI={excalidrawAPI}
174+
onClose={() => setShowAccountModal(false)}
175+
/>
176+
)}
177+
<MainMenu>
178+
<div className="main-menu__top-row">
179+
<span className="main-menu__label" style={{ gap: 0.2 }}>
180+
{email && (
181+
<img
182+
src={getGravatarUrl(email)}
183+
alt={username}
184+
className="main-menu__gravatar"
185+
width={20}
186+
height={20}
187+
style={{ borderRadius: '50%', marginRight: '8px' }}
188+
/>
189+
)}
190+
<span className="main-menu__label-username">{username}</span>
191+
</span>
192+
</div>
165193
<MainMenu.Separator />
166194

167195
<MainMenu.Group title="Files">
@@ -238,6 +266,13 @@ export const MainMenuConfig: React.FC<MainMenuConfigProps> = ({
238266

239267
<MainMenu.Separator />
240268

269+
<MainMenu.Item
270+
icon={<User />}
271+
onClick={handleAccountClick}
272+
>
273+
Account
274+
</MainMenu.Item>
275+
241276
<MainMenu.Item
242277
icon={<Settings />}
243278
onClick={handleSettingsClick}
@@ -274,5 +309,6 @@ export const MainMenuConfig: React.FC<MainMenuConfigProps> = ({
274309
</MainMenu.Item>
275310

276311
</MainMenu>
312+
</>
277313
);
278314
};

src/frontend/yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,11 @@
561561
dependencies:
562562
"@tanstack/query-core" "5.74.7"
563563

564+
"@types/crypto-js@^4.2.2":
565+
version "4.2.2"
566+
resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea"
567+
integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==
568+
564569
"@types/d3-scale-chromatic@^3.0.0":
565570
version "3.1.0"
566571
resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39"
@@ -719,6 +724,11 @@ cross-spawn@^7.0.1:
719724
shebang-command "^2.0.0"
720725
which "^2.0.1"
721726

727+
crypto-js@^4.2.0:
728+
version "4.2.0"
729+
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
730+
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
731+
722732
cytoscape-cose-bilkent@^4.1.0:
723733
version "4.1.0"
724734
resolved "https://registry.yarnpkg.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz#762fa121df9930ffeb51a495d87917c570ac209b"

0 commit comments

Comments
 (0)