Skip to content

Commit deb37a3

Browse files
committed
account dropdown menu
1 parent 7ce1072 commit deb37a3

File tree

9 files changed

+672
-23
lines changed

9 files changed

+672
-23
lines changed

auth.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import GitHub from 'next-auth/providers/github';
21
import type { NextAuthConfig } from 'next-auth';
2+
import GitHub from 'next-auth/providers/github';
33

44
// https://authjs.dev/guides/edge-compatibility#middleware
55
export default {

auth.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { MongoDBAdapter } from '@auth/mongodb-adapter';
2+
import { ObjectId } from 'mongodb';
23
import NextAuth from 'next-auth';
4+
35
import authConfig from './auth.config';
46
import mongoClientPromise from './lib/mongo-client';
5-
import { ObjectId } from 'mongodb';
67

78
export const { handlers, signIn, signOut, auth } = NextAuth({
89
adapter: MongoDBAdapter(mongoClientPromise),
@@ -26,14 +27,14 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
2627
callbacks: {
2728
async jwt({ token, profile, account }) {
2829
if (profile && account?.provider === 'github') {
29-
token.githubId = profile.node_id;
30+
token.githubLogin = profile.login;
3031
}
3132
return token;
3233
},
3334

3435
async session({ session, token }) {
35-
if (token?.githubId) {
36-
session.user.githubId = token.githubId as string;
36+
if (token?.githubLogin) {
37+
session.user.githubLogin = token.githubLogin as string;
3738
}
3839
return session;
3940
},

components/signin-button/signin-button.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,34 @@
11
'use client';
22
import { Avatar, AvatarFallback, AvatarImage } from '@radix-ui/react-avatar';
33
import { LogIn } from 'lucide-react';
4+
import Link from 'next/link';
45
import { signIn, signOut, useSession } from 'next-auth/react';
56

67
import { getInitials } from '@/utils/get-initials';
78

89
import { Button } from '../ui/button';
10+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../ui/dropdown-menu';
911

1012
export default function SigninButton() {
1113
const { data: session } = useSession();
1214

1315
return (
1416
<div>
1517
{session ? (
16-
<Avatar onClick={() => signOut()} className="cursor-pointer">
17-
<AvatarImage src={session.user.image!} className="rounded-full" width={32} height={32} />
18-
<AvatarFallback>{getInitials(session.user.name!)}</AvatarFallback>
19-
</Avatar>
18+
<DropdownMenu>
19+
<DropdownMenuTrigger asChild>
20+
<Avatar className="cursor-pointer">
21+
<AvatarImage src={session.user.image!} className="rounded-full" width={32} height={32} />
22+
<AvatarFallback>{getInitials(session.user.name!)}</AvatarFallback>
23+
</Avatar>
24+
</DropdownMenuTrigger>
25+
<DropdownMenuContent>
26+
<DropdownMenuItem asChild>
27+
<Link href={`/profile/${session?.user?.githubLogin}`}>Profile</Link>
28+
</DropdownMenuItem>
29+
<DropdownMenuItem onClick={() => signOut()}>Sign Out</DropdownMenuItem>
30+
</DropdownMenuContent>
31+
</DropdownMenu>
2032
) : (
2133
<Button onClick={() => signIn('github')} variant="ghost" size="sm">
2234
<LogIn className="size-4" />

components/ui/dropdown-menu.tsx

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5+
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6+
7+
import { cn } from "@/lib/utils"
8+
9+
function DropdownMenu({
10+
...props
11+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
12+
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
13+
}
14+
15+
function DropdownMenuPortal({
16+
...props
17+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
18+
return (
19+
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
20+
)
21+
}
22+
23+
function DropdownMenuTrigger({
24+
...props
25+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
26+
return (
27+
<DropdownMenuPrimitive.Trigger
28+
data-slot="dropdown-menu-trigger"
29+
{...props}
30+
/>
31+
)
32+
}
33+
34+
function DropdownMenuContent({
35+
className,
36+
sideOffset = 4,
37+
...props
38+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
39+
return (
40+
<DropdownMenuPrimitive.Portal>
41+
<DropdownMenuPrimitive.Content
42+
data-slot="dropdown-menu-content"
43+
sideOffset={sideOffset}
44+
className={cn(
45+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
46+
className
47+
)}
48+
{...props}
49+
/>
50+
</DropdownMenuPrimitive.Portal>
51+
)
52+
}
53+
54+
function DropdownMenuGroup({
55+
...props
56+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
57+
return (
58+
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
59+
)
60+
}
61+
62+
function DropdownMenuItem({
63+
className,
64+
inset,
65+
variant = "default",
66+
...props
67+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
68+
inset?: boolean
69+
variant?: "default" | "destructive"
70+
}) {
71+
return (
72+
<DropdownMenuPrimitive.Item
73+
data-slot="dropdown-menu-item"
74+
data-inset={inset}
75+
data-variant={variant}
76+
className={cn(
77+
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
78+
className
79+
)}
80+
{...props}
81+
/>
82+
)
83+
}
84+
85+
function DropdownMenuCheckboxItem({
86+
className,
87+
children,
88+
checked,
89+
...props
90+
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
91+
return (
92+
<DropdownMenuPrimitive.CheckboxItem
93+
data-slot="dropdown-menu-checkbox-item"
94+
className={cn(
95+
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
96+
className
97+
)}
98+
checked={checked}
99+
{...props}
100+
>
101+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
102+
<DropdownMenuPrimitive.ItemIndicator>
103+
<CheckIcon className="size-4" />
104+
</DropdownMenuPrimitive.ItemIndicator>
105+
</span>
106+
{children}
107+
</DropdownMenuPrimitive.CheckboxItem>
108+
)
109+
}
110+
111+
function DropdownMenuRadioGroup({
112+
...props
113+
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
114+
return (
115+
<DropdownMenuPrimitive.RadioGroup
116+
data-slot="dropdown-menu-radio-group"
117+
{...props}
118+
/>
119+
)
120+
}
121+
122+
function DropdownMenuRadioItem({
123+
className,
124+
children,
125+
...props
126+
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
127+
return (
128+
<DropdownMenuPrimitive.RadioItem
129+
data-slot="dropdown-menu-radio-item"
130+
className={cn(
131+
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
132+
className
133+
)}
134+
{...props}
135+
>
136+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
137+
<DropdownMenuPrimitive.ItemIndicator>
138+
<CircleIcon className="size-2 fill-current" />
139+
</DropdownMenuPrimitive.ItemIndicator>
140+
</span>
141+
{children}
142+
</DropdownMenuPrimitive.RadioItem>
143+
)
144+
}
145+
146+
function DropdownMenuLabel({
147+
className,
148+
inset,
149+
...props
150+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
151+
inset?: boolean
152+
}) {
153+
return (
154+
<DropdownMenuPrimitive.Label
155+
data-slot="dropdown-menu-label"
156+
data-inset={inset}
157+
className={cn(
158+
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
159+
className
160+
)}
161+
{...props}
162+
/>
163+
)
164+
}
165+
166+
function DropdownMenuSeparator({
167+
className,
168+
...props
169+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
170+
return (
171+
<DropdownMenuPrimitive.Separator
172+
data-slot="dropdown-menu-separator"
173+
className={cn("bg-border -mx-1 my-1 h-px", className)}
174+
{...props}
175+
/>
176+
)
177+
}
178+
179+
function DropdownMenuShortcut({
180+
className,
181+
...props
182+
}: React.ComponentProps<"span">) {
183+
return (
184+
<span
185+
data-slot="dropdown-menu-shortcut"
186+
className={cn(
187+
"text-muted-foreground ml-auto text-xs tracking-widest",
188+
className
189+
)}
190+
{...props}
191+
/>
192+
)
193+
}
194+
195+
function DropdownMenuSub({
196+
...props
197+
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
198+
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
199+
}
200+
201+
function DropdownMenuSubTrigger({
202+
className,
203+
inset,
204+
children,
205+
...props
206+
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
207+
inset?: boolean
208+
}) {
209+
return (
210+
<DropdownMenuPrimitive.SubTrigger
211+
data-slot="dropdown-menu-sub-trigger"
212+
data-inset={inset}
213+
className={cn(
214+
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
215+
className
216+
)}
217+
{...props}
218+
>
219+
{children}
220+
<ChevronRightIcon className="ml-auto size-4" />
221+
</DropdownMenuPrimitive.SubTrigger>
222+
)
223+
}
224+
225+
function DropdownMenuSubContent({
226+
className,
227+
...props
228+
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
229+
return (
230+
<DropdownMenuPrimitive.SubContent
231+
data-slot="dropdown-menu-sub-content"
232+
className={cn(
233+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
234+
className
235+
)}
236+
{...props}
237+
/>
238+
)
239+
}
240+
241+
export {
242+
DropdownMenu,
243+
DropdownMenuPortal,
244+
DropdownMenuTrigger,
245+
DropdownMenuContent,
246+
DropdownMenuGroup,
247+
DropdownMenuLabel,
248+
DropdownMenuItem,
249+
DropdownMenuCheckboxItem,
250+
DropdownMenuRadioGroup,
251+
DropdownMenuRadioItem,
252+
DropdownMenuSeparator,
253+
DropdownMenuShortcut,
254+
DropdownMenuSub,
255+
DropdownMenuSubTrigger,
256+
DropdownMenuSubContent,
257+
}

components/ui/popover.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as PopoverPrimitive from "@radix-ui/react-popover"
5+
6+
import { cn } from "@/lib/utils"
7+
8+
function Popover({
9+
...props
10+
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
11+
return <PopoverPrimitive.Root data-slot="popover" {...props} />
12+
}
13+
14+
function PopoverTrigger({
15+
...props
16+
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
17+
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
18+
}
19+
20+
function PopoverContent({
21+
className,
22+
align = "center",
23+
sideOffset = 4,
24+
...props
25+
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
26+
return (
27+
<PopoverPrimitive.Portal>
28+
<PopoverPrimitive.Content
29+
data-slot="popover-content"
30+
align={align}
31+
sideOffset={sideOffset}
32+
className={cn(
33+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
34+
className
35+
)}
36+
{...props}
37+
/>
38+
</PopoverPrimitive.Portal>
39+
)
40+
}
41+
42+
function PopoverAnchor({
43+
...props
44+
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
45+
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
46+
}
47+
48+
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

middleware.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import NextAuth from 'next-auth';
2+
23
import authConfig from './auth.config';
34

45
export const { auth: middleware } = NextAuth(authConfig);

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
"@hookform/resolvers": "^5.0.1",
1919
"@radix-ui/react-aspect-ratio": "^1.1.4",
2020
"@radix-ui/react-avatar": "^1.1.7",
21+
"@radix-ui/react-dropdown-menu": "^2.1.15",
2122
"@radix-ui/react-label": "^2.1.4",
2223
"@radix-ui/react-navigation-menu": "^1.2.12",
24+
"@radix-ui/react-popover": "^1.1.14",
2325
"@radix-ui/react-radio-group": "^1.3.4",
2426
"@radix-ui/react-select": "^2.2.2",
2527
"@radix-ui/react-separator": "^1.1.4",

0 commit comments

Comments
 (0)