Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"flat": "^6.0.1",
"framer-motion": "11.9.0",
"fuse.js": "7.0.0",
"input-otp": "^1.2.4",
"ioredis": "^5.4.1",
"ipaddr.js": "^2.2.0",
"lottie-react": "^2.4.0",
Expand All @@ -87,7 +88,6 @@
"react-icons": "^5.2.1",
"react-intersection-observer": "^9.10.3",
"react-markdown": "^9.0.1",
"react-otp-input": "^3.1.1",
"react-responsive-carousel": "^3.2.23",
"react-table": "^7.8.0",
"recharts": "^2.12.7",
Expand Down
72 changes: 72 additions & 0 deletions apps/dashboard/src/@/components/ui/input-otp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"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-input border-y border-r 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) => (
// biome-ignore lint/a11y/useFocusableInteractive: <explanation>
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
));
InputOTPSeparator.displayName = "InputOTPSeparator";

export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
69 changes: 30 additions & 39 deletions apps/dashboard/src/components/onboarding/ConfirmEmail.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { cn } from "@/lib/utils";
import {
useConfirmEmail,
useResendEmailConfirmation,
} from "@3rdweb-sdk/react/hooks/useApi";
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import { Flex, Input } from "@chakra-ui/react";
import { Flex } from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import {
type EmailConfirmationValidationSchema,
Expand All @@ -12,9 +18,9 @@ import {
import { useErrorHandler } from "contexts/error-handler";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
import { type ClipboardEvent, useState } from "react";
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
import { useState } from "react";
import { useForm } from "react-hook-form";
import OtpInput from "react-otp-input";
import { Button, Text } from "tw-components";
import { shortenString } from "utils/usedapp-external";
import { TitleAndDescription } from "./Title";
Expand Down Expand Up @@ -148,14 +154,6 @@ const OnboardingConfirmEmail: React.FC<OnboardingConfirmEmailProps> = ({
});
};

const handlePaste = (e: ClipboardEvent<HTMLDivElement>) => {
const data = e.clipboardData.getData("text");
if (data?.match(/^[A-Z]{6}$/)) {
form.setValue("confirmationToken", data);
handleSubmit();
}
};

return (
<>
<TitleAndDescription
Expand Down Expand Up @@ -197,36 +195,29 @@ const OnboardingConfirmEmail: React.FC<OnboardingConfirmEmailProps> = ({
{!completed && (
<form onSubmit={handleSubmit}>
<Flex gap={8} flexDir="column" w="full">
<OtpInput
shouldAutoFocus
<InputOTP
maxLength={6}
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
value={token}
onChange={handleChange}
onPaste={handlePaste}
skipDefaultStyles
numInputs={6}
containerStyle={{
display: "flex",
flexDirection: "row",
gap: "12px",
}}
renderInput={(props) => (
<Input
{...props}
w={20}
h={16}
rounded="md"
textAlign="center"
fontSize="larger"
isDisabled={saving}
borderColor={
form.getFieldState("confirmationToken", form.formState)
.error
? "red.500"
: "borderColor"
}
/>
)}
/>
disabled={saving}
>
<InputOTPGroup className="w-full">
{new Array(6).fill(0).map((_, idx) => (
<InputOTPSlot
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
key={idx}
index={idx}
className={cn("h-12 grow text-lg", {
"border-red-500": form.getFieldState(
"confirmationToken",
form.formState,
).error,
})}
/>
))}
</InputOTPGroup>
</InputOTP>

<Flex flexDir="column" gap={3}>
<Button
Expand Down
5 changes: 5 additions & 0 deletions apps/dashboard/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,16 @@ module.exports = {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
skeleton: "skeleton 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
"caret-blink": "caret-blink 1.25s ease-out infinite",
},
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
Expand Down
Loading
Loading