Skip to content

Commit e796db4

Browse files
authored
feat: enhance auth flow with sign-out dialogs and redirect functionality (#206)
* feat: add sign-out confirmation dialogs * refactor: update terminology from "Login" to "Sign in" * feat: implement redirect functionality for sign-in flow * feat: add success toast notification on user sign-in * feat: enhance forgot password with loading state and toast notifications
1 parent 64d67d4 commit e796db4

8 files changed

Lines changed: 233 additions & 127 deletions

File tree

src/components/layout/nav-user.tsx

Lines changed: 72 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
LogOut,
88
Sparkles,
99
} from 'lucide-react'
10+
import useDialogState from '@/hooks/use-dialog-state'
1011
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
1112
import {
1213
DropdownMenu,
@@ -23,6 +24,7 @@ import {
2324
SidebarMenuItem,
2425
useSidebar,
2526
} from '@/components/ui/sidebar'
27+
import { SignOutDialog } from '@/components/sign-out-dialog'
2628

2729
type NavUserProps = {
2830
user: {
@@ -34,35 +36,18 @@ type NavUserProps = {
3436

3537
export function NavUser({ user }: NavUserProps) {
3638
const { isMobile } = useSidebar()
39+
const [open, setOpen] = useDialogState()
3740

3841
return (
39-
<SidebarMenu>
40-
<SidebarMenuItem>
41-
<DropdownMenu>
42-
<DropdownMenuTrigger asChild>
43-
<SidebarMenuButton
44-
size='lg'
45-
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
46-
>
47-
<Avatar className='h-8 w-8 rounded-lg'>
48-
<AvatarImage src={user.avatar} alt={user.name} />
49-
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
50-
</Avatar>
51-
<div className='grid flex-1 text-start text-sm leading-tight'>
52-
<span className='truncate font-semibold'>{user.name}</span>
53-
<span className='truncate text-xs'>{user.email}</span>
54-
</div>
55-
<ChevronsUpDown className='ms-auto size-4' />
56-
</SidebarMenuButton>
57-
</DropdownMenuTrigger>
58-
<DropdownMenuContent
59-
className='w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg'
60-
side={isMobile ? 'bottom' : 'right'}
61-
align='end'
62-
sideOffset={4}
63-
>
64-
<DropdownMenuLabel className='p-0 font-normal'>
65-
<div className='flex items-center gap-2 px-1 py-1.5 text-start text-sm'>
42+
<>
43+
<SidebarMenu>
44+
<SidebarMenuItem>
45+
<DropdownMenu>
46+
<DropdownMenuTrigger asChild>
47+
<SidebarMenuButton
48+
size='lg'
49+
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
50+
>
6651
<Avatar className='h-8 w-8 rounded-lg'>
6752
<AvatarImage src={user.avatar} alt={user.name} />
6853
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
@@ -71,44 +56,66 @@ export function NavUser({ user }: NavUserProps) {
7156
<span className='truncate font-semibold'>{user.name}</span>
7257
<span className='truncate text-xs'>{user.email}</span>
7358
</div>
74-
</div>
75-
</DropdownMenuLabel>
76-
<DropdownMenuSeparator />
77-
<DropdownMenuGroup>
78-
<DropdownMenuItem>
79-
<Sparkles />
80-
Upgrade to Pro
81-
</DropdownMenuItem>
82-
</DropdownMenuGroup>
83-
<DropdownMenuSeparator />
84-
<DropdownMenuGroup>
85-
<DropdownMenuItem asChild>
86-
<Link to='/settings/account'>
87-
<BadgeCheck />
88-
Account
89-
</Link>
90-
</DropdownMenuItem>
91-
<DropdownMenuItem asChild>
92-
<Link to='/settings'>
93-
<CreditCard />
94-
Billing
95-
</Link>
96-
</DropdownMenuItem>
97-
<DropdownMenuItem asChild>
98-
<Link to='/settings/notifications'>
99-
<Bell />
100-
Notifications
101-
</Link>
59+
<ChevronsUpDown className='ms-auto size-4' />
60+
</SidebarMenuButton>
61+
</DropdownMenuTrigger>
62+
<DropdownMenuContent
63+
className='w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg'
64+
side={isMobile ? 'bottom' : 'right'}
65+
align='end'
66+
sideOffset={4}
67+
>
68+
<DropdownMenuLabel className='p-0 font-normal'>
69+
<div className='flex items-center gap-2 px-1 py-1.5 text-start text-sm'>
70+
<Avatar className='h-8 w-8 rounded-lg'>
71+
<AvatarImage src={user.avatar} alt={user.name} />
72+
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
73+
</Avatar>
74+
<div className='grid flex-1 text-start text-sm leading-tight'>
75+
<span className='truncate font-semibold'>{user.name}</span>
76+
<span className='truncate text-xs'>{user.email}</span>
77+
</div>
78+
</div>
79+
</DropdownMenuLabel>
80+
<DropdownMenuSeparator />
81+
<DropdownMenuGroup>
82+
<DropdownMenuItem>
83+
<Sparkles />
84+
Upgrade to Pro
85+
</DropdownMenuItem>
86+
</DropdownMenuGroup>
87+
<DropdownMenuSeparator />
88+
<DropdownMenuGroup>
89+
<DropdownMenuItem asChild>
90+
<Link to='/settings/account'>
91+
<BadgeCheck />
92+
Account
93+
</Link>
94+
</DropdownMenuItem>
95+
<DropdownMenuItem asChild>
96+
<Link to='/settings'>
97+
<CreditCard />
98+
Billing
99+
</Link>
100+
</DropdownMenuItem>
101+
<DropdownMenuItem asChild>
102+
<Link to='/settings/notifications'>
103+
<Bell />
104+
Notifications
105+
</Link>
106+
</DropdownMenuItem>
107+
</DropdownMenuGroup>
108+
<DropdownMenuSeparator />
109+
<DropdownMenuItem onClick={() => setOpen(true)}>
110+
<LogOut />
111+
Sign out
102112
</DropdownMenuItem>
103-
</DropdownMenuGroup>
104-
<DropdownMenuSeparator />
105-
<DropdownMenuItem>
106-
<LogOut />
107-
Log out
108-
</DropdownMenuItem>
109-
</DropdownMenuContent>
110-
</DropdownMenu>
111-
</SidebarMenuItem>
112-
</SidebarMenu>
113+
</DropdownMenuContent>
114+
</DropdownMenu>
115+
</SidebarMenuItem>
116+
</SidebarMenu>
117+
118+
<SignOutDialog open={!!open} onOpenChange={setOpen} />
119+
</>
113120
)
114121
}
Lines changed: 54 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Link } from '@tanstack/react-router'
2+
import useDialogState from '@/hooks/use-dialog-state'
23
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
34
import { Button } from '@/components/ui/button'
45
import {
@@ -11,55 +12,62 @@ import {
1112
DropdownMenuShortcut,
1213
DropdownMenuTrigger,
1314
} from '@/components/ui/dropdown-menu'
15+
import { SignOutDialog } from '@/components/sign-out-dialog'
1416

1517
export function ProfileDropdown() {
18+
const [open, setOpen] = useDialogState()
19+
1620
return (
17-
<DropdownMenu modal={false}>
18-
<DropdownMenuTrigger asChild>
19-
<Button variant='ghost' className='relative h-8 w-8 rounded-full'>
20-
<Avatar className='h-8 w-8'>
21-
<AvatarImage src='/avatars/01.png' alt='@shadcn' />
22-
<AvatarFallback>SN</AvatarFallback>
23-
</Avatar>
24-
</Button>
25-
</DropdownMenuTrigger>
26-
<DropdownMenuContent className='w-56' align='end' forceMount>
27-
<DropdownMenuLabel className='font-normal'>
28-
<div className='flex flex-col gap-1.5'>
29-
<p className='text-sm leading-none font-medium'>satnaing</p>
30-
<p className='text-muted-foreground text-xs leading-none'>
31-
satnaingdev@gmail.com
32-
</p>
33-
</div>
34-
</DropdownMenuLabel>
35-
<DropdownMenuSeparator />
36-
<DropdownMenuGroup>
37-
<DropdownMenuItem asChild>
38-
<Link to='/settings'>
39-
Profile
40-
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
41-
</Link>
42-
</DropdownMenuItem>
43-
<DropdownMenuItem asChild>
44-
<Link to='/settings'>
45-
Billing
46-
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
47-
</Link>
21+
<>
22+
<DropdownMenu modal={false}>
23+
<DropdownMenuTrigger asChild>
24+
<Button variant='ghost' className='relative h-8 w-8 rounded-full'>
25+
<Avatar className='h-8 w-8'>
26+
<AvatarImage src='/avatars/01.png' alt='@shadcn' />
27+
<AvatarFallback>SN</AvatarFallback>
28+
</Avatar>
29+
</Button>
30+
</DropdownMenuTrigger>
31+
<DropdownMenuContent className='w-56' align='end' forceMount>
32+
<DropdownMenuLabel className='font-normal'>
33+
<div className='flex flex-col gap-1.5'>
34+
<p className='text-sm leading-none font-medium'>satnaing</p>
35+
<p className='text-muted-foreground text-xs leading-none'>
36+
satnaingdev@gmail.com
37+
</p>
38+
</div>
39+
</DropdownMenuLabel>
40+
<DropdownMenuSeparator />
41+
<DropdownMenuGroup>
42+
<DropdownMenuItem asChild>
43+
<Link to='/settings'>
44+
Profile
45+
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
46+
</Link>
47+
</DropdownMenuItem>
48+
<DropdownMenuItem asChild>
49+
<Link to='/settings'>
50+
Billing
51+
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
52+
</Link>
53+
</DropdownMenuItem>
54+
<DropdownMenuItem asChild>
55+
<Link to='/settings'>
56+
Settings
57+
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
58+
</Link>
59+
</DropdownMenuItem>
60+
<DropdownMenuItem>New Team</DropdownMenuItem>
61+
</DropdownMenuGroup>
62+
<DropdownMenuSeparator />
63+
<DropdownMenuItem onClick={() => setOpen(true)}>
64+
Sign out
65+
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
4866
</DropdownMenuItem>
49-
<DropdownMenuItem asChild>
50-
<Link to='/settings'>
51-
Settings
52-
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
53-
</Link>
54-
</DropdownMenuItem>
55-
<DropdownMenuItem>New Team</DropdownMenuItem>
56-
</DropdownMenuGroup>
57-
<DropdownMenuSeparator />
58-
<DropdownMenuItem>
59-
Log out
60-
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
61-
</DropdownMenuItem>
62-
</DropdownMenuContent>
63-
</DropdownMenu>
67+
</DropdownMenuContent>
68+
</DropdownMenu>
69+
70+
<SignOutDialog open={!!open} onOpenChange={setOpen} />
71+
</>
6472
)
6573
}

src/components/sign-out-dialog.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useNavigate, useLocation } from '@tanstack/react-router'
2+
import { useAuthStore } from '@/stores/auth-store'
3+
import { ConfirmDialog } from '@/components/confirm-dialog'
4+
5+
interface SignOutDialogProps {
6+
open: boolean
7+
onOpenChange: (open: boolean) => void
8+
}
9+
10+
export function SignOutDialog({ open, onOpenChange }: SignOutDialogProps) {
11+
const navigate = useNavigate()
12+
const location = useLocation()
13+
const { auth } = useAuthStore()
14+
15+
const handleSignOut = () => {
16+
auth.reset()
17+
// Preserve current location for redirect after sign-in
18+
const currentPath = location.href
19+
navigate({
20+
to: '/sign-in',
21+
search: { redirect: currentPath },
22+
replace: true,
23+
})
24+
}
25+
26+
return (
27+
<ConfirmDialog
28+
open={open}
29+
onOpenChange={onOpenChange}
30+
title='Sign out'
31+
desc='Are you sure you want to sign out? You will need to sign in again to access your account.'
32+
confirmText='Sign out'
33+
handleConfirm={handleSignOut}
34+
className='sm:max-w-sm'
35+
/>
36+
)
37+
}

src/features/auth/forgot-password/components/forgot-password-form.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { useState } from 'react'
22
import { z } from 'zod'
33
import { useForm } from 'react-hook-form'
44
import { zodResolver } from '@hookform/resolvers/zod'
5+
import { useNavigate } from '@tanstack/react-router'
6+
import { ArrowRight, Loader2 } from 'lucide-react'
7+
import { toast } from 'sonner'
58
import { cn } from '@/lib/utils'
9+
import { sleep } from '@/utils/sleep'
610
import { Button } from '@/components/ui/button'
711
import {
812
Form,
@@ -24,6 +28,7 @@ export function ForgotPasswordForm({
2428
className,
2529
...props
2630
}: React.HTMLAttributes<HTMLFormElement>) {
31+
const navigate = useNavigate()
2732
const [isLoading, setIsLoading] = useState(false)
2833

2934
const form = useForm<z.infer<typeof formSchema>>({
@@ -36,9 +41,16 @@ export function ForgotPasswordForm({
3641
// eslint-disable-next-line no-console
3742
console.log(data)
3843

39-
setTimeout(() => {
40-
setIsLoading(false)
41-
}, 3000)
44+
toast.promise(sleep(2000), {
45+
loading: 'Sending email...',
46+
success: () => {
47+
setIsLoading(false)
48+
form.reset()
49+
navigate({ to: '/otp' })
50+
return `Email sent to ${data.email}`
51+
},
52+
error: 'Error',
53+
})
4254
}
4355

4456
return (
@@ -63,6 +75,7 @@ export function ForgotPasswordForm({
6375
/>
6476
<Button className='mt-2' disabled={isLoading}>
6577
Continue
78+
{isLoading ? <Loader2 className='animate-spin' /> : <ArrowRight />}
6679
</Button>
6780
</form>
6881
</Form>

0 commit comments

Comments
 (0)