Skip to content

Commit f157c21

Browse files
Merge pull request #15 from Sreejit-Sengupto/feat/profile-page
profile page for users details and other ops
2 parents 65fd265 + 77d5abb commit f157c21

File tree

12 files changed

+729
-4
lines changed

12 files changed

+729
-4
lines changed

public/profile_fallback.png

18 KB
Loading

src/app/(authenticated)/profile/page.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
1+
import DangerZone from "@/components/application/profile/danger-zone";
2+
import ProfileDetails from "@/components/application/profile/details";
13
import React from "react";
24

35
const Profile = () => {
46
return (
5-
<div className="flex h-full w-full flex-1 flex-col gap-2 rounded-tl-2xl bg-background p-5">
6-
Profile
7-
</div>
7+
<section className="flex h-full w-full flex-1 flex-col gap-2 rounded-tl-2xl bg-background p-5 overflow-auto">
8+
<p className="scroll-m-20 text-2xl font-semibold tracking-tight">
9+
User Profile
10+
</p>
11+
12+
<div className="mt-20 flex justify-center items-center">
13+
<ProfileDetails />
14+
</div>
15+
16+
<div className="mt-10">
17+
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight text-red-500 mb-2">
18+
Danger Zone
19+
</h3>
20+
<DangerZone />
21+
</div>
22+
</section>
823
);
924
};
1025

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"use client";
2+
import ResetPasswordForm from "@/components/application/authentication/reset-password-form";
3+
import React from "react";
4+
import Image from "next/image";
5+
6+
const ResetPassword = () => {
7+
return (
8+
<section className="w-full lg:grid grid-cols-2 place-items-center">
9+
<div className="lg:hidden absolute top-1/12 lg:top-1/3 left-0 w-full h-[250px] bg-gradient-to-b lg:bg-gradient-to-l from-pink-500/40 via-purple-500/40 to-blue-500/40 blur-[120px]"></div>
10+
<section className="w-[90%] lg:w-[60%]">
11+
<ResetPasswordForm />
12+
</section>
13+
14+
<section className="hidden relative w-full lg:flex justify-center items-center">
15+
<div className="absolute top-1/5 lg:top-1/3 left-0 w-full h-[250px] bg-gradient-to-b lg:bg-gradient-to-l from-pink-500/40 via-purple-500/40 to-blue-500/40 blur-[120px]"></div>
16+
<Image
17+
src={"/pixel_hero.gif"}
18+
alt="hero img"
19+
width={470}
20+
height={470}
21+
className="rounded-4xl shadow-2xl border border-black"
22+
/>
23+
</section>
24+
</section>
25+
);
26+
};
27+
28+
export default ResetPassword;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"use server";
2+
3+
import { auth, reverificationError } from "@clerk/nextjs/server";
4+
5+
export const reverifyUser = async () => {
6+
const { has } = await auth.protect();
7+
const shouldUserRevalidate = !has({ reverification: "strict" });
8+
if (shouldUserRevalidate) {
9+
return reverificationError("strict");
10+
}
11+
return { success: true };
12+
};

src/components/application/authentication/authentication-form.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,10 +383,13 @@ const AuthenticationForm: React.FC<TAuthForm> = ({ type }) => {
383383
)}
384384
</section>
385385

