@slyxup/ui 0.1.0 → 0.2.0

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +105 -0
  3. package/dist/components/EmailVerification/EmailVerification.d.ts +9 -0
  4. package/dist/components/EmailVerification/EmailVerification.d.ts.map +1 -0
  5. package/dist/components/EmailVerification/EmailVerification.js +72 -0
  6. package/dist/components/EmailVerification/EmailVerification.js.map +1 -0
  7. package/dist/components/ForgotPassword/ForgotPassword.d.ts +8 -0
  8. package/dist/components/ForgotPassword/ForgotPassword.d.ts.map +1 -0
  9. package/dist/components/ForgotPassword/ForgotPassword.js +43 -0
  10. package/dist/components/ForgotPassword/ForgotPassword.js.map +1 -0
  11. package/dist/components/ResetPassword/ResetPassword.d.ts +9 -0
  12. package/dist/components/ResetPassword/ResetPassword.d.ts.map +1 -0
  13. package/dist/components/ResetPassword/ResetPassword.js +50 -0
  14. package/dist/components/ResetPassword/ResetPassword.js.map +1 -0
  15. package/dist/components/SignIn/SignIn.d.ts +11 -0
  16. package/dist/components/SignIn/SignIn.d.ts.map +1 -0
  17. package/dist/components/SignIn/SignIn.js +42 -0
  18. package/dist/components/SignIn/SignIn.js.map +1 -0
  19. package/dist/components/SignUp/SignUp.d.ts +8 -0
  20. package/dist/components/SignUp/SignUp.d.ts.map +1 -0
  21. package/dist/components/SignUp/SignUp.js +43 -0
  22. package/dist/components/SignUp/SignUp.js.map +1 -0
  23. package/dist/components/SocialButtons/SocialButtons.d.ts +9 -0
  24. package/dist/components/SocialButtons/SocialButtons.d.ts.map +1 -0
  25. package/dist/components/SocialButtons/SocialButtons.js +16 -0
  26. package/dist/components/SocialButtons/SocialButtons.js.map +1 -0
  27. package/dist/components/UserButton/UserButton.d.ts +3 -0
  28. package/dist/components/UserButton/UserButton.d.ts.map +1 -0
  29. package/dist/components/UserButton/UserButton.js +43 -0
  30. package/dist/components/UserButton/UserButton.js.map +1 -0
  31. package/dist/components/UserProfile/UserProfile.d.ts +3 -0
  32. package/dist/components/UserProfile/UserProfile.d.ts.map +1 -0
  33. package/dist/components/UserProfile/UserProfile.js +38 -0
  34. package/dist/components/UserProfile/UserProfile.js.map +1 -0
  35. package/dist/icons.d.ts +6 -0
  36. package/dist/icons.d.ts.map +1 -0
  37. package/dist/icons.js +15 -0
  38. package/dist/icons.js.map +1 -0
  39. package/dist/index.d.ts +15 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +22 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/styles.d.ts +8 -0
  44. package/dist/styles.d.ts.map +1 -0
  45. package/dist/styles.js +198 -0
  46. package/dist/styles.js.map +1 -0
  47. package/package.json +30 -2
  48. package/src/components/EmailVerification/EmailVerification.tsx +180 -0
  49. package/src/components/ForgotPassword/ForgotPassword.tsx +122 -0
  50. package/src/components/ResetPassword/ResetPassword.tsx +120 -0
  51. package/src/components/SignIn/SignIn.tsx +143 -0
  52. package/src/components/SignUp/SignUp.tsx +156 -0
  53. package/src/components/SocialButtons/SocialButtons.tsx +39 -0
  54. package/src/components/UserButton/UserButton.tsx +89 -0
  55. package/src/components/UserProfile/UserProfile.tsx +93 -0
  56. package/src/icons.tsx +71 -0
  57. package/src/index.ts +37 -1
  58. package/src/styles.ts +199 -0
