@realtimex/email-automator 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/server.ts +4 -8
- package/api/src/config/index.ts +6 -3
- package/bin/email-automator-setup.js +2 -3
- package/bin/email-automator.js +7 -11
- package/dist/api/server.js +109 -0
- package/dist/api/src/config/index.js +88 -0
- package/dist/api/src/middleware/auth.js +119 -0
- package/dist/api/src/middleware/errorHandler.js +78 -0
- package/dist/api/src/middleware/index.js +4 -0
- package/dist/api/src/middleware/rateLimit.js +57 -0
- package/dist/api/src/middleware/validation.js +111 -0
- package/dist/api/src/routes/actions.js +173 -0
- package/dist/api/src/routes/auth.js +106 -0
- package/dist/api/src/routes/emails.js +100 -0
- package/dist/api/src/routes/health.js +33 -0
- package/dist/api/src/routes/index.js +19 -0
- package/dist/api/src/routes/migrate.js +61 -0
- package/dist/api/src/routes/rules.js +104 -0
- package/dist/api/src/routes/settings.js +178 -0
- package/dist/api/src/routes/sync.js +118 -0
- package/dist/api/src/services/eventLogger.js +41 -0
- package/dist/api/src/services/gmail.js +350 -0
- package/dist/api/src/services/intelligence.js +243 -0
- package/dist/api/src/services/microsoft.js +256 -0
- package/dist/api/src/services/processor.js +503 -0
- package/dist/api/src/services/scheduler.js +210 -0
- package/dist/api/src/services/supabase.js +59 -0
- package/dist/api/src/utils/contentCleaner.js +94 -0
- package/dist/api/src/utils/crypto.js +68 -0
- package/dist/api/src/utils/logger.js +119 -0
- package/package.json +5 -5
- package/src/App.tsx +0 -622
- package/src/components/AccountSettings.tsx +0 -310
- package/src/components/AccountSettingsPage.tsx +0 -390
- package/src/components/Configuration.tsx +0 -1345
- package/src/components/Dashboard.tsx +0 -940
- package/src/components/ErrorBoundary.tsx +0 -71
- package/src/components/LiveTerminal.tsx +0 -308
- package/src/components/LoadingSpinner.tsx +0 -39
- package/src/components/Login.tsx +0 -371
- package/src/components/Logo.tsx +0 -57
- package/src/components/SetupWizard.tsx +0 -388
- package/src/components/Toast.tsx +0 -109
- package/src/components/migration/MigrationBanner.tsx +0 -97
- package/src/components/migration/MigrationModal.tsx +0 -458
- package/src/components/migration/MigrationPulseIndicator.tsx +0 -38
- package/src/components/mode-toggle.tsx +0 -24
- package/src/components/theme-provider.tsx +0 -72
- package/src/components/ui/alert.tsx +0 -66
- package/src/components/ui/button.tsx +0 -57
- package/src/components/ui/card.tsx +0 -75
- package/src/components/ui/dialog.tsx +0 -133
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/otp-input.tsx +0 -184
- package/src/context/AppContext.tsx +0 -422
- package/src/context/MigrationContext.tsx +0 -53
- package/src/context/TerminalContext.tsx +0 -31
- package/src/core/actions.ts +0 -76
- package/src/core/auth.ts +0 -108
- package/src/core/intelligence.ts +0 -76
- package/src/core/processor.ts +0 -112
- package/src/hooks/useRealtimeEmails.ts +0 -111
- package/src/index.css +0 -140
- package/src/lib/api-config.ts +0 -42
- package/src/lib/api-old.ts +0 -228
- package/src/lib/api.ts +0 -421
- package/src/lib/migration-check.ts +0 -264
- package/src/lib/sounds.ts +0 -120
- package/src/lib/supabase-config.ts +0 -117
- package/src/lib/supabase.ts +0 -28
- package/src/lib/types.ts +0 -166
- package/src/lib/utils.ts +0 -6
- package/src/main.tsx +0 -10
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
-
|
|
5
|
-
import { cn } from "../../lib/utils"
|
|
6
|
-
|
|
7
|
-
const buttonVariants = cva(
|
|
8
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0",
|
|
9
|
-
{
|
|
10
|
-
variants: {
|
|
11
|
-
variant: {
|
|
12
|
-
default:
|
|
13
|
-
"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
|
|
14
|
-
destructive:
|
|
15
|
-
"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
|
|
16
|
-
outline:
|
|
17
|
-
"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
|
|
18
|
-
secondary:
|
|
19
|
-
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
20
|
-
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
21
|
-
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
-
},
|
|
23
|
-
size: {
|
|
24
|
-
default: "h-9 px-4 py-2",
|
|
25
|
-
sm: "h-8 rounded-md px-3 text-xs",
|
|
26
|
-
lg: "h-10 rounded-md px-8",
|
|
27
|
-
icon: "h-9 w-9",
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
defaultVariants: {
|
|
31
|
-
variant: "default",
|
|
32
|
-
size: "default",
|
|
33
|
-
},
|
|
34
|
-
}
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
export interface ButtonProps
|
|
38
|
-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
39
|
-
VariantProps<typeof buttonVariants> {
|
|
40
|
-
asChild?: boolean
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
44
|
-
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
45
|
-
const Comp = asChild ? Slot : "button"
|
|
46
|
-
return (
|
|
47
|
-
<Comp
|
|
48
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
49
|
-
ref={ref}
|
|
50
|
-
{...props}
|
|
51
|
-
/>
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
)
|
|
55
|
-
Button.displayName = "Button"
|
|
56
|
-
|
|
57
|
-
export { Button, buttonVariants }
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { cn } from "../../lib/utils"
|
|
3
|
-
|
|
4
|
-
const Card = React.forwardRef<
|
|
5
|
-
HTMLDivElement,
|
|
6
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
7
|
-
>(({ className, ...props }, ref) => (
|
|
8
|
-
<div
|
|
9
|
-
ref={ref}
|
|
10
|
-
className={cn(
|
|
11
|
-
"rounded-xl border bg-card text-card-foreground shadow",
|
|
12
|
-
className
|
|
13
|
-
)}
|
|
14
|
-
{...props}
|
|
15
|
-
/>
|
|
16
|
-
))
|
|
17
|
-
Card.displayName = "Card"
|
|
18
|
-
|
|
19
|
-
const CardHeader = React.forwardRef<
|
|
20
|
-
HTMLDivElement,
|
|
21
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
22
|
-
>(({ className, ...props }, ref) => (
|
|
23
|
-
<div
|
|
24
|
-
ref={ref}
|
|
25
|
-
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
26
|
-
{...props}
|
|
27
|
-
/>
|
|
28
|
-
))
|
|
29
|
-
CardHeader.displayName = "CardHeader"
|
|
30
|
-
|
|
31
|
-
const CardTitle = React.forwardRef<
|
|
32
|
-
HTMLDivElement,
|
|
33
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
34
|
-
>(({ className, ...props }, ref) => (
|
|
35
|
-
<div
|
|
36
|
-
ref={ref}
|
|
37
|
-
className={cn("font-semibold leading-none tracking-tight", className)}
|
|
38
|
-
{...props}
|
|
39
|
-
/>
|
|
40
|
-
))
|
|
41
|
-
CardTitle.displayName = "CardTitle"
|
|
42
|
-
|
|
43
|
-
const CardDescription = React.forwardRef<
|
|
44
|
-
HTMLDivElement,
|
|
45
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
46
|
-
>(({ className, ...props }, ref) => (
|
|
47
|
-
<div
|
|
48
|
-
ref={ref}
|
|
49
|
-
className={cn("text-sm text-muted-foreground", className)}
|
|
50
|
-
{...props}
|
|
51
|
-
/>
|
|
52
|
-
))
|
|
53
|
-
CardDescription.displayName = "CardDescription"
|
|
54
|
-
|
|
55
|
-
const CardContent = React.forwardRef<
|
|
56
|
-
HTMLDivElement,
|
|
57
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
58
|
-
>(({ className, ...props }, ref) => (
|
|
59
|
-
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
60
|
-
))
|
|
61
|
-
CardContent.displayName = "CardContent"
|
|
62
|
-
|
|
63
|
-
const CardFooter = React.forwardRef<
|
|
64
|
-
HTMLDivElement,
|
|
65
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
66
|
-
>(({ className, ...props }, ref) => (
|
|
67
|
-
<div
|
|
68
|
-
ref={ref}
|
|
69
|
-
className={cn("flex items-center p-6 pt-0", className)}
|
|
70
|
-
{...props}
|
|
71
|
-
/>
|
|
72
|
-
))
|
|
73
|
-
CardFooter.displayName = "CardFooter"
|
|
74
|
-
|
|
75
|
-
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
3
|
-
import { XIcon } from "lucide-react"
|
|
4
|
-
|
|
5
|
-
import { cn } from "@/lib/utils"
|
|
6
|
-
|
|
7
|
-
function Dialog({
|
|
8
|
-
...props
|
|
9
|
-
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
10
|
-
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function DialogTrigger({
|
|
14
|
-
...props
|
|
15
|
-
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
16
|
-
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function DialogPortal({
|
|
20
|
-
...props
|
|
21
|
-
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
22
|
-
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function DialogClose({
|
|
26
|
-
...props
|
|
27
|
-
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
28
|
-
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function DialogOverlay({
|
|
32
|
-
className,
|
|
33
|
-
...props
|
|
34
|
-
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
|
35
|
-
return (
|
|
36
|
-
<DialogPrimitive.Overlay
|
|
37
|
-
data-slot="dialog-overlay"
|
|
38
|
-
className={cn(
|
|
39
|
-
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
40
|
-
className
|
|
41
|
-
)}
|
|
42
|
-
{...props}
|
|
43
|
-
/>
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function DialogContent({
|
|
48
|
-
className,
|
|
49
|
-
children,
|
|
50
|
-
...props
|
|
51
|
-
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
|
52
|
-
return (
|
|
53
|
-
<DialogPortal data-slot="dialog-portal">
|
|
54
|
-
<DialogOverlay />
|
|
55
|
-
<DialogPrimitive.Content
|
|
56
|
-
data-slot="dialog-content"
|
|
57
|
-
className={cn(
|
|
58
|
-
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
|
59
|
-
className
|
|
60
|
-
)}
|
|
61
|
-
{...props}
|
|
62
|
-
>
|
|
63
|
-
{children}
|
|
64
|
-
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
|
65
|
-
<XIcon />
|
|
66
|
-
<span className="sr-only">Close</span>
|
|
67
|
-
</DialogPrimitive.Close>
|
|
68
|
-
</DialogPrimitive.Content>
|
|
69
|
-
</DialogPortal>
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
74
|
-
return (
|
|
75
|
-
<div
|
|
76
|
-
data-slot="dialog-header"
|
|
77
|
-
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
|
78
|
-
{...props}
|
|
79
|
-
/>
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
84
|
-
return (
|
|
85
|
-
<div
|
|
86
|
-
data-slot="dialog-footer"
|
|
87
|
-
className={cn(
|
|
88
|
-
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
89
|
-
className
|
|
90
|
-
)}
|
|
91
|
-
{...props}
|
|
92
|
-
/>
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function DialogTitle({
|
|
97
|
-
className,
|
|
98
|
-
...props
|
|
99
|
-
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
100
|
-
return (
|
|
101
|
-
<DialogPrimitive.Title
|
|
102
|
-
data-slot="dialog-title"
|
|
103
|
-
className={cn("text-lg leading-none font-semibold", className)}
|
|
104
|
-
{...props}
|
|
105
|
-
/>
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function DialogDescription({
|
|
110
|
-
className,
|
|
111
|
-
...props
|
|
112
|
-
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
113
|
-
return (
|
|
114
|
-
<DialogPrimitive.Description
|
|
115
|
-
data-slot="dialog-description"
|
|
116
|
-
className={cn("text-muted-foreground text-sm", className)}
|
|
117
|
-
{...props}
|
|
118
|
-
/>
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export {
|
|
123
|
-
Dialog,
|
|
124
|
-
DialogClose,
|
|
125
|
-
DialogContent,
|
|
126
|
-
DialogDescription,
|
|
127
|
-
DialogFooter,
|
|
128
|
-
DialogHeader,
|
|
129
|
-
DialogOverlay,
|
|
130
|
-
DialogPortal,
|
|
131
|
-
DialogTitle,
|
|
132
|
-
DialogTrigger,
|
|
133
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
|
|
3
|
-
import { cn } from "../../lib/utils"
|
|
4
|
-
|
|
5
|
-
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
|
6
|
-
({ className, type, ...props }, ref) => {
|
|
7
|
-
return (
|
|
8
|
-
<input
|
|
9
|
-
type={type}
|
|
10
|
-
className={cn(
|
|
11
|
-
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
12
|
-
className
|
|
13
|
-
)}
|
|
14
|
-
ref={ref}
|
|
15
|
-
{...props}
|
|
16
|
-
/>
|
|
17
|
-
)
|
|
18
|
-
}
|
|
19
|
-
)
|
|
20
|
-
Input.displayName = "Input"
|
|
21
|
-
|
|
22
|
-
export { Input }
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
5
|
-
|
|
6
|
-
import { cn } from "@/lib/utils"
|
|
7
|
-
|
|
8
|
-
function Label({
|
|
9
|
-
className,
|
|
10
|
-
...props
|
|
11
|
-
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
12
|
-
return (
|
|
13
|
-
<LabelPrimitive.Root
|
|
14
|
-
data-slot="label"
|
|
15
|
-
className={cn(
|
|
16
|
-
"flex items-center gap-2 text-sm leading-none font-medium select-none text-muted-foreground group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
17
|
-
className
|
|
18
|
-
)}
|
|
19
|
-
{...props}
|
|
20
|
-
/>
|
|
21
|
-
)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export { Label }
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { useRef, useState, KeyboardEvent, ClipboardEvent } from "react";
|
|
2
|
-
import { Input } from "./input";
|
|
3
|
-
import { cn } from "../../lib/utils";
|
|
4
|
-
|
|
5
|
-
interface OtpInputProps {
|
|
6
|
-
length?: number;
|
|
7
|
-
value: string;
|
|
8
|
-
onChange: (value: string) => void;
|
|
9
|
-
onComplete?: (value: string) => void;
|
|
10
|
-
disabled?: boolean;
|
|
11
|
-
error?: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function OtpInput({
|
|
15
|
-
length = 6,
|
|
16
|
-
value,
|
|
17
|
-
onChange,
|
|
18
|
-
onComplete,
|
|
19
|
-
disabled = false,
|
|
20
|
-
error = false,
|
|
21
|
-
}: OtpInputProps) {
|
|
22
|
-
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
|
23
|
-
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
|
24
|
-
|
|
25
|
-
const handleChange = (index: number, inputValue: string) => {
|
|
26
|
-
// Only allow digits
|
|
27
|
-
const digit = inputValue.replace(/[^0-9]/g, "");
|
|
28
|
-
|
|
29
|
-
if (digit.length === 0) {
|
|
30
|
-
// Handle backspace/delete
|
|
31
|
-
const newValue = value.split("");
|
|
32
|
-
newValue[index] = " "; // Use space as placeholder to maintain length if needed, or better logic below
|
|
33
|
-
// Actually, split("") of string length N gives N chars.
|
|
34
|
-
// If we want to clear index, we should rebuild string carefully.
|
|
35
|
-
|
|
36
|
-
// Better approach for fixed length string representation:
|
|
37
|
-
// We can't easily mutate string directly.
|
|
38
|
-
// Let's assume value is a string of digits, potentially shorter than length?
|
|
39
|
-
// No, usually value is just the current OTP string.
|
|
40
|
-
|
|
41
|
-
const newChars = value.split("");
|
|
42
|
-
// Pad if needed? No, value might be "12" for length 6.
|
|
43
|
-
// But we map over length.
|
|
44
|
-
|
|
45
|
-
// Let's follow the atomic-crm logic logic carefully or improve it.
|
|
46
|
-
// atomic-crm logic:
|
|
47
|
-
// newValue[index] = "";
|
|
48
|
-
// updatedValue = newValue.join("");
|
|
49
|
-
// This reduces length if value was "123" and we clear index 1 -> "13".
|
|
50
|
-
// This shifts subsequent digits left, which is standard backspace behavior for text input,
|
|
51
|
-
// but typical OTP inputs usually clear the digit IN PLACE.
|
|
52
|
-
// However, seeing atomic-crm implementation:
|
|
53
|
-
/*
|
|
54
|
-
const newValue = value.split("");
|
|
55
|
-
newValue[index] = "";
|
|
56
|
-
const updatedValue = newValue.join("");
|
|
57
|
-
*/
|
|
58
|
-
// This effectively DELETES the char at index.
|
|
59
|
-
|
|
60
|
-
// Let's stick to the ported code exactly to match expectations.
|
|
61
|
-
// However, I need to make sure 'value' passed in handles getting shorter.
|
|
62
|
-
|
|
63
|
-
let valArray = value.split('');
|
|
64
|
-
if (index < valArray.length) {
|
|
65
|
-
valArray.splice(index, 1); // Remove char at index
|
|
66
|
-
}
|
|
67
|
-
const updatedValue = valArray.join('');
|
|
68
|
-
|
|
69
|
-
onChange(updatedValue);
|
|
70
|
-
|
|
71
|
-
// Move to previous input
|
|
72
|
-
if (index > 0) {
|
|
73
|
-
inputRefs.current[index - 1]?.focus();
|
|
74
|
-
}
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Update the value at the current index
|
|
79
|
-
// If we are typing in an empty slot (index >= value.length), append?
|
|
80
|
-
// If we are typing in existing slot, replace?
|
|
81
|
-
|
|
82
|
-
// atomic-crm logic:
|
|
83
|
-
/*
|
|
84
|
-
const newValue = value.split("");
|
|
85
|
-
newValue[index] = digit[0];
|
|
86
|
-
const updatedValue = newValue.join("");
|
|
87
|
-
*/
|
|
88
|
-
// This implies value has length equal to inputs? Or at least up to index?
|
|
89
|
-
// If value is "1", and I type in box 2 (index 1), newValue[1] = digit.
|
|
90
|
-
// "1" split is ["1"]. newValue[1] = "2" -> ["1", "2"]. Join -> "12". Works.
|
|
91
|
-
|
|
92
|
-
// But if I click box 3 with value "1", index is 2. newValue[2] = "3". -> ["1", undefined, "3"].
|
|
93
|
-
// Join might be "13" or "1undefined3".
|
|
94
|
-
|
|
95
|
-
// Let's write robust logic.
|
|
96
|
-
const chars = value.split('');
|
|
97
|
-
// Fill gaps if jumping ahead? Usually OTP fields auto-focus next.
|
|
98
|
-
// But if user clicks manualy...
|
|
99
|
-
// Let's just assume we append if index >= length
|
|
100
|
-
|
|
101
|
-
// Actually, sticking to exact atomic-crm port is safest if we trust it works there.
|
|
102
|
-
// The provided code was:
|
|
103
|
-
/*
|
|
104
|
-
const newValue = value.split("");
|
|
105
|
-
newValue[index] = digit[0];
|
|
106
|
-
const updatedValue = newValue.join("");
|
|
107
|
-
*/
|
|
108
|
-
|
|
109
|
-
// I will use a slightly more robust version that ensures we don't get holes if possible,
|
|
110
|
-
// or just trust the array manipulation.
|
|
111
|
-
|
|
112
|
-
const newChars = [...value]; // split
|
|
113
|
-
newChars[index] = digit[0];
|
|
114
|
-
onChange(newChars.join(""));
|
|
115
|
-
|
|
116
|
-
// Move to next input if not the last one
|
|
117
|
-
if (index < length - 1) {
|
|
118
|
-
inputRefs.current[index + 1]?.focus();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Check if OTP is complete
|
|
122
|
-
const resultingStr = newChars.join("");
|
|
123
|
-
if (resultingStr.length === length && onComplete) {
|
|
124
|
-
onComplete(resultingStr);
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const handleKeyDown = (index: number, e: KeyboardEvent<HTMLInputElement>) => {
|
|
129
|
-
if (e.key === "Backspace" && !value[index] && index > 0) {
|
|
130
|
-
// If current input is empty and backspace is pressed, move to previous
|
|
131
|
-
inputRefs.current[index - 1]?.focus();
|
|
132
|
-
} else if (e.key === "ArrowLeft" && index > 0) {
|
|
133
|
-
inputRefs.current[index - 1]?.focus();
|
|
134
|
-
} else if (e.key === "ArrowRight" && index < length - 1) {
|
|
135
|
-
inputRefs.current[index + 1]?.focus();
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
|
|
140
|
-
e.preventDefault();
|
|
141
|
-
const pastedData = e.clipboardData.getData("text/plain");
|
|
142
|
-
const digits = pastedData.replace(/[^0-9]/g, "").slice(0, length);
|
|
143
|
-
|
|
144
|
-
onChange(digits);
|
|
145
|
-
|
|
146
|
-
// Focus the next empty input or the last input
|
|
147
|
-
const nextIndex = Math.min(digits.length, length - 1);
|
|
148
|
-
inputRefs.current[nextIndex]?.focus();
|
|
149
|
-
|
|
150
|
-
// Check if OTP is complete
|
|
151
|
-
if (digits.length === length && onComplete) {
|
|
152
|
-
onComplete(digits);
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
<div className="flex gap-2 justify-center">
|
|
158
|
-
{Array.from({ length }).map((_, index) => (
|
|
159
|
-
<Input
|
|
160
|
-
key={index}
|
|
161
|
-
ref={(el) => {
|
|
162
|
-
inputRefs.current[index] = el;
|
|
163
|
-
}}
|
|
164
|
-
type="text"
|
|
165
|
-
inputMode="numeric"
|
|
166
|
-
maxLength={1}
|
|
167
|
-
value={value[index] || ""}
|
|
168
|
-
onChange={(e) => handleChange(index, e.target.value)}
|
|
169
|
-
onKeyDown={(e) => handleKeyDown(index, e)}
|
|
170
|
-
onPaste={handlePaste}
|
|
171
|
-
onFocus={() => setFocusedIndex(index)}
|
|
172
|
-
onBlur={() => setFocusedIndex(null)}
|
|
173
|
-
disabled={disabled}
|
|
174
|
-
className={cn(
|
|
175
|
-
"w-10 h-10 sm:w-12 sm:h-12 text-center text-lg font-semibold px-0",
|
|
176
|
-
error && "border-destructive focus-visible:ring-destructive",
|
|
177
|
-
focusedIndex === index && "ring-2 ring-ring",
|
|
178
|
-
)}
|
|
179
|
-
aria-label={`Digit ${index + 1}`}
|
|
180
|
-
/>
|
|
181
|
-
))}
|
|
182
|
-
</div>
|
|
183
|
-
);
|
|
184
|
-
}
|