satnaing 2 лет назад
Родитель
Сommit
91b3bb1985
4 измененных файлов с 149 добавлено и 0 удалено
  1. 7 0
      src/data/sidelinks.tsx
  2. 85 0
      src/pages/auth/components/otp-form.tsx
  3. 51 0
      src/pages/auth/otp.tsx
  4. 6 0
      src/router.tsx

+ 7 - 0
src/data/sidelinks.tsx

@@ -10,6 +10,7 @@ import {
   IconHexagonNumber2,
   IconHexagonNumber3,
   IconHexagonNumber4,
+  IconHexagonNumber5,
   IconLayoutDashboard,
   IconMessages,
   IconRouteAltLeft,
@@ -80,6 +81,12 @@ export const sidelinks: SideLink[] = [
         href: '/forgot-password',
         icon: <IconHexagonNumber4 size={18} />,
       },
+      {
+        title: 'OTP',
+        label: '',
+        href: '/otp',
+        icon: <IconHexagonNumber5 size={18} />,
+      },
     ],
   },
   {

+ 85 - 0
src/pages/auth/components/otp-form.tsx

@@ -0,0 +1,85 @@
+import { HTMLAttributes, useState } from 'react'
+import { cn } from '@/lib/utils'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { useForm } from 'react-hook-form'
+import { z } from 'zod'
+import { Button } from '@/components/custom/button'
+import {
+  Form,
+  FormControl,
+  FormField,
+  FormItem,
+  FormMessage,
+} from '@/components/ui/form'
+import { Input } from '@/components/ui/input'
+import { PinInput, PinInputField } from '@/components/custom/pin-input'
+import { Separator } from '@/components/ui/separator'
+
+interface OtpFormProps extends HTMLAttributes<HTMLDivElement> {}
+
+const formSchema = z.object({
+  otp: z.string().min(1, { message: 'Please enter your otp code.' }),
+})
+
+export function OtpForm({ className, ...props }: OtpFormProps) {
+  const [isLoading, setIsLoading] = useState(false)
+  const [disabledBtn, setDisabledBtn] = useState(true)
+
+  const form = useForm<z.infer<typeof formSchema>>({
+    resolver: zodResolver(formSchema),
+    defaultValues: { otp: '' },
+  })
+
+  function onSubmit(data: z.infer<typeof formSchema>) {
+    setIsLoading(true)
+    console.log({ data })
+
+    setTimeout(() => {
+      form.reset()
+      setIsLoading(false)
+    }, 2000)
+  }
+
+  return (
+    <div className={cn('grid gap-6', className)} {...props}>
+      <Form {...form}>
+        <form onSubmit={form.handleSubmit(onSubmit)}>
+          <div className='grid gap-2'>
+            <FormField
+              control={form.control}
+              name='otp'
+              render={({ field }) => (
+                <FormItem className='space-y-1'>
+                  <FormControl>
+                    <PinInput
+                      {...field}
+                      className='flex h-10 justify-between'
+                      onComplete={() => setDisabledBtn(false)}
+                      onIncomplete={() => setDisabledBtn(true)}
+                    >
+                      {Array.from({ length: 7 }, (_, i) => {
+                        if (i === 3)
+                          return <Separator key={i} orientation='vertical' />
+                        return (
+                          <PinInputField
+                            key={i}
+                            component={Input}
+                            className={`${form.getFieldState('otp').invalid ? 'border-red-500' : ''}`}
+                          />
+                        )
+                      })}
+                    </PinInput>
+                  </FormControl>
+                  <FormMessage />
+                </FormItem>
+              )}
+            />
+            <Button className='mt-2' disabled={disabledBtn} loading={isLoading}>
+              Verify
+            </Button>
+          </div>
+        </form>
+      </Form>
+    </div>
+  )
+}

+ 51 - 0
src/pages/auth/otp.tsx

@@ -0,0 +1,51 @@
+import { Card } from '@/components/ui/card'
+import { Link } from 'react-router-dom'
+import { OtpForm } from './components/otp-form'
+
+export default function Otp() {
+  return (
+    <>
+      <div className='container grid h-svh flex-col items-center justify-center bg-primary-foreground lg:max-w-none lg:px-0'>
+        <div className='mx-auto flex w-full flex-col justify-center space-y-2 sm:w-[480px] lg:p-8'>
+          <div className='mb-4 flex items-center justify-center'>
+            <svg
+              xmlns='http://www.w3.org/2000/svg'
+              viewBox='0 0 24 24'
+              fill='none'
+              stroke='currentColor'
+              strokeWidth='2'
+              strokeLinecap='round'
+              strokeLinejoin='round'
+              className='mr-2 h-6 w-6'
+            >
+              <path d='M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3' />
+            </svg>
+            <h1 className='text-xl font-medium'>Shadcn Admin</h1>
+          </div>
+          <Card className='p-6'>
+            <div className='mb-2 flex flex-col space-y-2 text-left'>
+              <h1 className='text-md font-semibold tracking-tight'>
+                Two-factor Authentication
+              </h1>
+              <p className='text-sm text-muted-foreground'>
+                Please enter the authentication code. <br /> We have sent the
+                authentication code to your email.
+              </p>
+            </div>
+            <OtpForm />
+            <p className='mt-4 px-8 text-center text-sm text-muted-foreground'>
+              Haven't received it?{' '}
+              <Link
+                to='/resent-new-code'
+                className='underline underline-offset-4 hover:text-primary'
+              >
+                Resend a new code.
+              </Link>
+              .
+            </p>
+          </Card>
+        </div>
+      </div>
+    </>
+  )
+}

+ 6 - 0
src/router.tsx

@@ -29,6 +29,12 @@ const router = createBrowserRouter([
       Component: (await import('./pages/auth/forgot-password')).default,
     }),
   },
+  {
+    path: '/otp',
+    lazy: async () => ({
+      Component: (await import('./pages/auth/otp')).default,
+    }),
+  },
 
   // Main routes
   {