255 lines
9.3 KiB
TypeScript
255 lines
9.3 KiB
TypeScript
"use client";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
|
import { toast } from "sonner";
|
|
import { useState } from "react";
|
|
import { Loader2 } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { authClient } from "@/lib/auth-session/auth-client";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
|
|
export function ResetPasswordForm({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<"div">) {
|
|
const searchParams = useSearchParams();
|
|
const router = useRouter();
|
|
|
|
const token = searchParams.get("token");
|
|
|
|
const [password, setPassword] = useState("");
|
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
function togglePassword(e: React.MouseEvent<HTMLButtonElement>) {
|
|
e.preventDefault();
|
|
setShowPassword((s) => !s);
|
|
}
|
|
|
|
function toggleConfirm(e: React.MouseEvent<HTMLButtonElement>) {
|
|
e.preventDefault();
|
|
setShowConfirm((s) => !s);
|
|
}
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setIsLoading(true);
|
|
|
|
if (!token) {
|
|
toast.error("Invalid or missing reset token");
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
if (password.length < 8) {
|
|
toast.error("Password must be at least 8 characters");
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
if (password !== confirmPassword) {
|
|
toast.error("Passwords do not match");
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
const { error } = await authClient.resetPassword({
|
|
newPassword: password,
|
|
token,
|
|
});
|
|
|
|
if (error) {
|
|
toast.error(error.message || "Failed to reset password");
|
|
} else {
|
|
toast.success("Password reset successfully");
|
|
router.push("/sign-in");
|
|
}
|
|
|
|
setIsLoading(false);
|
|
}
|
|
|
|
return (
|
|
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
|
<Card>
|
|
<CardHeader className="text-center">
|
|
<CardTitle className="text-xl">Reset Password</CardTitle>
|
|
<CardDescription>Enter your new password</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={handleSubmit}>
|
|
<FieldGroup>
|
|
<Field>
|
|
<FieldLabel htmlFor="password">Password</FieldLabel>
|
|
<div className="relative">
|
|
<Input
|
|
id="password"
|
|
type={showPassword ? "text" : "password"}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="Enter your new password"
|
|
disabled={isLoading}
|
|
minLength={8}
|
|
required
|
|
/>
|
|
<button
|
|
aria-label={
|
|
showPassword ? "Hide password" : "Show password"
|
|
}
|
|
onClick={togglePassword}
|
|
type="button"
|
|
className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 text-sm opacity-70 hover:opacity-100"
|
|
>
|
|
{showPassword ? (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="16"
|
|
height="16"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
d="M13.875 18.825A10.05 10.05 0 0112 19c-5.523 0-10-4.477-10-10a9.97 9.97 0 012.175-5.875M6.343 6.343A9.97 9.97 0 0112 5c5.523 0 10 4.477 10 10 0 1.042-.161 2.045-.463 2.998M3 3l18 18"
|
|
/>
|
|
</svg>
|
|
) : (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="16"
|
|
height="16"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
/>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
/>
|
|
</svg>
|
|
)}
|
|
</button>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
Must be at least 8 characters
|
|
</p>
|
|
</Field>
|
|
<Field>
|
|
<FieldLabel htmlFor="confirmPassword">
|
|
Confirm Password
|
|
</FieldLabel>
|
|
<div className="relative">
|
|
<Input
|
|
id="confirmPassword"
|
|
type={showConfirm ? "text" : "password"}
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
placeholder="Confirm your new password"
|
|
disabled={isLoading}
|
|
minLength={8}
|
|
required
|
|
/>
|
|
<button
|
|
aria-label={
|
|
showConfirm
|
|
? "Hide confirm password"
|
|
: "Show confirm password"
|
|
}
|
|
onClick={toggleConfirm}
|
|
type="button"
|
|
className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center p-1 text-sm opacity-70 hover:opacity-100"
|
|
>
|
|
{showConfirm ? (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="16"
|
|
height="16"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
d="M13.875 18.825A10.05 10.05 0 0112 19c-5.523 0-10-4.477-10-10a9.97 9.97 0 012.175-5.875M6.343 6.343A9.97 9.97 0 0112 5c5.523 0 10 4.477 10 10 0 1.042-.161 2.045-.463 2.998M3 3l18 18"
|
|
/>
|
|
</svg>
|
|
) : (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="16"
|
|
height="16"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
/>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth="2"
|
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
/>
|
|
</svg>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</Field>
|
|
<Field>
|
|
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="size-4 animate-spin mr-2" />
|
|
Resetting...
|
|
</>
|
|
) : (
|
|
"Reset Password"
|
|
)}
|
|
</Button>
|
|
</Field>
|
|
<div className="text-center text-sm">
|
|
Don't have an account?{" "}
|
|
<Link href="/sign-up" className="underline underline-offset-4">
|
|
Sign up
|
|
</Link>
|
|
</div>
|
|
</FieldGroup>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
|
|
By clicking continue, you agree to our{" "}
|
|
<Link href="/docs/terms-of-service">Terms of Service</Link> and{" "}
|
|
<Link href="/docs/privacy-policy">Privacy Policy</Link>.
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|