From 0b82fa915e878ddcb6de32b053a0ec8373508bd1 Mon Sep 17 00:00:00 2001 From: Vivian Phung Date: Fri, 21 Jun 2024 20:37:03 -0400 Subject: [PATCH] feat: 5 digit code ui --- packages/backend/src/routes/auth.ts | 5 +- .../VerifyCodeInput/VerifyCodeInput.tsx | 117 ++++++++++++++++++ .../shared/VerifyCodeInput/index.ts | 1 + .../frontend/src/pages/auth/AccessCode.tsx | 27 ++-- 4 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 packages/frontend/src/components/shared/VerifyCodeInput/VerifyCodeInput.tsx create mode 100644 packages/frontend/src/components/shared/VerifyCodeInput/index.ts diff --git a/packages/backend/src/routes/auth.ts b/packages/backend/src/routes/auth.ts index 90111485..c57d1e01 100644 --- a/packages/backend/src/routes/auth.ts +++ b/packages/backend/src/routes/auth.ts @@ -5,10 +5,13 @@ import { authenticateUser, createUser } from '../turnkey-backend'; const router = Router(); +// +// Access Code +// router.post('/accesscode', async (req, res) => { console.log('Access Code', req.body); const { accesscode } = req.body; - if (accesscode === '444444') { + if (accesscode === '44444') { return res.send({ isValid: true }); } else { return res.sendStatus(204); diff --git a/packages/frontend/src/components/shared/VerifyCodeInput/VerifyCodeInput.tsx b/packages/frontend/src/components/shared/VerifyCodeInput/VerifyCodeInput.tsx new file mode 100644 index 00000000..9428bbcc --- /dev/null +++ b/packages/frontend/src/components/shared/VerifyCodeInput/VerifyCodeInput.tsx @@ -0,0 +1,117 @@ +import { useEffect, useRef } from 'react'; +import { Input } from '../Input'; + +export interface VerifyCodeInputProps { + code: string; + setCode: (code: string) => void; + submitCode: () => void; + loading: boolean; +} + +export const VerifyCodeInput = ({ + code, + setCode, + submitCode, + loading, +}: VerifyCodeInputProps) => { + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + const handlePaste = ( + e: React.ClipboardEvent, + i: number, + ) => { + e.preventDefault(); + const pasteData = e.clipboardData.getData('text').replace(/\D/g, ''); // Only digits + if (pasteData.length > 0) { + let newCodeArray = code.split(''); + for (let j = 0; j < pasteData.length && i + j < 6; j++) { + newCodeArray[i + j] = pasteData[j]; + } + const newCode = newCodeArray.join(''); + setCode(newCode); + const nextIndex = Math.min(i + pasteData.length, 5); + const nextInput = inputRefs.current[nextIndex]; + if (nextInput) nextInput.focus(); + if (!newCode.includes(' ')) { + submitCode(); + } + } + }; + + const handleKeyDown = ( + e: React.KeyboardEvent, + i: number, + ) => { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; + + if (e.key === 'Backspace') { + e.preventDefault(); + const isEmpty = code[i] === ' '; + const newCode = !isEmpty + ? code.slice(0, i) + ' ' + code.slice(i + 1, 6) + : code.slice(0, i - 1) + ' ' + code.slice(i, 6); + + setCode(newCode.slice(0, 6)); + + if (i === 0 || !isEmpty) return; + const prev = inputRefs.current[i - 1]; + if (prev) prev.focus(); + return; + } + + if (!e.key.match(/[0-9]/)) return; + + e.preventDefault(); // Prevent the default event to avoid duplicate input + + const newCode = code.slice(0, i) + e.key + code.slice(i + 1, 6); + setCode(newCode); + + if (i === 5) { + submitCode(); + return; + } + + const next = inputRefs.current[i + 1]; + if (next) next.focus(); + }; + + const handleChange = (e: React.ChangeEvent, i: number) => { + const value = e.target.value.slice(-1); + if (!value.match(/[0-9]/)) return; + + const newCode = code.slice(0, i) + value + code.slice(i + 1, 6); + setCode(newCode); + + if (i < 5) { + const next = inputRefs.current[i + 1]; + if (next) next.focus(); + } + + if (!newCode.includes(' ')) { + submitCode(); + } + }; + + useEffect(() => { + if (inputRefs.current[0]) { + inputRefs.current[0].focus(); + } + }, []); + + return ( +
+ {code.split('').map((char, i) => ( + (inputRefs.current[i] = el)} + onChange={(e) => handleChange(e, i)} + onPaste={(e) => handlePaste(e, i)} + onKeyDown={(e) => handleKeyDown(e, i)} + disabled={!!loading} + style={{ textAlign: 'center' }} // Add this line to center text + /> + ))} +
+ ); +}; diff --git a/packages/frontend/src/components/shared/VerifyCodeInput/index.ts b/packages/frontend/src/components/shared/VerifyCodeInput/index.ts new file mode 100644 index 00000000..a00f35d9 --- /dev/null +++ b/packages/frontend/src/components/shared/VerifyCodeInput/index.ts @@ -0,0 +1 @@ +export { VerifyCodeInput } from './VerifyCodeInput'; diff --git a/packages/frontend/src/pages/auth/AccessCode.tsx b/packages/frontend/src/pages/auth/AccessCode.tsx index e35bd893..327647e8 100644 --- a/packages/frontend/src/pages/auth/AccessCode.tsx +++ b/packages/frontend/src/pages/auth/AccessCode.tsx @@ -6,7 +6,7 @@ import { LoaderIcon, } from 'components/shared/CustomIcon'; import { WavyBorder } from 'components/shared/WavyBorder'; -import { Input } from 'components/shared/Input'; +import { VerifyCodeInput } from 'components/shared/VerifyCodeInput'; import { verifyAccessCode } from 'utils/accessCode'; type AccessMethod = 'accesscode' | 'passkey'; @@ -20,7 +20,7 @@ type AccessCodeProps = { export const AccessCode: React.FC = ({ onCorrectAccessCode, }) => { - const [accessCode, setAccessCode] = useState(''); + const [accessCode, setAccessCode] = useState(' '); const [error, setError] = useState(); const [accessMethod, setAccessMethod] = useState(false); @@ -28,6 +28,9 @@ export const AccessCode: React.FC = ({ setAccessMethod('accesscode'); try { const isValidAccessCode = await verifyAccessCode(accessCode); + + // add a pause for ux + await new Promise((resolve) => setTimeout(resolve, 250)); if (isValidAccessCode) { localStorage.setItem('accessCode', accessCode); onCorrectAccessCode(); @@ -43,7 +46,7 @@ export const AccessCode: React.FC = ({ } const loading = accessMethod; - const isValidAccessCodeLength = accessCode.length === 6; + const isValidAccessCodeLength = accessCode.trim().length === 5; return (
@@ -56,10 +59,11 @@ export const AccessCode: React.FC = ({
- setAccessCode(e.target.value)} - disabled={!!loading} +