@authdog/react-elements 0.0.28 → 0.0.30

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.
@@ -0,0 +1,231 @@
1
+ "use client"
2
+
3
+ import type React from "react"
4
+
5
+ import { useState, useRef } from "react"
6
+ import { Button } from "../../components/ui/button"
7
+ import { Card, CardContent } from "../../components/ui/card"
8
+ import { Alert, AlertDescription } from "../../components/ui/alert"
9
+ import { Shield, CheckCircle, AlertCircle } from "lucide-react"
10
+
11
+ interface TOTPValidatorProps {
12
+ onValidate: (code: string) => Promise<void>;
13
+ }
14
+
15
+ export const TOTPValidator = (
16
+ {
17
+ onValidate
18
+ }: TOTPValidatorProps
19
+ ) => {
20
+ const [code, setCode] = useState(["", "", "", "", "", ""])
21
+ const [loading, setLoading] = useState(false)
22
+ const [error, setError] = useState("")
23
+ const [success, setSuccess] = useState(false)
24
+ const inputRefs = useRef<(HTMLInputElement | null)[]>([])
25
+
26
+ const handleInputChange = (index: number, value: string) => {
27
+ // Only allow digits
28
+ if (!/^\d*$/.test(value)) return
29
+
30
+ const newCode = [...code]
31
+ newCode[index] = value.slice(-1) // Only take the last character
32
+
33
+ setCode(newCode)
34
+ setError("")
35
+ setSuccess(false)
36
+
37
+ // Auto-focus next input
38
+ if (value && index < 5) {
39
+ inputRefs.current[index + 1]?.focus()
40
+ }
41
+ }
42
+
43
+ const handleKeyDown = (index: number, e: React.KeyboardEvent) => {
44
+ // Handle backspace
45
+ if (e.key === "Backspace" && !code[index] && index > 0) {
46
+ inputRefs.current[index - 1]?.focus()
47
+ }
48
+
49
+ // Handle paste
50
+ if (e.key === "v" && (e.ctrlKey || e.metaKey)) {
51
+ e.preventDefault()
52
+ navigator.clipboard.readText().then((text) => {
53
+ const digits = text.replace(/\D/g, "").slice(0, 6).split("")
54
+ const newCode = [...code]
55
+ digits.forEach((digit, i) => {
56
+ if (i < 6) newCode[i] = digit
57
+ })
58
+ setCode(newCode)
59
+
60
+ // Focus the next empty input or the last one
61
+ const nextIndex = Math.min(digits.length, 5)
62
+ inputRefs.current[nextIndex]?.focus()
63
+ })
64
+ }
65
+ }
66
+
67
+ const validateTOTP = async () => {
68
+ const totpCode = code.join("")
69
+
70
+ if (totpCode.length !== 6) {
71
+ setError("Please enter all 6 digits")
72
+ return
73
+ }
74
+
75
+ setLoading(true)
76
+ setError("")
77
+
78
+ try {
79
+ await onValidate(totpCode)
80
+ setSuccess(true)
81
+ } catch (error) {
82
+ setError("Invalid TOTP code. Please try again.")
83
+ } finally {
84
+ setLoading(false)
85
+ }
86
+
87
+
88
+
89
+ // try {
90
+ // Call your TOTP validation endpoint
91
+ // const response = await fetch("/api/validate-totp", {
92
+ // method: "POST",
93
+ // headers: {
94
+ // "Content-Type": "application/json",
95
+ // },
96
+ // body: JSON.stringify({ code: totpCode }),
97
+ // })
98
+
99
+ // // Check if response is JSON
100
+ // const contentType = response.headers.get("content-type")
101
+ // if (!contentType || !contentType.includes("application/json")) {
102
+ // throw new Error("Server returned non-JSON response")
103
+ // }
104
+
105
+ // const result = await response.json()
106
+
107
+ // if (response.ok && result.valid) {
108
+ // setSuccess(true)
109
+ // setError("")
110
+ // } else {
111
+ // setError(result.message || "Invalid TOTP code. Please try again.")
112
+ // // Clear the code on error
113
+ // setCode(["", "", "", "", "", ""])
114
+ // inputRefs.current[0]?.focus()
115
+ // }
116
+ // } catch (err) {
117
+ // console.error("TOTP validation error:", err)
118
+ // if (err instanceof Error && err.message.includes("non-JSON")) {
119
+ // setError("Server error. Please try again later.")
120
+ // } else {
121
+ // setError("Network error. Please try again.")
122
+ // }
123
+ // // Clear the code on error
124
+ // setCode(["", "", "", "", "", ""])
125
+ // inputRefs.current[0]?.focus()
126
+ // } finally {
127
+ // setLoading(false)
128
+ // }
129
+ }
130
+
131
+ const handleSubmit = (e: React.FormEvent) => {
132
+ e.preventDefault()
133
+ validateTOTP()
134
+ }
135
+
136
+ const clearCode = () => {
137
+ setCode(["", "", "", "", "", ""])
138
+ setError("")
139
+ setSuccess(false)
140
+ setLoading(false)
141
+ inputRefs.current[0]?.focus()
142
+ }
143
+
144
+ if (success) {
145
+ return (
146
+ <div className="max-w-md mx-auto">
147
+ <Card className="border-green-200 bg-green-50">
148
+ <CardContent className="pt-6">
149
+ <div className="text-center space-y-4">
150
+ <div className="mx-auto w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
151
+ <CheckCircle className="w-8 h-8 text-green-600" />
152
+ </div>
153
+ <div>
154
+ <h3 className="text-lg font-semibold text-green-900">Verification Successful!</h3>
155
+ <p className="text-sm text-green-700 mt-1">Your TOTP code has been validated.</p>
156
+ </div>
157
+ <Button onClick={clearCode} variant="outline" className="bg-white">
158
+ Verify Another Code
159
+ </Button>
160
+ </div>
161
+ </CardContent>
162
+ </Card>
163
+ </div>
164
+ )
165
+ }
166
+
167
+ return (
168
+ <div className="max-w-md mx-auto">
169
+ <Card>
170
+ <CardContent className="pt-6">
171
+ <div className="text-center space-y-6">
172
+ <div>
173
+ <div className="mx-auto w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mb-4">
174
+ <Shield className="w-6 h-6 text-blue-600" />
175
+ </div>
176
+ <h3 className="text-lg font-semibold">Enter Verification Code</h3>
177
+ <p className="text-sm text-muted-foreground mt-1">Enter the 6-digit code from your authenticator app</p>
178
+ </div>
179
+
180
+ <form onSubmit={handleSubmit} className="space-y-6">
181
+ <div className="flex justify-center gap-2">
182
+ {code.map((digit, index) => (
183
+ <Card key={index} className="w-12 h-14 border-2 focus-within:border-blue-500 transition-colors">
184
+ <CardContent className="p-0 h-full flex items-center justify-center">
185
+ <input
186
+ ref={(el) => {
187
+ inputRefs.current[index] = el
188
+ }}
189
+ type="tel"
190
+ inputMode="numeric"
191
+ maxLength={1}
192
+ value={digit}
193
+ onChange={(e) => handleInputChange(index, e.target.value)}
194
+ onKeyDown={(e) => handleKeyDown(index, e)}
195
+ className="w-full h-full text-center text-2xl font-bold border-none outline-none bg-transparent"
196
+ autoComplete="one-time-code"
197
+ disabled={loading}
198
+ style={{
199
+ height: 'auto'
200
+ }}
201
+ />
202
+ </CardContent>
203
+ </Card>
204
+ ))}
205
+ </div>
206
+
207
+ {error && (
208
+ <Alert variant="destructive">
209
+ <AlertCircle className="h-4 w-4" />
210
+ <AlertDescription>{error}</AlertDescription>
211
+ </Alert>
212
+ )}
213
+
214
+ <div className="space-y-3">
215
+ <Button type="submit" className="w-full" disabled={loading || code.some((digit) => digit === "")}>
216
+ {loading ? "Verifying..." : "Verify Code"}
217
+ </Button>
218
+
219
+ <Button type="button" variant="ghost" size="sm" onClick={clearCode} className="w-full text-xs">
220
+ Clear Code
221
+ </Button>
222
+ </div>
223
+ </form>
224
+
225
+ <p className="text-xs text-muted-foreground">Codes refresh every 30 seconds</p>
226
+ </div>
227
+ </CardContent>
228
+ </Card>
229
+ </div>
230
+ )
231
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export {Button} from "./components/ui/button";
2
+ export { ClientOnly } from "./components/core/client-only";
2
3
  export {Navbar} from "./components/core/navbar";
3
4
  export {UserProfile} from "./components/core/user-profile";
4
- export {PlaceholderAlert} from "./components/core/placeholder-alert";
5
+ export {PlaceholderAlert} from "./components/core/placeholder-alert";
6
+ export {TOTPValidator} from "./components/flow/totp-validator";
@@ -0,0 +1,14 @@
1
+ import type { Story } from '@ladle/react';
2
+ import { TOTPValidator } from "../components/flow/totp-validator"
3
+ import "../global.css"
4
+
5
+ export const Default: Story = () => <TOTPValidator
6
+ onValidate={async (code) => {
7
+ console.log(code)
8
+ }}
9
+ />;
10
+ Default.storyName = 'Default TOTP Validator';
11
+
12
+ // export const defaultTOTPValidator: Story = () => (
13
+ // <TOTPValidator />
14
+ // );