feat: add input-otp component for enhanced email verification

This commit is contained in:
Mauricio Siu
2025-03-23 14:23:09 -06:00
parent a0583f05ed
commit 054627e80d
6 changed files with 116 additions and 19 deletions

View File

@@ -80,8 +80,6 @@ export default function LicenseSuccess() {
toast.success("Copied to clipboard");
};
console.log(error);
return (
<div className="relative min-h-screen bg-gradient-to-b from-black via-zinc-900 to-black">
<div className="absolute inset-0 bg-[url('/grid.svg')] bg-center [mask-image:linear-gradient(180deg,white,rgba(255,255,255,0))]" />

View File

@@ -1,12 +1,14 @@
"use client";
import { Container } from "@/components/Container";
import { SERVER_LICENSE_URL } from "@/components/pricing";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useState } from "react";
import { toast } from "sonner";
export default function ResetLicensePage() {
const [email, setEmail] = useState("");
const [showOtp, setShowOtp] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
@@ -14,24 +16,35 @@ export default function ResetLicensePage() {
setIsLoading(true);
try {
// Here you would add the API call to reset the license
// For now, we'll just simulate a success response
await new Promise((resolve) => setTimeout(resolve, 1500));
const result = await fetch(`${SERVER_LICENSE_URL}/license/verification`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
});
// toast({
// title: "Success!",
// description:
// "If an account exists with this email, you will receive instructions to reset your license.",
// variant: "default",
// });
const data = await result.json();
console.log(data);
setEmail("");
if (data.error) {
toast.error(
"Error sending verification code. Please try again later.",
{
description: data.error,
},
);
} else {
toast.success(
"We've sent you a code to verify your email. Please check your email for the code.",
);
setShowOtp(true);
}
} catch (error) {
// toast({
// title: "Error",
// description: "Something went wrong. Please try again later.",
// variant: "destructive",
// });
toast.error("Something went wrong. Please try again later.", {
duration: 15000,
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setIsLoading(false);
}

View File

@@ -64,7 +64,7 @@ export default async function RootLayout({
<body>
<NextIntlClientProvider messages={messages}>
{children}
<Toaster />
<Toaster richColors />
</NextIntlClientProvider>
</body>
</html>

View File

@@ -0,0 +1,71 @@
"use client";
import { OTPInput, OTPInputContext } from "input-otp";
import { Dot } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
containerClassName,
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
));
InputOTP.displayName = "InputOTP";
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} />
));
InputOTPGroup.displayName = "InputOTPGroup";
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
return (
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>
);
});
InputOTPSlot.displayName = "InputOTPSlot";
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
));
InputOTPSeparator.displayName = "InputOTPSeparator";
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View File

@@ -36,6 +36,7 @@
"copy-to-clipboard": "3.3.3",
"framer-motion": "^11.3.19",
"hast-util-to-jsx-runtime": "2.3.5",
"input-otp": "^1.4.2",
"lucide-react": "0.364.0",
"next": "15.2.0",
"next-intl": "^3.26.5",

14
pnpm-lock.yaml generated
View File

@@ -163,6 +163,9 @@ importers:
hast-util-to-jsx-runtime:
specifier: 2.3.5
version: 2.3.5
input-otp:
specifier: ^1.4.2
version: 1.4.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
lucide-react:
specifier: 0.364.0
version: 0.364.0(react@18.2.0)
@@ -2551,6 +2554,12 @@ packages:
inline-style-parser@0.2.3:
resolution: {integrity: sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==}
input-otp@1.4.2:
resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
intl-messageformat@10.5.14:
resolution: {integrity: sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==}
@@ -6468,6 +6477,11 @@ snapshots:
inline-style-parser@0.2.3: {}
input-otp@1.4.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
intl-messageformat@10.5.14:
dependencies:
'@formatjs/ecma402-abstract': 2.0.0