mirror of
https://github.com/Dokploy/website
synced 2025-06-26 18:16:01 +00:00
feat: add input-otp component for enhanced email verification
This commit is contained in:
@@ -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))]" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default async function RootLayout({
|
||||
<body>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
{children}
|
||||
<Toaster />
|
||||
<Toaster richColors />
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
71
apps/website/components/ui/input-otp.tsx
Normal file
71
apps/website/components/ui/input-otp.tsx
Normal 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 };
|
||||
@@ -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
14
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user