Component Details
Auth Component 04

ResetPasswordContent.tsx
Next.js•Tailwind CSS
1// app/auth/reset-password/reset-password-content.tsx
2"use client";
3
4import type React from "react";
5import { useState, useEffect } from "react";
6import { useRouter, useSearchParams } from "next/navigation";
7import Link from "next/link";
8import { Button } from "@/components/ui/button";
9import { Input } from "@/components/ui/input";
10import {
11 Card,
12 CardContent,
13 CardHeader,
14 CardTitle,
15 CardDescription,
16 CardFooter,
17} from "@/components/ui/card";
18import { Alert, AlertDescription } from "@/components/ui/alert";
19import { Label } from "@/components/ui/label";
20import {
21 AlertCircle,
22 Loader2,
23 CheckCircle,
24 Eye,
25 EyeOff,
26 ArrowLeft,
27} from "lucide-react";
28
29export default function ResetPasswordContent() {
30 const [newPassword, setNewPassword] = useState("");
31 const [confirmPassword, setConfirmPassword] = useState("");
32 const [showNewPassword, setShowNewPassword] = useState(false);
33 const [showConfirmPassword, setShowConfirmPassword] = useState(false);
34 const [error, setError] = useState("");
35 const [success, setSuccess] = useState("");
36 const [isLoading, setIsLoading] = useState(false);
37 const [isValidToken, setIsValidToken] = useState(false);
38 const [email, setEmail] = useState("");
39 const router = useRouter();
40 const searchParams = useSearchParams();
41
42 useEffect(() => {
43 // Check for token in URL
44 const token = searchParams.get("token");
45
46 // Simulate token verification
47 setTimeout(() => {
48 if (token === "demo-token" || !token) {
49 setIsValidToken(true);
50 // Get email from localStorage (simulated)
51 const savedEmail =
52 typeof window !== "undefined"
53 ? localStorage.getItem("passwordResetEmail") || "user@example.com"
54 : "user@example.com";
55 setEmail(savedEmail);
56 } else {
57 setError(
58 "Invalid or expired reset token. Please request a new reset link."
59 );
60 }
61 }, 500);
62 }, [searchParams]);
63
64 const handleResetPassword = async (e: React.FormEvent) => {
65 e.preventDefault();
66 setError("");
67 setIsLoading(true);
68
69 // Validate
70 if (!newPassword || !confirmPassword) {
71 setError("Please enter and confirm your new password");
72 setIsLoading(false);
73 return;
74 }
75
76 if (newPassword.length < 8) {
77 setError("Password must be at least 8 characters long");
78 setIsLoading(false);
79 return;
80 }
81
82 if (newPassword !== confirmPassword) {
83 setError("Passwords do not match");
84 setIsLoading(false);
85 return;
86 }
87
88 // Simulate API call
89 await new Promise((resolve) => setTimeout(resolve, 1500));
90
91 // Success
92 setSuccess(
93 "Password reset successfully! You can now sign in with your new password."
94 );
95
96 // Clear the reset email from storage
97 if (typeof window !== "undefined") {
98 localStorage.removeItem("passwordResetEmail");
99
100 // Store demo password reset
101 localStorage.setItem(
102 "passwordReset",
103 JSON.stringify({
104 email: email,
105 resetAt: new Date().toISOString(),
106 })
107 );
108 }
109
110 // Redirect to login after delay
111 setTimeout(() => {
112 router.push("/signin");
113 }, 2000);
114
115 setIsLoading(false);
116 };
117
118 if (!isValidToken && error) {
119 return (
120 <div className="flex flex-col">
121 <div className="flex-1 flex items-center justify-center px-4 py-12">
122 <div className="w-full max-w-md">
123 <Card className="border shadow-sm">
124 <CardHeader>
125 <CardTitle className="text-2xl">Invalid Reset Link</CardTitle>
126 <CardDescription>
127 This password reset link is invalid or has expired
128 </CardDescription>
129 </CardHeader>
130 <CardContent>
131 <Alert variant="destructive">
132 <AlertCircle className="h-4 w-4" />
133 <AlertDescription>{error}</AlertDescription>
134 </Alert>
135
136 <div className="mt-4 space-y-3">
137 <p className="text-sm text-muted-foreground">
138 Please request a new password reset link.
139 </p>
140 <Button className="w-full" asChild>
141 <Link href="/forgot-password">Request New Reset Link</Link>
142 </Button>
143 </div>
144 </CardContent>
145 <CardFooter>
146 <Button variant="outline" className="w-full" asChild>
147 <Link href="/">
148 <ArrowLeft className="mr-2 h-4 w-4" />
149 Back to Home
150 </Link>
151 </Button>
152 </CardFooter>
153 </Card>
154 </div>
155 </div>
156 </div>
157 );
158 }
159
160 return (
161 <div className="flex flex-col">
162 <main className="flex-1 flex items-center justify-center px-4 py-12">
163 <div className="w-full max-w-md">
164 <div className="text-center mb-8">
165 <h1 className="text-3xl font-bold mb-2">Create new password</h1>
166 <p className="text-muted-foreground">
167 Set a new password for your account
168 </p>
169 {email && (
170 <p className="text-sm text-muted-foreground mt-2">
171 Resetting password for:{" "}
172 <span className="font-medium">{email}</span>
173 </p>
174 )}
175 </div>
176
177 <Card className="border shadow-sm">
178 <CardHeader className="space-y-1">
179 <CardTitle className="text-2xl">Reset Password</CardTitle>
180 <CardDescription>
181 Enter a new password for your account
182 </CardDescription>
183 </CardHeader>
184
185 <CardContent>
186 <form onSubmit={handleResetPassword} className="space-y-4">
187 <div className="space-y-2">
188 <Label htmlFor="newPassword">New password</Label>
189 <div className="relative">
190 <Input
191 id="newPassword"
192 type={showNewPassword ? "text" : "password"}
193 placeholder="Enter new password (min. 8 characters)"
194 className="pr-10"
195 value={newPassword}
196 onChange={(e) => setNewPassword(e.target.value)}
197 disabled={isLoading}
198 autoComplete="new-password"
199 />
200 <Button
201 type="button"
202 variant="ghost"
203 size="sm"
204 className="absolute right-0 top-0 h-full px-3"
205 onClick={() => setShowNewPassword(!showNewPassword)}
206 >
207 {showNewPassword ? (
208 <EyeOff className="h-4 w-4" />
209 ) : (
210 <Eye className="h-4 w-4" />
211 )}
212 </Button>
213 </div>
214 </div>
215
216 <div className="space-y-2">
217 <Label htmlFor="confirmPassword">Confirm new password</Label>
218 <div className="relative">
219 <Input
220 id="confirmPassword"
221 type={showConfirmPassword ? "text" : "password"}
222 placeholder="Confirm your new password"
223 className="pr-10"
224 value={confirmPassword}
225 onChange={(e) => setConfirmPassword(e.target.value)}
226 disabled={isLoading}
227 autoComplete="new-password"
228 />
229 <Button
230 type="button"
231 variant="ghost"
232 size="sm"
233 className="absolute right-0 top-0 h-full px-3"
234 onClick={() =>
235 setShowConfirmPassword(!showConfirmPassword)
236 }
237 >
238 {showConfirmPassword ? (
239 <EyeOff className="h-4 w-4" />
240 ) : (
241 <Eye className="h-4 w-4" />
242 )}
243 </Button>
244 </div>
245 </div>
246
247 {error && (
248 <Alert variant="destructive">
249 <AlertCircle className="h-4 w-4" />
250 <AlertDescription>{error}</AlertDescription>
251 </Alert>
252 )}
253
254 {success && (
255 <Alert
256 variant="default"
257 className="bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800"
258 >
259 <CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400" />
260 <AlertDescription className="text-green-800 dark:text-green-300">
261 {success}
262 </AlertDescription>
263 </Alert>
264 )}
265
266 <Button type="submit" className="w-full" disabled={isLoading}>
267 {isLoading ? (
268 <>
269 <Loader2 className="mr-2 h-4 w-4 animate-spin" />
270 Resetting password...
271 </>
272 ) : (
273 "Reset Password"
274 )}
275 </Button>
276 </form>
277 </CardContent>
278
279 <CardFooter className="flex flex-col space-y-3">
280 <div className="text-center text-sm text-muted-foreground w-full">
281 <div className="space-y-3">
282 <div className="relative">
283 <div className="absolute inset-0 flex items-center">
284 <div className="w-full border-t"></div>
285 </div>
286 <div className="relative flex justify-center text-xs">
287 <span className="bg-background px-2 text-muted-foreground">
288 Remember your password?
289 </span>
290 </div>
291 </div>
292
293 <Button
294 type="button"
295 variant="outline"
296 className="w-full"
297 asChild
298 >
299 <Link href="/signin">Back to Sign in</Link>
300 </Button>
301 </div>
302 </div>
303 </CardFooter>
304 </Card>
305 </div>
306 </main>
307 </div>
308 );
309}
310
311// app/auth/reset-password/page.tsx - Main page with Suspense
312import { Suspense } from "react";
313import ResetPasswordContent from "./reset-password-content";
314
315export default function ResetPasswordPage() {
316 return (
317 <Suspense fallback={<ResetPasswordSkeleton />}>
318 <ResetPasswordContent />
319 </Suspense>
320 );
321}
322
323// Skeleton loading component
324function ResetPasswordSkeleton() {
325 return (
326 <div className="flex flex-col">
327 <main className="flex-1 flex items-center justify-center px-4 py-12">
328 <div className="w-full max-w-md">
329 <div className="text-center mb-8 animate-pulse">
330 <div className="h-8 bg-gray-200 dark:bg-gray-800 rounded w-3/4 mx-auto mb-2"></div>
331 <div className="h-4 bg-gray-200 dark:bg-gray-800 rounded w-1/2 mx-auto"></div>
332 </div>
333 <div className="border shadow-sm rounded-lg p-6 space-y-6 animate-pulse">
334 <div className="h-6 bg-gray-200 dark:bg-gray-800 rounded w-1/2"></div>
335 <div className="space-y-4">
336 <div className="h-10 bg-gray-200 dark:bg-gray-800 rounded"></div>
337 <div className="h-10 bg-gray-200 dark:bg-gray-800 rounded"></div>
338 <div className="h-12 bg-gray-200 dark:bg-gray-800 rounded"></div>
339 </div>
340 </div>
341 </div>
342 </main>
343 </div>
344 );
345}