diff --git a/ee/tabby-ui/app/auth/components/signup.tsx b/ee/tabby-ui/app/auth/components/signup.tsx
index 0528fd7..f43bbc2 100644
--- a/ee/tabby-ui/app/auth/components/signup.tsx
+++ b/ee/tabby-ui/app/auth/components/signup.tsx
@@ -15,7 +15,7 @@ export default function Signup() {
{title}
- Enter your credentials below to create account
+ Fill form below to create your account
diff --git a/ee/tabby-ui/app/auth/components/user-auth-form.tsx b/ee/tabby-ui/app/auth/components/user-auth-form.tsx
index 3dd1b73..a5fb309 100644
--- a/ee/tabby-ui/app/auth/components/user-auth-form.tsx
+++ b/ee/tabby-ui/app/auth/components/user-auth-form.tsx
@@ -2,76 +2,99 @@
import * as React from "react"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import * as z from "zod"
+
import { cn } from "@/lib/utils"
import { IconSpinner } from "@/components/ui/icons"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
+
+const formSchema = z.object({
+ email: z.string().email("Invalid email address"),
+ password1: z.string(),
+ password2: z.string(),
+ invitationCode: z.string().optional(),
+}).refine((data) => data.password1 === data.password2, {
+ message: "Passwords don't match",
+ path: ["password2"],
+});
interface UserAuthFormProps extends React.HTMLAttributes {
invitationCode?: string
}
export function UserAuthForm({ className, invitationCode, ...props }: UserAuthFormProps) {
- const [isLoading, setIsLoading] = React.useState(false)
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ email: "",
+ password1: "",
+ password2: "",
+ invitationCode,
+ }
+ })
- async function onSubmit(event: React.SyntheticEvent) {
- event.preventDefault()
- setIsLoading(true)
+ const { isSubmitSuccessful, isSubmitting } = form.formState;
- setTimeout(() => {
- setIsLoading(false)
- }, 3000)
+ async function onSubmit(values: z.infer) {
+ await new Promise(resolve => setTimeout(resolve, 500000))
}
return (
)
}
\ No newline at end of file
diff --git a/ee/tabby-ui/components/ui/form.tsx b/ee/tabby-ui/components/ui/form.tsx
new file mode 100644
index 0000000..4603f8b
--- /dev/null
+++ b/ee/tabby-ui/components/ui/form.tsx
@@ -0,0 +1,176 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState, formState } = useFormContext()
+
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+)
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId()
+
+ return (
+
+
+
+ )
+})
+FormItem.displayName = "FormItem"
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField()
+
+ return (
+
+ )
+})
+FormLabel.displayName = "FormLabel"
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+})
+FormControl.displayName = "FormControl"
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+})
+FormDescription.displayName = "FormDescription"
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message) : children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+})
+FormMessage.displayName = "FormMessage"
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/ee/tabby-ui/package.json b/ee/tabby-ui/package.json
index 53cad01..f86d1d6 100644
--- a/ee/tabby-ui/package.json
+++ b/ee/tabby-ui/package.json
@@ -19,6 +19,7 @@
"codegen:watch": "graphql-codegen --config codegen.ts --watch"
},
"dependencies": {
+ "@hookform/resolvers": "^3.3.2",
"@radix-ui/react-alert-dialog": "1.0.4",
"@radix-ui/react-dialog": "1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
@@ -48,6 +49,7 @@
"openai-edge": "^0.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-hook-form": "^7.48.2",
"react-hot-toast": "^2.4.1",
"react-intersection-observer": "^9.4.4",
"react-markdown": "^8.0.7",
@@ -56,6 +58,7 @@
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"swr": "^2.2.4",
+ "zod": "^3.22.4",
"zustand": "^4.4.6"
},
"devDependencies": {
diff --git a/ee/tabby-ui/yarn.lock b/ee/tabby-ui/yarn.lock
index 5bbcc95..b0d49a6 100644
--- a/ee/tabby-ui/yarn.lock
+++ b/ee/tabby-ui/yarn.lock
@@ -1022,6 +1022,11 @@
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
+"@hookform/resolvers@^3.3.2":
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.3.2.tgz#5c40f06fe8137390b071d961c66d27ee8f76f3bc"
+ integrity sha512-Tw+GGPnBp+5DOsSg4ek3LCPgkBOuOgS5DsDV7qsWNH9LZc433kgsWICjlsh2J9p04H2K66hsXPPb9qn9ILdUtA==
+
"@humanwhocodes/config-array@^0.11.11":
version "0.11.11"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844"
@@ -5835,6 +5840,11 @@ react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
+react-hook-form@^7.48.2:
+ version "7.48.2"
+ resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.48.2.tgz#01150354d2be61412ff56a030b62a119283b9935"
+ integrity sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A==
+
react-hot-toast@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.1.tgz#df04295eda8a7b12c4f968e54a61c8d36f4c0994"
@@ -7284,6 +7294,11 @@ zod@3.21.4:
resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==
+zod@^3.22.4:
+ version "3.22.4"
+ resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"
+ integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==
+
zustand@^4.4.6:
version "4.4.6"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.6.tgz#03c78e3e2686c47095c93714c0c600b72a6512bd"