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");
|
toast.success("Copied to clipboard");
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(error);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen bg-gradient-to-b from-black via-zinc-900 to-black">
|
<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))]" />
|
<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";
|
"use client";
|
||||||
|
|
||||||
import { Container } from "@/components/Container";
|
import { Container } from "@/components/Container";
|
||||||
|
import { SERVER_LICENSE_URL } from "@/components/pricing";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
export default function ResetLicensePage() {
|
export default function ResetLicensePage() {
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
|
const [showOtp, setShowOtp] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
@@ -14,24 +16,35 @@ export default function ResetLicensePage() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Here you would add the API call to reset the license
|
const result = await fetch(`${SERVER_LICENSE_URL}/license/verification`, {
|
||||||
// For now, we'll just simulate a success response
|
method: "POST",
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
});
|
||||||
|
|
||||||
// toast({
|
const data = await result.json();
|
||||||
// title: "Success!",
|
console.log(data);
|
||||||
// description:
|
|
||||||
// "If an account exists with this email, you will receive instructions to reset your license.",
|
|
||||||
// variant: "default",
|
|
||||||
// });
|
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
// toast({
|
toast.error("Something went wrong. Please try again later.", {
|
||||||
// title: "Error",
|
duration: 15000,
|
||||||
// description: "Something went wrong. Please try again later.",
|
description: error instanceof Error ? error.message : "Unknown error",
|
||||||
// variant: "destructive",
|
});
|
||||||
// });
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default async function RootLayout({
|
|||||||
<body>
|
<body>
|
||||||
<NextIntlClientProvider messages={messages}>
|
<NextIntlClientProvider messages={messages}>
|
||||||
{children}
|
{children}
|
||||||
<Toaster />
|
<Toaster richColors />
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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",
|
"copy-to-clipboard": "3.3.3",
|
||||||
"framer-motion": "^11.3.19",
|
"framer-motion": "^11.3.19",
|
||||||
"hast-util-to-jsx-runtime": "2.3.5",
|
"hast-util-to-jsx-runtime": "2.3.5",
|
||||||
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "0.364.0",
|
"lucide-react": "0.364.0",
|
||||||
"next": "15.2.0",
|
"next": "15.2.0",
|
||||||
"next-intl": "^3.26.5",
|
"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:
|
hast-util-to-jsx-runtime:
|
||||||
specifier: 2.3.5
|
specifier: 2.3.5
|
||||||
version: 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:
|
lucide-react:
|
||||||
specifier: 0.364.0
|
specifier: 0.364.0
|
||||||
version: 0.364.0(react@18.2.0)
|
version: 0.364.0(react@18.2.0)
|
||||||
@@ -2551,6 +2554,12 @@ packages:
|
|||||||
inline-style-parser@0.2.3:
|
inline-style-parser@0.2.3:
|
||||||
resolution: {integrity: sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==}
|
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:
|
intl-messageformat@10.5.14:
|
||||||
resolution: {integrity: sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==}
|
resolution: {integrity: sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==}
|
||||||
|
|
||||||
@@ -6468,6 +6477,11 @@ snapshots:
|
|||||||
|
|
||||||
inline-style-parser@0.2.3: {}
|
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:
|
intl-messageformat@10.5.14:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/ecma402-abstract': 2.0.0
|
'@formatjs/ecma402-abstract': 2.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user