386-
<section className="my-4">
386+
<section className="my-4 flex flex-col gap-2 justify-center items-center">
387387
<Link href={redirectLink} className="text-blue-400">
388388
{footerText}
389389
</Link>
390+
{type === 'login' && <Link href={'/reset-password'} className="text-blue-200">
391+
Forgot Password?
392+
</Link>}
390393
</section>
391394
</CardFooter>
392395
</Card>
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
"use client";
2+
import {
3+
Card,
4+
CardContent,
5+
CardFooter,
6+
CardHeader,
7+
CardTitle,
8+
} from "@/components/ui/card";
9+
import React, { useEffect, useState } from "react";
10+
import Logo from "../logo";
11+
import { Input } from "@/components/ui/input";
12+
import { Label } from "@/components/ui/label";
13+
import { Eye, EyeClosed, Loader2 } from "lucide-react";
14+
import { Button } from "@/components/ui/button";
15+
import Link from "next/link";
16+
import { useAuth, useSignIn } from "@clerk/nextjs";
17+
import {
18+
InputOTP,
19+
InputOTPGroup,
20+
InputOTPSlot,
21+
} from "@/components/ui/input-otp";
22+
import { useRouter } from "next/navigation";
23+
import { showError, showSuccess, showWarning } from "@/lib/sonner";
24+
25+
const ResetPasswordForm = () => {
26+
// states
27+
const [email, setEmail] = useState('')
28+
const [password, setPassword] = useState('')
29+
const [code, setCode] = useState('')
30+
const [successfulCreation, setSuccessfulCreation] = useState(false)
31+
const [secondFactor, setSecondFactor] = useState(false)
32+
const [error, setError] = useState('')
33+
const [showPassword, setShowPassword] = useState(false)
34+
const [cooldown, setCooldown] = useState(60);
35+
const [loaders, setLoaders] = useState({
36+
createLoader: false,
37+
resetLoader: false
38+
})
39+
40+
41+
// hooks
42+
const router = useRouter()
43+
const { isSignedIn } = useAuth()
44+
const { isLoaded, signIn, setActive } = useSignIn()
45+
46+
useEffect(() => {
47+
if (isSignedIn) {
48+
router.push('/')
49+
}
50+
}, [isSignedIn, router])
51+
52+
useEffect(() => {
53+
if (cooldown <= 0) {
54+
return;
55+
}
56+
const timer = setInterval(() => {
57+
setCooldown((prev) => prev - 1);
58+
}, 1000);
59+
60+
return () => clearInterval(timer);
61+
}, [cooldown]);
62+
63+
64+
if (!isLoaded) {
65+
return null
66+
}
67+
68+
async function create(e: React.FormEvent) {
69+
e.preventDefault()
70+
if (!email) {
71+
showWarning("Email is required")
72+
return;
73+
}
74+
setLoaders((prev) => ({ ...prev, createLoader: true }))
75+
await signIn
76+
?.create({
77+
strategy: 'reset_password_email_code',
78+
identifier: email,
79+
})
80+
.then(() => {
81+
setSuccessfulCreation(true)
82+
showSuccess("Code sent to your E-mail")
83+
setError('')
84+
})
85+
.catch((err) => {
86+
console.error('error', err.errors[0].longMessage)
87+
setError(err.errors[0].longMessage)
88+
showError(err.errors[0].longMessage)
89+
}).finally(() => setLoaders((prev) => ({ ...prev, createLoader: false })))
90+
}
91+
92+
// Reset the user's password.
93+
// Upon successful reset, the user will be
94+
// signed in and redirected to the home page
95+
async function reset(e: React.FormEvent) {
96+
e.preventDefault()
97+
setLoaders((prev) => ({ ...prev, resetLoader: true }))
98+
await signIn
99+
?.attemptFirstFactor({
100+
strategy: 'reset_password_email_code',
101+
code,
102+
password,
103+
})
104+
.then((result) => {
105+
// Check if 2FA is required
106+
if (result.status === 'needs_second_factor') {
107+
setSecondFactor(true)
108+
setError('')
109+
} else if (result.status === 'complete') {
110+
// Set the active session to
111+
// the newly created session (user is now signed in)
112+
setActive({
113+
session: result.createdSessionId,
114+
navigate: async ({ session }) => {
115+
if (session?.currentTask) {
116+
// Check for tasks and navigate to custom UI to help users resolve them
117+
// See https://clerk.com/docs/custom-flows/overview#session-tasks
118+
console.log(session?.currentTask)
119+
return
120+
}
121+
122+
router.push('/workplace')
123+
},
124+
})
125+
showSuccess("Password reset success. Go to Dashboard!")
126+
setError('')
127+
} else {
128+
console.log(result)
129+
}
130+
})
131+
.catch((err) => {
132+
console.error('error', err.errors[0].longMessage)
133+
setError(err.errors[0].longMessage)
134+
showError(err.errors[0].longMessage)
135+
}).finally(() => setLoaders((prev) => ({ ...prev, resetLoader: false })))
136+
}
137+
138+
return (
139+
<Card>
140+
<CardHeader>
141+
<CardTitle className="flex flex-col lg:flex-row text-center justify-center items-center gap-3 text-2xl text-wrap">
142+
<span>Reset your password</span>
143+
<span>
144+
<Logo textClasses="text-2xl" hideLogo />
145+
</span>
146+
</CardTitle>
147+
</CardHeader>
148+
<CardContent>
149+
<form onSubmit={!successfulCreation ? create : reset}>
150+
{!successfulCreation && (
151+
<div className="flex flex-col justify-center items-center gap-6">
152+
<div className="w-full space-y-2">
153+
<Label htmlFor="email">Provide your email address</Label>
154+
<Input
155+
type="email"
156+
placeholder="e.g john@doe.com"
157+
value={email}
158+
onChange={(e) => setEmail(e.target.value)}
159+
/>
160+
</div>
161+
162+
<Button className="cursor-pointer" disabled={loaders.createLoader}>{loaders.createLoader ? <Loader2 className="animate-spin" /> : "Send password reset code"}</Button>
163+
{error && <p>{error}</p>}
164+
</div>
165+
)}
166+
167+
{successfulCreation && (
168+
<div className="flex flex-col justify-center items-center gap-6">
169+
<div className="relative w-full space-y-2">
170+
<Label htmlFor="password">Enter your new password</Label>
171+
<Input
172+
placeholder="ssshhhhh....."
173+
type={showPassword ? "text" : "password"}
174+
value={password}
175+
onChange={(e) => {
176+
e.preventDefault()
177+
setPassword(e.target.value)
178+
}}
179+
required
180+
/>
181+
<button
182+
onClick={(e) => {
183+
e.preventDefault()
184+
setShowPassword(prev => !prev)
185+
}}
186+
className="absolute right-2 top-1/2 "
187+
>
188+
{showPassword ? (
189+
<Eye
190+
className="cursor-pointer"
191+
color="gray"
192+
size={20}
193+
/>
194+
) : (
195+
<EyeClosed
196+
className="cursor-pointer"
197+
color="gray"
198+
size={20}
199+
/>
200+
)}
201+
</button>
202+
</div>
203+
204+
<div className="w-full space-y-2">
205+
<Label htmlFor="code">Enter the password reset code that was sent to your email</Label>
206+
<InputOTP
207+
maxLength={6}
208+
value={code}
209+
onChange={setCode}
210+
>
211+
<InputOTPGroup>
212+
<InputOTPSlot index={0} />
213+
<InputOTPSlot index={1} />
214+
<InputOTPSlot index={2} />
215+
<InputOTPSlot index={3} />
216+
<InputOTPSlot index={4} />
217+
<InputOTPSlot index={5} />
218+
</InputOTPGroup>
219+
</InputOTP>
220+
</div>
221+
<Button className="w-full cursor-pointer" disabled={loaders.resetLoader}>{loaders.resetLoader ? <Loader2 className="animate-spin" /> : "Reset"}</Button>
222+
<Button
223+
variant={"outline"}
224+
className="mx-auto cursor-pointer"
225+
disabled={cooldown > 0 || loaders.resetLoader}
226+
onClick={(e) => {
227+
e.preventDefault();
228+
create(e);
229+
setCooldown(60);
230+
}}
231+
>
232+
{cooldown > 0 ? (
233+
`Resend verification code in ${cooldown}`
234+
) : loaders.resetLoader ? (
235+
<Loader2 className="animate-spin" />
236+
) : (
237+
"Resend verification code"
238+
)}
239+
</Button>
240+
{error && <p>{error}</p>}
241+
</div>
242+
)}
243+
244+
{secondFactor && <p>2FA is required, but this UI does not handle that</p>}
245+
</form>
246+
</CardContent>
247+
248+
<CardFooter className="flex flex-col gap-2 justify-center items-center">
249+
<section className="my-4">
250+
<Link href="/login" className="text-blue-400">
251+
Back to login
252+
</Link>
253+
</section>
254+
</CardFooter>
255+
</Card>
256+
);
257+
};
258+
259+
export default ResetPasswordForm;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from "react";
2+
import ChangePassword from "./danger-zone/change-password";
3+
import DeleteAccount from "./danger-zone/delete-account";
4+
5+
const DangerZone = () => {
6+
return (
7+
<section className="w-full bg-red-950/60 rounded-lg flex flex-col justify-center items-center divide-y-2">
8+
<div className="w-full flex flex-col lg:flex-row justify-between items-center p-5">
9+
<div>
10+
<h2 className="scroll-m-20 text-xl font-semibold tracking-tight first:mt-0">
11+
Update Password
12+
</h2>
13+
<p className="leading-7 [&:not(:first-child)]:mt-0 text-sm">
14+
Change your old password with a new one.
15+
</p>
16+
</div>
17+
<ChangePassword />
18+
</div>
19+
20+
<div className="w-full flex flex-col lg:flex-row justify-between items-center p-5">
21+
<div>
22+
<h2 className="scroll-m-20 text-xl font-semibold tracking-tight first:mt-0">
23+
Delete Account
24+
</h2>
25+
<p className="leading-7 [&:not(:first-child)]:mt-0 text-sm">
26+
Permanently delete your account from flag0ut.
27+
</p>
28+
</div>
29+
<DeleteAccount />
30+
</div>
31+
</section>
32+
);
33+
};
34+
35+
export default DangerZone;

0 commit comments

Comments
 (0)