@@ -0,0 +1,122 @@
1
+ import { SlyxupClient } from '@slyxup/core';
2
+ import { type FormEvent, useEffect, useState } from 'react';
3
+ import { CheckIcon, KeyholeMark } from '../../icons';
4
+
5
+ export interface ForgotPasswordProps {
6
+ apiUrl?: string;
7
+ onSuccess?: () => void;
8
+ onBackToSignIn?: () => void;
9
+ }
10
+
11
+ /** Request a password-reset email. */
12
+ export function ForgotPassword({
13
+ apiUrl,
14
+ onSuccess,
15
+ onBackToSignIn,
16
+ }: ForgotPasswordProps) {
17
+ const [email, setEmail] = useState('');
18
+ const [sent, setSent] = useState(false);
19
+ const [busy, setBusy] = useState(false);
20
+ const [error, setError] = useState<string | null>(null);
21
+
22
+ useEffect(() => {
23
+ if (error) {
24
+ const t = setTimeout(() => setError(null), 4000);
25
+ return () => clearTimeout(t);
26
+ }
27
+ }, [error]);
28
+
29
+ async function onSubmit(e: FormEvent) {
30
+ e.preventDefault();
31
+ setBusy(true);
32
+ setError(null);
33
+ try {
34
+ await fetch(
35
+ `${(apiUrl ?? process.env.NEXT_PUBLIC_SLYXUP_API_URL ?? 'https://auth.slyxup.online').replace(/\/$/, '')}/v1/verification/password/forgot`,
36
+ {
37
+ method: 'POST',
38
+ headers: { 'Content-Type': 'application/json' },
39
+ body: JSON.stringify({ email }),
40
+ }
41
+ );
42
+ setSent(true); // always success — never reveal account existence
43
+ onSuccess?.();
44
+ } catch {
45
+ setError('Network problem. Check your connection and try again.');
46
+ } finally {
47
+ setBusy(false);
48
+ }
49
+ void SlyxupClient; // tree-shake guard
50
+ }
51
+
52
+ if (sent) {
53
+ return (
54
+ <div className="slx-card">
55
+ <div className="slx-success-icon">
56
+ <CheckIcon />
57
+ </div>
58
+ <h1 className="slx-title" style={{ textAlign: 'center' }}>
59
+ Check your email
60
+ </h1>
61
+ <p className="slx-subtitle" style={{ textAlign: 'center' }}>
62
+ If an account exists for {email}, a reset link is on its way.
63
+ </p>
64
+ {onBackToSignIn && (
65
+ <p className="slx-footer">
66
+ <button type="button" className="slx-link" onClick={onBackToSignIn}>
67
+ Back to sign in
68
+ </button>
69
+ </p>
70
+ )}
71
+ </div>
72
+ );
73
+ }
74
+
75
+ return (
76
+ <div className={`slx-card${error ? ' slx-card-error' : ''}`}>
77
+ <div className="slx-mark">
78
+ <KeyholeMark />
79
+ </div>
80
+ <h1 className="slx-title">Reset your password</h1>
81
+ <p className="slx-subtitle">
82
+ Enter your email and we&apos;ll send you a reset link.
83
+ </p>
84
+
85
+ {error && (
86
+ <p className="slx-error-text" role="alert">
87
+ {error}
88
+ </p>
89
+ )}
90
+
91
+ <form onSubmit={onSubmit}>
92
+ <div className="slx-field">
93
+ <label className="slx-label" htmlFor="slx-forgot-email">
94
+ Email
95
+ </label>
96
+ <input
97
+ id="slx-forgot-email"
98
+ className="slx-input"
99
+ type="email"
100
+ autoComplete="email"
101
+ placeholder="you@example.com"
102
+ value={email}
103
+ onChange={(e) => setEmail(e.target.value)}
104
+ required
105
+ />
106
+ </div>
107
+ <button className="slx-btn" type="submit" disabled={busy}>
108
+ {busy && <span className="slx-spinner" aria-hidden="true" />}
109
+ {busy ? 'Sending…' : 'Send reset link'}
110
+ </button>
111
+ </form>
112
+
113
+ {onBackToSignIn && (
114
+ <p className="slx-footer">
115
+ <button type="button" className="slx-link" onClick={onBackToSignIn}>
116
+ Back to sign in
117
+ </button>
118
+ </p>
119
+ )}
120
+ </div>
121
+ );
122
+ }
@@ -0,0 +1,120 @@
1
+ import { type FormEvent, useEffect, useState } from 'react';
2
+ import { CheckIcon, KeyholeMark } from '../../icons';
3
+
4
+ export interface ResetPasswordProps {
5
+ /** Reset token (from email link ?token=...) */
6
+ token: string;
7
+ apiUrl?: string;
8
+ onSuccess?: () => void;
9
+ }
10
+
11
+ /** Set a new password using the emailed reset token. */
12
+ export function ResetPassword({
13
+ token,
14
+ apiUrl,
15
+ onSuccess,
16
+ }: ResetPasswordProps) {
17
+ const [password, setPassword] = useState('');
18
+ const [done, setDone] = useState(false);
19
+ const [busy, setBusy] = useState(false);
20
+ const [error, setError] = useState<string | null>(null);
21
+
22
+ useEffect(() => {
23
+ if (error) {
24
+ const t = setTimeout(() => setError(null), 4000);
25
+ return () => clearTimeout(t);
26
+ }
27
+ }, [error]);
28
+
29
+ async function onSubmit(e: FormEvent) {
30
+ e.preventDefault();
31
+ setBusy(true);
32
+ setError(null);
33
+ try {
34
+ const base = (
35
+ apiUrl ??
36
+ process.env.NEXT_PUBLIC_SLYXUP_API_URL ??
37
+ 'https://auth.slyxup.online'
38
+ ).replace(/\/$/, '');
39
+ const res = await fetch(`${base}/v1/verification/password/reset`, {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ token, password }),
43
+ });
44
+ if (!res.ok) {
45
+ const data = await res
46
+ .json()
47
+ .catch(() => ({ error: 'Invalid or expired link' }));
48
+ throw new Error(data.error ?? 'Invalid or expired link');
49
+ }
50
+ setDone(true);
51
+ onSuccess?.();
52
+ } catch (err) {
53
+ setError(err instanceof Error ? err.message : 'Something went wrong.');
54
+ } finally {
55
+ setBusy(false);
56
+ }
57
+ }
58
+
59
+ if (done) {
60
+ return (
61
+ <div className="slx-card">
62
+ <div className="slx-success-icon">
63
+ <CheckIcon />
64
+ </div>
65
+ <h1 className="slx-title" style={{ textAlign: 'center' }}>
66
+ Password updated
67
+ </h1>
68
+ <p className="slx-subtitle" style={{ textAlign: 'center' }}>
69
+ Your password has been changed. Use it to sign in.
70
+ </p>
71
+ {onSuccess && (
72
+ <button type="button" className="slx-btn" onClick={onSuccess}>
73
+ Continue to sign in
74
+ </button>
75
+ )}
76
+ </div>
77
+ );
78
+ }
79
+
80
+ return (
81
+ <div className={`slx-card${error ? ' slx-card-error' : ''}`}>
82
+ <div className="slx-mark">
83
+ <KeyholeMark />
84
+ </div>
85
+ <h1 className="slx-title">Choose a new password</h1>
86
+ <p className="slx-subtitle">
87
+ Pick something strong you haven&apos;t used before.
88
+ </p>
89
+
90
+ {error && (
91
+ <p className="slx-error-text" role="alert">
92
+ {error}
93
+ </p>
94
+ )}
95
+
96
+ <form onSubmit={onSubmit}>
97
+ <div className="slx-field">
98
+ <label className="slx-label" htmlFor="slx-reset-password">
99
+ New password
100
+ </label>
101
+ <input
102
+ id="slx-reset-password"
103
+ className="slx-input"
104
+ type="password"
105
+ autoComplete="new-password"
106
+ placeholder="At least 8 characters"
107
+ value={password}
108
+ onChange={(e) => setPassword(e.target.value)}
109
+ required
110
+ minLength={8}
111
+ />
112
+ </div>
113
+ <button className="slx-btn" type="submit" disabled={busy}>
114
+ {busy && <span className="slx-spinner" aria-hidden="true" />}
115
+ {busy ? 'Updating…' : 'Update password'}
116
+ </button>
117
+ </form>
118
+ </div>
119
+ );
120
+ }
@@ -0,0 +1,143 @@
1
+ import { SlyxupError } from '@slyxup/core';
2
+ import { useAuth } from '@slyxup/react';
3
+ import { type FormEvent, useEffect, useRef, useState } from 'react';
4
+ import { GitHubIcon, GoogleIcon, KeyholeMark } from '../../icons';
5
+
6
+ export interface SignInProps {
7
+ /** Show social buttons (default true) */
8
+ social?: boolean;
9
+ /** Called after successful sign in */
10
+ onSuccess?: () => void;
11
+ /** Switch to sign up */
12
+ onSignUpClick?: () => void;
13
+ }
14
+
15
+ /** Email/password + OAuth sign-in card. */
16
+ export function SignIn({
17
+ social = true,
18
+ onSuccess,
19
+ onSignUpClick,
20
+ }: SignInProps) {
21
+ const { signIn } = useAuth();
22
+ const [email, setEmail] = useState('');
23
+ const [password, setPassword] = useState('');
24
+ const [busy, setBusy] = useState(false);
25
+ const [error, setError] = useState<string | null>(null);
26
+ const cardRef = useRef<HTMLDivElement>(null);
27
+
28
+ useEffect(() => {
29
+ if (error) {
30
+ const t = setTimeout(() => setError(null), 4000);
31
+ return () => clearTimeout(t);
32
+ }
33
+ }, [error]);
34
+
35
+ async function onSubmit(e: FormEvent) {
36
+ e.preventDefault();
37
+ setBusy(true);
38
+ setError(null);
39
+ try {
40
+ await signIn({ email, password });
41
+ onSuccess?.();
42
+ } catch (err) {
43
+ setError(
44
+ err instanceof SlyxupError
45
+ ? err.message
46
+ : 'Something went wrong. Try again.'
47
+ );
48
+ } finally {
49
+ setBusy(false);
50
+ }
51
+ }
52
+
53
+ function oauth(provider: 'google' | 'github') {
54
+ window.location.href = `/v1/oauth/${provider}`;
55
+ }
56
+
57
+ return (
58
+ <div ref={cardRef} className={`slx-card${error ? ' slx-card-error' : ''}`}>
59
+ <div className="slx-mark">
60
+ <KeyholeMark />
61
+ </div>
62
+ <h1 className="slx-title">Sign in</h1>
63
+ <p className="slx-subtitle">
64
+ Welcome back. Enter your details to continue.
65
+ </p>
66
+
67
+ {social && (
68
+ <>
69
+ <div className="slx-social">
70
+ <button
71
+ type="button"
72
+ className="slx-social-btn"
73
+ onClick={() => oauth('google')}
74
+ >
75
+ <GoogleIcon /> Continue with Google
76
+ </button>
77
+ <button
78
+ type="button"
79
+ className="slx-social-btn"
80
+ onClick={() => oauth('github')}
81
+ >
82
+ <GitHubIcon /> Continue with GitHub
83
+ </button>
84
+ </div>
85
+ <div className="slx-divider">or</div>
86
+ </>
87
+ )}
88
+
89
+ {error && (
90
+ <p className="slx-error-text" role="alert">
91
+ {error}
92
+ </p>
93
+ )}
94
+
95
+ <form onSubmit={onSubmit} noValidate={false}>
96
+ <div className="slx-field">
97
+ <label className="slx-label" htmlFor="slx-signin-email">
98
+ Email
99
+ </label>
100
+ <input
101
+ id="slx-signin-email"
102
+ className="slx-input"
103
+ type="email"
104
+ autoComplete="email"
105
+ placeholder="you@example.com"
106
+ value={email}
107
+ onChange={(e) => setEmail(e.target.value)}
108
+ required
109
+ />
110
+ </div>
111
+ <div className="slx-field">
112
+ <label className="slx-label" htmlFor="slx-signin-password">
113
+ Password
114
+ </label>
115
+ <input
116
+ id="slx-signin-password"
117
+ className="slx-input"
118
+ type="password"
119
+ autoComplete="current-password"
120
+ placeholder="••••••••"
121
+ value={password}
122
+ onChange={(e) => setPassword(e.target.value)}
123
+ required
124
+ minLength={8}
125
+ />
126
+ </div>
127
+ <button className="slx-btn" type="submit" disabled={busy}>
128
+ {busy && <span className="slx-spinner" aria-hidden="true" />}
129
+ {busy ? 'Signing in…' : 'Sign in'}
130
+ </button>
131
+ </form>
132
+
133
+ {onSignUpClick && (
134
+ <p className="slx-footer">
135
+ Don&apos;t have an account?{' '}
136
+ <button type="button" className="slx-link" onClick={onSignUpClick}>
137
+ Sign up
138
+ </button>
139
+ </p>
140
+ )}
141
+ </div>
142
+ );
143
+ }
@@ -0,0 +1,156 @@
1
+ import { SlyxupError } from '@slyxup/core';
2
+ import { useAuth } from '@slyxup/react';
3
+ import { type FormEvent, useEffect, useRef, useState } from 'react';
4
+ import { GitHubIcon, GoogleIcon, KeyholeMark } from '../../icons';
5
+
6
+ export interface SignUpProps {
7
+ social?: boolean;
8
+ onSuccess?: () => void;
9
+ onSignInClick?: () => void;
10
+ }
11
+
12
+ /** Email/password + OAuth sign-up card. */
13
+ export function SignUp({
14
+ social = true,
15
+ onSuccess,
16
+ onSignInClick,
17
+ }: SignUpProps) {
18
+ const { signUp } = useAuth();
19
+ const [firstName, setFirstName] = useState('');
20
+ const [email, setEmail] = useState('');
21
+ const [password, setPassword] = useState('');
22
+ const [busy, setBusy] = useState(false);
23
+ const [error, setError] = useState<string | null>(null);
24
+ const cardRef = useRef<HTMLDivElement>(null);
25
+
26
+ useEffect(() => {
27
+ if (error) {
28
+ const t = setTimeout(() => setError(null), 4000);
29
+ return () => clearTimeout(t);
30
+ }
31
+ }, [error]);
32
+
33
+ async function onSubmit(e: FormEvent) {
34
+ e.preventDefault();
35
+ setBusy(true);
36
+ setError(null);
37
+ try {
38
+ await signUp({ email, password, firstName: firstName || undefined });
39
+ onSuccess?.();
40
+ } catch (err) {
41
+ setError(
42
+ err instanceof SlyxupError
43
+ ? err.message
44
+ : 'Something went wrong. Try again.'
45
+ );
46
+ } finally {
47
+ setBusy(false);
48
+ }
49
+ }
50
+
51
+ function oauth(provider: 'google' | 'github') {
52
+ window.location.href = `/v1/oauth/${provider}`;
53
+ }
54
+
55
+ return (
56
+ <div ref={cardRef} className={`slx-card${error ? ' slx-card-error' : ''}`}>
57
+ <div className="slx-mark">
58
+ <KeyholeMark />
59
+ </div>
60
+ <h1 className="slx-title">Create your account</h1>
61
+ <p className="slx-subtitle">A minute to set up. Sign in forever after.</p>
62
+
63
+ {social && (
64
+ <>
65
+ <div className="slx-social">
66
+ <button
67
+ type="button"
68
+ className="slx-social-btn"
69
+ onClick={() => oauth('google')}
70
+ >
71
+ <GoogleIcon /> Continue with Google
72
+ </button>
73
+ <button
74
+ type="button"
75
+ className="slx-social-btn"
76
+ onClick={() => oauth('github')}
77
+ >
78
+ <GitHubIcon /> Continue with GitHub
79
+ </button>
80
+ </div>
81
+ <div className="slx-divider">or</div>
82
+ </>
83
+ )}
84
+
85
+ {error && (
86
+ <p className="slx-error-text" role="alert">
87
+ {error}
88
+ </p>
89
+ )}
90
+
91
+ <form onSubmit={onSubmit}>
92
+ <div className="slx-field">
93
+ <label className="slx-label" htmlFor="slx-signup-name">
94
+ First name
95
+ </label>
96
+ <input
97
+ id="slx-signup-name"
98
+ className="slx-input"
99
+ type="text"
100
+ autoComplete="given-name"
101
+ placeholder="Ada"
102
+ value={firstName}
103
+ onChange={(e) => setFirstName(e.target.value)}
104
+ />
105
+ </div>
106
+ <div className="slx-field">
107
+ <label className="slx-label" htmlFor="slx-signup-email">
108
+ Email
109
+ </label>
110
+ <input
111
+ id="slx-signup-email"
112
+ className="slx-input"
113
+ type="email"
114
+ autoComplete="email"
115
+ placeholder="you@example.com"
116
+ value={email}
117
+ onChange={(e) => setEmail(e.target.value)}
118
+ required
119
+ />
120
+ </div>
121
+ <div className="slx-field">
122
+ <label className="slx-label" htmlFor="slx-signup-password">
123
+ Password
124
+ </label>
125
+ <input
126
+ id="slx-signup-password"
127
+ className="slx-input"
128
+ type="password"
129
+ autoComplete="new-password"
130
+ placeholder="At least 8 characters"
131
+ value={password}
132
+ onChange={(e) => setPassword(e.target.value)}
133
+ required
134
+ minLength={8}
135
+ />
136
+ <p className="slx-hint">
137
+ Use 8+ characters with a mix of letters and numbers.
138
+ </p>
139
+ </div>
140
+ <button className="slx-btn" type="submit" disabled={busy}>
141
+ {busy && <span className="slx-spinner" aria-hidden="true" />}
142
+ {busy ? 'Creating account…' : 'Create account'}
143
+ </button>
144
+ </form>
145
+
146
+ {onSignInClick && (
147
+ <p className="slx-footer">
148
+ Already have an account?{' '}
149
+ <button type="button" className="slx-link" onClick={onSignInClick}>
150
+ Sign in
151
+ </button>
152
+ </p>
153
+ )}
154
+ </div>
155
+ );
156
+ }
@@ -0,0 +1,39 @@
1
+ import { GitHubIcon, GoogleIcon } from '../../icons';
2
+
3
+ export interface SocialButtonsProps {
4
+ /** Show only these providers. Default: both */
5
+ providers?: Array<'google' | 'github'>;
6
+ /** OAuth start path base (default /v1/oauth) */
7
+ basePath?: string;
8
+ }
9
+
10
+ const META = {
11
+ google: { label: 'Continue with Google', Icon: GoogleIcon },
12
+ github: { label: 'Continue with GitHub', Icon: GitHubIcon },
13
+ } as const;
14
+
15
+ /** Provider buttons that redirect to hosted OAuth start. */
16
+ export function SocialButtons({
17
+ providers = ['google', 'github'],
18
+ basePath = '/v1/oauth',
19
+ }: SocialButtonsProps) {
20
+ return (
21
+ <div className="slx-social">
22
+ {providers.map((p) => {
23
+ const { label, Icon } = META[p];
24
+ return (
25
+ <button
26
+ key={p}
27
+ type="button"
28
+ className="slx-social-btn"
29
+ onClick={() => {
30
+ window.location.href = `${basePath}/${p}`;
31
+ }}
32
+ >
33
+ <Icon /> {label}
34
+ </button>
35
+ );
36
+ })}
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,89 @@
1
+ import { useAuth, useUser } from '@slyxup/react';
2
+ import { useEffect, useRef, useState } from 'react';
3
+
4
+ function initials(name: string | null | undefined, email: string): string {
5
+ if (name?.trim()) return name.trim().slice(0, 1).toUpperCase();
6
+ return email.slice(0, 1).toUpperCase();
7
+ }
8
+
9
+ /** Avatar + dropdown with profile actions and sign out. */
10
+ export function UserButton() {
11
+ const { isLoaded, user } = useUser();
12
+ const { signOut } = useAuth();
13
+ const [open, setOpen] = useState(false);
14
+ const wrapRef = useRef<HTMLDivElement>(null);
15
+
16
+ useEffect(() => {
17
+ function onDocClick(e: MouseEvent) {
18
+ if (wrapRef.current && !wrapRef.current.contains(e.target as Node))
19
+ setOpen(false);
20
+ }
21
+ function onKey(e: KeyboardEvent) {
22
+ if (e.key === 'Escape') setOpen(false);
23
+ }
24
+ document.addEventListener('mousedown', onDocClick);
25
+ document.addEventListener('keydown', onKey);
26
+ return () => {
27
+ document.removeEventListener('mousedown', onDocClick);
28
+ document.removeEventListener('keydown', onKey);
29
+ };
30
+ }, []);
31
+
32
+ if (!isLoaded)
33
+ return <div className="slx-userbtn-avatar" aria-hidden="true" />;
34
+ const email = user?.email ?? '';
35
+ const name = user?.firstName;
36
+
37
+ async function onSignOut() {
38
+ await signOut();
39
+ setOpen(false);
40
+ }
41
+
42
+ return (
43
+ <div className="slx-userbtn-wrap" ref={wrapRef}>
44
+ <button
45
+ type="button"
46
+ className="slx-userbtn-avatar"
47
+ onClick={() => setOpen((o) => !o)}
48
+ aria-haspopup="menu"
49
+ aria-expanded={open}
50
+ aria-label="Account menu"
51
+ >
52
+ {user?.avatarUrl ? (
53
+ <img src={user.avatarUrl} alt="" />
54
+ ) : (
55
+ initials(name, email || '?')
56
+ )}
57
+ </button>
58
+
59
+ {open && (
60
+ <div className="slx-menu" role="menu">
61
+ <div className="slx-menu-header">
62
+ <p className="slx-menu-name">
63
+ {name
64
+ ? `${name}${user?.lastName ? ` ${user.lastName}` : ''}`
65
+ : email.split('@')[0]}
66
+ </p>
67
+ <p className="slx-menu-email">{email}</p>
68
+ </div>
69
+ <button
70
+ type="button"
71
+ className="slx-menu-item"
72
+ role="menuitem"
73
+ onClick={() => setOpen(false)}
74
+ >
75
+ Profile settings
76
+ </button>
77
+ <button
78
+ type="button"
79
+ className="slx-menu-item slx-menu-item-danger"
80
+ role="menuitem"
81
+ onClick={onSignOut}
82
+ >
83
+ Sign out
84
+ </button>
85
+ </div>
86
+ )}
87
+ </div>
88
+ );
89
+ }