@authdog/react-elements 0.0.50 → 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.
- package/dist/components/ui/alert.d.mts +12 -0
- package/dist/components/ui/alert.d.ts +12 -0
- package/dist/components/ui/alert.js.map +1 -1
- package/dist/components/ui/alert.mjs.map +1 -1
- package/dist/components/ui/avatar.d.mts +8 -0
- package/dist/components/ui/avatar.d.ts +8 -0
- package/dist/components/ui/avatar.js.map +1 -1
- package/dist/components/ui/avatar.mjs.map +1 -1
- package/dist/components/ui/badge.d.mts +12 -0
- package/dist/components/ui/badge.d.ts +12 -0
- package/dist/components/ui/badge.js.map +1 -1
- package/dist/components/ui/badge.mjs.map +1 -1
- package/dist/components/ui/button.d.mts +14 -0
- package/dist/components/ui/button.d.ts +14 -0
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/button.mjs.map +1 -1
- package/dist/components/ui/card.d.mts +11 -0
- package/dist/components/ui/card.d.ts +11 -0
- package/dist/components/ui/card.js.map +1 -1
- package/dist/components/ui/card.mjs.map +1 -1
- package/dist/components/ui/dropdown-menu.d.mts +27 -0
- package/dist/components/ui/dropdown-menu.d.ts +27 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -1
- package/dist/components/ui/dropdown-menu.mjs.map +1 -1
- package/dist/components/ui/input.d.mts +5 -0
- package/dist/components/ui/input.d.ts +5 -0
- package/dist/components/ui/input.js.map +1 -1
- package/dist/components/ui/input.mjs.map +1 -1
- package/dist/components/ui/label.d.mts +6 -0
- package/dist/components/ui/label.d.ts +6 -0
- package/dist/components/ui/label.js.map +1 -1
- package/dist/components/ui/label.mjs.map +1 -1
- package/dist/components/ui/separator.d.mts +6 -0
- package/dist/components/ui/separator.d.ts +6 -0
- package/dist/components/ui/separator.js.map +1 -1
- package/dist/components/ui/separator.mjs.map +1 -1
- package/dist/components/ui/sheet.d.mts +15 -0
- package/dist/components/ui/sheet.d.ts +15 -0
- package/dist/components/ui/sheet.js.map +1 -1
- package/dist/components/ui/sheet.mjs.map +1 -1
- package/dist/components/ui/theme-toggle.d.mts +5 -0
- package/dist/components/ui/theme-toggle.d.ts +5 -0
- package/dist/components/ui/theme-toggle.js.map +1 -1
- package/dist/components/ui/theme-toggle.mjs.map +1 -1
- package/dist/index.d.mts +12 -21
- package/dist/index.d.ts +12 -21
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/lib/utils.d.mts +5 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js.map +1 -1
- package/dist/lib/utils.mjs.map +1 -1
- package/dist/styles.css +1 -4
- package/package.json +40 -25
- package/.eslintrc.js +0 -9
- package/.storybook/main.ts +0 -21
- package/.storybook/preview.ts +0 -17
- package/.storybook/vitest.setup.ts +0 -7
- package/.turbo/turbo-build.log +0 -77
- package/CHANGELOG.md +0 -292
- package/components.json +0 -20
- package/postcss.config.mjs +0 -11
- package/src/components/core/client-only.tsx +0 -15
- package/src/components/core/navbar.tsx +0 -312
- package/src/components/core/placeholder-alert.tsx +0 -23
- package/src/components/core/user-dropdown.tsx +0 -160
- package/src/components/core/user-profile.tsx +0 -521
- package/src/components/flow/login.tsx +0 -167
- package/src/components/flow/totp-validator.tsx +0 -252
- package/src/components/icons.tsx +0 -30
- package/src/components/ui/alert.tsx +0 -66
- package/src/components/ui/avatar.tsx +0 -53
- package/src/components/ui/badge.tsx +0 -46
- package/src/components/ui/button.tsx +0 -56
- package/src/components/ui/card.tsx +0 -92
- package/src/components/ui/dropdown-menu.tsx +0 -265
- package/src/components/ui/input.tsx +0 -21
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/sheet.tsx +0 -142
- package/src/components/ui/theme-toggle.tsx +0 -56
- package/src/global.css +0 -81
- package/src/index.ts +0 -8
- package/src/lib/utils.ts +0 -6
- package/src/stories/core/Navbar.stories.tsx +0 -93
- package/src/stories/core/PlaceholderAlert.stories.tsx +0 -23
- package/src/stories/core/UserDropdown.stories.tsx +0 -56
- package/src/stories/core/UserProfile.stories.tsx +0 -47
- package/src/stories/flow/LoginForm.stories.tsx +0 -20
- package/src/stories/flow/TotpValidator.stories.tsx +0 -23
- package/src/stories/showcase/Landing.stories.tsx +0 -376
- package/src/stories/ui/Button.stories.tsx +0 -45
- package/src/types.ts +0 -0
- package/tailwind.config.ts +0 -82
- package/tsconfig.json +0 -11
- package/tsup.config.ts +0 -31
- package/vitest.config.ts +0 -39
- package/vitest.shims.d.ts +0 -1
- package/wrangler.prod.toml +0 -4
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import type React from "react";
|
|
4
|
-
import { useState } from "react";
|
|
5
|
-
import { User, LogOut, Menu } from "lucide-react";
|
|
6
|
-
|
|
7
|
-
import { cn } from "../../lib/utils";
|
|
8
|
-
import {
|
|
9
|
-
Avatar,
|
|
10
|
-
AvatarFallback,
|
|
11
|
-
AvatarImage,
|
|
12
|
-
} from "../../components/ui/avatar";
|
|
13
|
-
import { Button } from "../../components/ui/button";
|
|
14
|
-
import {
|
|
15
|
-
DropdownMenu,
|
|
16
|
-
DropdownMenuContent,
|
|
17
|
-
DropdownMenuGroup,
|
|
18
|
-
DropdownMenuItem,
|
|
19
|
-
DropdownMenuLabel,
|
|
20
|
-
DropdownMenuSeparator,
|
|
21
|
-
DropdownMenuTrigger,
|
|
22
|
-
} from "../../components/ui/dropdown-menu";
|
|
23
|
-
import { Sheet, SheetContent, SheetTrigger } from "../../components/ui/sheet";
|
|
24
|
-
import { IconWrapper } from "../icons";
|
|
25
|
-
import { ThemeToggle } from "../ui/theme-toggle";
|
|
26
|
-
|
|
27
|
-
export interface NavItem {
|
|
28
|
-
title: string;
|
|
29
|
-
href: string;
|
|
30
|
-
disabled?: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface DropdownMenuItem {
|
|
34
|
-
name: string;
|
|
35
|
-
uri: string;
|
|
36
|
-
icon?: React.ComponentType<{ className?: string }>;
|
|
37
|
-
disabled?: boolean;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface NavbarProps {
|
|
41
|
-
items?: NavItem[] | undefined;
|
|
42
|
-
children?: React.ReactNode;
|
|
43
|
-
className?: string;
|
|
44
|
-
logoText?: string;
|
|
45
|
-
logoSrc?: string;
|
|
46
|
-
/**
|
|
47
|
-
* Optional custom alt text element rendered next to the logo.
|
|
48
|
-
* When not provided, the plain `logoText` string is rendered.
|
|
49
|
-
* Use this to pass in custom typography / font classes.
|
|
50
|
-
*/
|
|
51
|
-
altText?: React.ReactNode;
|
|
52
|
-
isLoading?: boolean;
|
|
53
|
-
user?: any;
|
|
54
|
-
onNavigateHome?: () => void;
|
|
55
|
-
onNavItemClick?: (href: string) => void;
|
|
56
|
-
/**
|
|
57
|
-
* Custom dropdown menu items that appear in the user avatar dropdown.
|
|
58
|
-
* If not provided, defaults to a "Profile" link.
|
|
59
|
-
*/
|
|
60
|
-
dropdownMenuItems?: DropdownMenuItem[];
|
|
61
|
-
/**
|
|
62
|
-
* Callback when a dropdown menu item is clicked.
|
|
63
|
-
* Receives the URI of the clicked item.
|
|
64
|
-
*/
|
|
65
|
-
onDropdownMenuItemClick?: (uri: string) => void;
|
|
66
|
-
/**
|
|
67
|
-
* @deprecated Use dropdownMenuItems with a custom item instead
|
|
68
|
-
*/
|
|
69
|
-
onProfileSelected?: () => void;
|
|
70
|
-
onLogout?: () => void;
|
|
71
|
-
// signinUrl?: string;
|
|
72
|
-
identityHost?: string;
|
|
73
|
-
environmentId?: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function Navbar({
|
|
77
|
-
items = [
|
|
78
|
-
// { title: "Dashboard", href: "/dashboard" },
|
|
79
|
-
],
|
|
80
|
-
children,
|
|
81
|
-
className,
|
|
82
|
-
logoText = "ACME Corp",
|
|
83
|
-
logoSrc,
|
|
84
|
-
altText,
|
|
85
|
-
user = {
|
|
86
|
-
name: "John Doe",
|
|
87
|
-
email: "john@example.com",
|
|
88
|
-
image: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
|
|
89
|
-
},
|
|
90
|
-
onNavigateHome = () => console.log("Navigating to home"),
|
|
91
|
-
onNavItemClick = (href: string) => console.log(`Navigating to ${href}`),
|
|
92
|
-
dropdownMenuItems = [
|
|
93
|
-
{ name: "Profile", uri: "/profile", icon: User },
|
|
94
|
-
],
|
|
95
|
-
onDropdownMenuItemClick = (uri: string) => console.log(`Navigating to ${uri}`),
|
|
96
|
-
onProfileSelected,
|
|
97
|
-
onLogout = () => console.log("Logout clicked"),
|
|
98
|
-
isLoading = false,
|
|
99
|
-
identityHost = "https://stg-id.authdog.xyz",
|
|
100
|
-
environmentId = "58be35b0-708f-49f6-84f0-6695d307d997",
|
|
101
|
-
}: NavbarProps) {
|
|
102
|
-
const [open, setOpen] = useState(false);
|
|
103
|
-
const [logoFailed, setLogoFailed] = useState(false);
|
|
104
|
-
const isAuthenticated =
|
|
105
|
-
user !== null &&
|
|
106
|
-
user !== undefined &&
|
|
107
|
-
user.id !== null &&
|
|
108
|
-
user.id !== undefined;
|
|
109
|
-
|
|
110
|
-
return (
|
|
111
|
-
<header
|
|
112
|
-
className={cn(
|
|
113
|
-
// Keep the navbar simple and predictable so host apps can style around it.
|
|
114
|
-
// We avoid sticky positioning, backdrop filters, and custom width constraints.
|
|
115
|
-
"w-full border-b bg-background",
|
|
116
|
-
className,
|
|
117
|
-
)}
|
|
118
|
-
>
|
|
119
|
-
<div
|
|
120
|
-
className={cn(
|
|
121
|
-
// Basic centered container with horizontal padding.
|
|
122
|
-
"mx-auto flex h-16 w-full max-w-6xl items-center justify-between px-4 md:px-6",
|
|
123
|
-
)}
|
|
124
|
-
>
|
|
125
|
-
<div className="flex items-center gap-3 md:gap-4">
|
|
126
|
-
<button
|
|
127
|
-
type="button"
|
|
128
|
-
onClick={onNavigateHome}
|
|
129
|
-
className={cn(
|
|
130
|
-
"group inline-flex items-center gap-2 md:gap-3 rounded-md px-1 py-1 text-left cursor-pointer",
|
|
131
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
132
|
-
)}
|
|
133
|
-
aria-label="Go to homepage"
|
|
134
|
-
>
|
|
135
|
-
{logoSrc && !logoFailed && (
|
|
136
|
-
<span className="inline-flex h-8 w-8 items-center justify-center overflow-hidden rounded-md bg-muted/80 ring-1 ring-border">
|
|
137
|
-
<img
|
|
138
|
-
src={logoSrc}
|
|
139
|
-
alt={logoText}
|
|
140
|
-
className="h-7 w-7 object-contain"
|
|
141
|
-
onError={() => setLogoFailed(true)}
|
|
142
|
-
/>
|
|
143
|
-
</span>
|
|
144
|
-
)}
|
|
145
|
-
{altText ?? (
|
|
146
|
-
<span className="text-base font-semibold tracking-tight md:text-lg group-hover:text-primary">
|
|
147
|
-
{logoText}
|
|
148
|
-
</span>
|
|
149
|
-
)}
|
|
150
|
-
</button>
|
|
151
|
-
{children}
|
|
152
|
-
</div>
|
|
153
|
-
<div className="flex flex-1 items-center justify-end gap-6">
|
|
154
|
-
<nav className="hidden md:flex items-center gap-6">
|
|
155
|
-
{items?.map((item, index) => (
|
|
156
|
-
<span
|
|
157
|
-
key={index}
|
|
158
|
-
onClick={() => {
|
|
159
|
-
if (!item.disabled) {
|
|
160
|
-
onNavItemClick(item.href);
|
|
161
|
-
}
|
|
162
|
-
}}
|
|
163
|
-
className={cn(
|
|
164
|
-
"text-sm font-medium transition-colors hover:text-primary cursor-pointer",
|
|
165
|
-
item.disabled && "cursor-not-allowed opacity-80",
|
|
166
|
-
)}
|
|
167
|
-
>
|
|
168
|
-
{item.title}
|
|
169
|
-
</span>
|
|
170
|
-
))}
|
|
171
|
-
</nav>
|
|
172
|
-
<div className="flex items-center gap-3">
|
|
173
|
-
<Sheet open={open} onOpenChange={setOpen}>
|
|
174
|
-
<SheetTrigger asChild>
|
|
175
|
-
<Button
|
|
176
|
-
variant="ghost"
|
|
177
|
-
size="icon"
|
|
178
|
-
className="md:hidden"
|
|
179
|
-
aria-label="Open Menu"
|
|
180
|
-
>
|
|
181
|
-
<IconWrapper Icon={Menu} withMargin={false} />
|
|
182
|
-
</Button>
|
|
183
|
-
</SheetTrigger>
|
|
184
|
-
<SheetContent side="left" className="pr-0">
|
|
185
|
-
<nav className="grid gap-2 py-6">
|
|
186
|
-
{items?.map((item, index) => (
|
|
187
|
-
<a
|
|
188
|
-
key={index}
|
|
189
|
-
href={item.href}
|
|
190
|
-
className={cn(
|
|
191
|
-
"flex w-full items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-accent cursor-pointer",
|
|
192
|
-
item.disabled && "cursor-not-allowed opacity-80",
|
|
193
|
-
)}
|
|
194
|
-
onClick={() => setOpen(false)}
|
|
195
|
-
>
|
|
196
|
-
{item.title}
|
|
197
|
-
</a>
|
|
198
|
-
))}
|
|
199
|
-
</nav>
|
|
200
|
-
</SheetContent>
|
|
201
|
-
</Sheet>
|
|
202
|
-
<ThemeToggle />
|
|
203
|
-
{isLoading ? (
|
|
204
|
-
<div
|
|
205
|
-
className="h-8 w-8 animate-pulse rounded-full bg-muted"
|
|
206
|
-
aria-label="Loading user"
|
|
207
|
-
/>
|
|
208
|
-
) : isAuthenticated ? (
|
|
209
|
-
<DropdownMenu>
|
|
210
|
-
<DropdownMenuTrigger asChild>
|
|
211
|
-
<Button
|
|
212
|
-
variant="ghost"
|
|
213
|
-
className="relative h-8 w-8 rounded-full cursor-pointer"
|
|
214
|
-
disabled={isLoading}
|
|
215
|
-
>
|
|
216
|
-
<Avatar className="h-8 w-8">
|
|
217
|
-
{isLoading ? (
|
|
218
|
-
<div className="h-8 w-8 animate-pulse bg-muted rounded-full" />
|
|
219
|
-
) : (
|
|
220
|
-
<>
|
|
221
|
-
<AvatarImage
|
|
222
|
-
src={user.photos?.[0]?.value || "/placeholder.svg"}
|
|
223
|
-
alt={user.displayName}
|
|
224
|
-
/>
|
|
225
|
-
<AvatarFallback>
|
|
226
|
-
{user.displayName?.charAt(0)}
|
|
227
|
-
</AvatarFallback>
|
|
228
|
-
</>
|
|
229
|
-
)}
|
|
230
|
-
</Avatar>
|
|
231
|
-
</Button>
|
|
232
|
-
</DropdownMenuTrigger>
|
|
233
|
-
<DropdownMenuContent className="w-56" align="end" forceMount>
|
|
234
|
-
{isLoading ? (
|
|
235
|
-
<div className="p-4">
|
|
236
|
-
<div className="h-4 w-3/4 animate-pulse bg-muted rounded mb-2" />
|
|
237
|
-
<div className="h-3 w-1/2 animate-pulse bg-muted rounded" />
|
|
238
|
-
</div>
|
|
239
|
-
) : (
|
|
240
|
-
<>
|
|
241
|
-
<DropdownMenuLabel className="font-normal">
|
|
242
|
-
<div className="flex flex-col space-y-1">
|
|
243
|
-
<p className="text-sm font-medium leading-none">
|
|
244
|
-
{user.displayName}
|
|
245
|
-
</p>
|
|
246
|
-
<p className="text-xs leading-none text-muted-foreground">
|
|
247
|
-
{user.emails?.[0]?.value}
|
|
248
|
-
</p>
|
|
249
|
-
</div>
|
|
250
|
-
</DropdownMenuLabel>
|
|
251
|
-
{dropdownMenuItems.length > 0 && (
|
|
252
|
-
<>
|
|
253
|
-
<DropdownMenuSeparator />
|
|
254
|
-
<DropdownMenuGroup>
|
|
255
|
-
{dropdownMenuItems.map((item, index) => (
|
|
256
|
-
<DropdownMenuItem
|
|
257
|
-
key={index}
|
|
258
|
-
onClick={() => {
|
|
259
|
-
if (!item.disabled) {
|
|
260
|
-
// Backward compatibility: if it's the profile item and onProfileSelected exists
|
|
261
|
-
if (item.uri === "/profile" && onProfileSelected) {
|
|
262
|
-
onProfileSelected();
|
|
263
|
-
}
|
|
264
|
-
onDropdownMenuItemClick(item.uri);
|
|
265
|
-
}
|
|
266
|
-
}}
|
|
267
|
-
disabled={item.disabled}
|
|
268
|
-
className="cursor-pointer"
|
|
269
|
-
>
|
|
270
|
-
{item.icon && <IconWrapper Icon={item.icon} />}
|
|
271
|
-
<span>{item.name}</span>
|
|
272
|
-
</DropdownMenuItem>
|
|
273
|
-
))}
|
|
274
|
-
</DropdownMenuGroup>
|
|
275
|
-
<DropdownMenuSeparator />
|
|
276
|
-
</>
|
|
277
|
-
)}
|
|
278
|
-
<DropdownMenuItem onClick={onLogout} className="cursor-pointer">
|
|
279
|
-
<IconWrapper Icon={LogOut} />
|
|
280
|
-
<span>Log out</span>
|
|
281
|
-
</DropdownMenuItem>
|
|
282
|
-
</>
|
|
283
|
-
)}
|
|
284
|
-
</DropdownMenuContent>
|
|
285
|
-
</DropdownMenu>
|
|
286
|
-
) : (
|
|
287
|
-
<Button
|
|
288
|
-
variant="default"
|
|
289
|
-
className="cursor-pointer"
|
|
290
|
-
aria-label="Sign in"
|
|
291
|
-
onClick={() => {
|
|
292
|
-
if (!environmentId) {
|
|
293
|
-
throw new Error("Environment ID is required");
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (!identityHost) {
|
|
297
|
-
throw new Error("Identity Host is required");
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const signinUrl = `${identityHost}/signin/${environmentId}`;
|
|
301
|
-
window.open(signinUrl, "_blank");
|
|
302
|
-
}}
|
|
303
|
-
>
|
|
304
|
-
Sign in
|
|
305
|
-
</Button>
|
|
306
|
-
)}
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
</div>
|
|
310
|
-
</header>
|
|
311
|
-
);
|
|
312
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { CheckCircle2Icon } from "lucide-react";
|
|
2
|
-
import { Alert, AlertDescription, AlertTitle } from "../../components/ui/alert";
|
|
3
|
-
import { IconWrapper } from "../icons";
|
|
4
|
-
|
|
5
|
-
interface PlaceholderAlertProps {
|
|
6
|
-
title?: string;
|
|
7
|
-
description?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const PlaceholderAlert = (props: PlaceholderAlertProps) => {
|
|
11
|
-
const title = props.title ?? "Placeholder Alert";
|
|
12
|
-
const description = props.description ?? "This is a placeholder alert.";
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<div className="grid w-full max-w-xl items-start gap-4">
|
|
16
|
-
<Alert>
|
|
17
|
-
<IconWrapper Icon={CheckCircle2Icon} />
|
|
18
|
-
<AlertTitle>{title}</AlertTitle>
|
|
19
|
-
<AlertDescription>{description}</AlertDescription>
|
|
20
|
-
</Alert>
|
|
21
|
-
</div>
|
|
22
|
-
);
|
|
23
|
-
};
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
DropdownMenu,
|
|
5
|
-
DropdownMenuContent,
|
|
6
|
-
DropdownMenuItem,
|
|
7
|
-
DropdownMenuSeparator,
|
|
8
|
-
DropdownMenuTrigger,
|
|
9
|
-
} from "../../components/ui/dropdown-menu";
|
|
10
|
-
import {
|
|
11
|
-
Avatar,
|
|
12
|
-
AvatarFallback,
|
|
13
|
-
AvatarImage,
|
|
14
|
-
} from "../../components/ui/avatar";
|
|
15
|
-
import { cn } from "../../lib/utils";
|
|
16
|
-
import { LogOut, Settings, ExternalLink } from "lucide-react";
|
|
17
|
-
import type { ComponentType } from "react";
|
|
18
|
-
|
|
19
|
-
export type UserDropdownLink = {
|
|
20
|
-
label: string;
|
|
21
|
-
href?: string;
|
|
22
|
-
onClick?: () => void;
|
|
23
|
-
icon?: React.ComponentType<any>;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export interface UserDropdownProps {
|
|
27
|
-
trigger: React.ReactElement;
|
|
28
|
-
user: {
|
|
29
|
-
displayName?: string;
|
|
30
|
-
name?: string;
|
|
31
|
-
email?: string;
|
|
32
|
-
emails?: { value: string }[];
|
|
33
|
-
photos?: { value: string }[];
|
|
34
|
-
avatar?: string;
|
|
35
|
-
};
|
|
36
|
-
className?: string;
|
|
37
|
-
onManageAccount?: () => void;
|
|
38
|
-
onSignout?: () => void;
|
|
39
|
-
links?: UserDropdownLink[];
|
|
40
|
-
side?: "top" | "right" | "bottom" | "left";
|
|
41
|
-
align?: "start" | "center" | "end";
|
|
42
|
-
sideOffset?: number;
|
|
43
|
-
modal?: boolean;
|
|
44
|
-
triggerAsChild?: boolean;
|
|
45
|
-
triggerWrapperClassName?: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const getInitials = (name?: string) => {
|
|
49
|
-
if (!name) return "?";
|
|
50
|
-
const parts = String(name).trim().split(/\s+/);
|
|
51
|
-
const initials = parts
|
|
52
|
-
.slice(0, 2)
|
|
53
|
-
.map((p) => p[0]?.toUpperCase())
|
|
54
|
-
.join("");
|
|
55
|
-
return initials || "?";
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export const UserDropdown = ({
|
|
59
|
-
trigger,
|
|
60
|
-
user,
|
|
61
|
-
className,
|
|
62
|
-
onManageAccount,
|
|
63
|
-
onSignout,
|
|
64
|
-
links = [],
|
|
65
|
-
side = "bottom",
|
|
66
|
-
align = "end",
|
|
67
|
-
sideOffset = 8,
|
|
68
|
-
modal = false,
|
|
69
|
-
triggerAsChild = false,
|
|
70
|
-
triggerWrapperClassName,
|
|
71
|
-
}: UserDropdownProps) => {
|
|
72
|
-
const primaryEmail = user?.emails?.[0]?.value || user?.email || "";
|
|
73
|
-
const displayName = user?.displayName || user?.name || "";
|
|
74
|
-
const avatar = user?.photos?.[0]?.value || user?.avatar || "";
|
|
75
|
-
|
|
76
|
-
const handleLink = (item: UserDropdownLink) => {
|
|
77
|
-
if (item.onClick) return item.onClick();
|
|
78
|
-
if (item.href) {
|
|
79
|
-
if (item.href.startsWith("http")) {
|
|
80
|
-
window.open(item.href, "_blank");
|
|
81
|
-
} else {
|
|
82
|
-
window.location.assign(item.href);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const IconExternal = ExternalLink as any;
|
|
88
|
-
const SettingsIcon = Settings as ComponentType<any>;
|
|
89
|
-
const LogOutIcon = LogOut as ComponentType<any>;
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<DropdownMenu modal={modal}>
|
|
93
|
-
{triggerAsChild ? (
|
|
94
|
-
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
|
95
|
-
) : (
|
|
96
|
-
<DropdownMenuTrigger
|
|
97
|
-
className={cn(
|
|
98
|
-
"inline-flex items-center justify-center bg-transparent p-0 border-0 outline-none focus-visible:outline-none",
|
|
99
|
-
triggerWrapperClassName,
|
|
100
|
-
)}
|
|
101
|
-
>
|
|
102
|
-
{trigger}
|
|
103
|
-
</DropdownMenuTrigger>
|
|
104
|
-
)}
|
|
105
|
-
<DropdownMenuContent
|
|
106
|
-
align={align}
|
|
107
|
-
side={side}
|
|
108
|
-
sideOffset={sideOffset}
|
|
109
|
-
className={cn(
|
|
110
|
-
"w-72 p-2 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md",
|
|
111
|
-
className,
|
|
112
|
-
)}
|
|
113
|
-
>
|
|
114
|
-
<div className="flex items-center gap-3 px-4 pt-4 pb-3">
|
|
115
|
-
<Avatar className="h-9 w-9 rounded-full">
|
|
116
|
-
<AvatarImage src={avatar} alt={displayName} />
|
|
117
|
-
<AvatarFallback className="rounded-full">
|
|
118
|
-
{getInitials(displayName)}
|
|
119
|
-
</AvatarFallback>
|
|
120
|
-
</Avatar>
|
|
121
|
-
<div className="min-w-0">
|
|
122
|
-
<div className="text-sm font-semibold truncate">{displayName}</div>
|
|
123
|
-
<div className="text-xs text-muted-foreground truncate">
|
|
124
|
-
{primaryEmail}
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
</div>
|
|
128
|
-
<DropdownMenuSeparator />
|
|
129
|
-
<DropdownMenuItem
|
|
130
|
-
className="cursor-pointer py-2"
|
|
131
|
-
onClick={() => onManageAccount?.()}
|
|
132
|
-
>
|
|
133
|
-
<SettingsIcon className="mr-2 h-4 w-4" />
|
|
134
|
-
<span>Manage account</span>
|
|
135
|
-
</DropdownMenuItem>
|
|
136
|
-
{links.map((item, idx) => {
|
|
137
|
-
const Icon = (item.icon ?? IconExternal) as any;
|
|
138
|
-
return (
|
|
139
|
-
<DropdownMenuItem
|
|
140
|
-
key={`${item.label}-${idx}`}
|
|
141
|
-
className="cursor-pointer py-2"
|
|
142
|
-
onClick={() => handleLink(item)}
|
|
143
|
-
>
|
|
144
|
-
<Icon className="mr-2 h-4 w-4" />
|
|
145
|
-
<span>{item.label}</span>
|
|
146
|
-
</DropdownMenuItem>
|
|
147
|
-
);
|
|
148
|
-
})}
|
|
149
|
-
<DropdownMenuSeparator />
|
|
150
|
-
<DropdownMenuItem
|
|
151
|
-
className="cursor-pointer py-2 rounded-md font-semibold text-red-600 dark:text-red-300 hover:bg-red-50 hover:text-red-700 focus:bg-red-50 focus:text-red-700 dark:hover:bg-red-500/20 dark:focus:bg-red-500/25 dark:hover:text-red-100 dark:focus:text-red-100 border border-transparent dark:border-red-500/30 ring-0 focus-visible:ring-2 focus-visible:ring-red-400/40 dark:focus-visible:ring-red-400/40"
|
|
152
|
-
onClick={() => onSignout?.()}
|
|
153
|
-
>
|
|
154
|
-
<LogOutIcon className="mr-2 h-4 w-4 text-red-600 dark:text-red-300" />
|
|
155
|
-
<span>Sign out</span>
|
|
156
|
-
</DropdownMenuItem>
|
|
157
|
-
</DropdownMenuContent>
|
|
158
|
-
</DropdownMenu>
|
|
159
|
-
);
|
|
160
|
-
};
|