diff --git a/frontend/bun.lockb b/frontend/bun.lockb index d57c95a..1e717ae 100644 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/package.json b/frontend/package.json index 38ae999..f49a21a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@fontsource/inter": "^5.0.19", + "@hookform/resolvers": "^3.9.0", "@it-incubator/prettier-config": "^0.1.2", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-dialog": "^1.1.1", @@ -33,6 +34,7 @@ "lucide-react": "^0.400.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.52.1", "sonner": "^1.5.0", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", diff --git a/frontend/src/components/ui/form/form-input.tsx b/frontend/src/components/ui/form/form-input.tsx new file mode 100644 index 0000000..4fa0d6d --- /dev/null +++ b/frontend/src/components/ui/form/form-input.tsx @@ -0,0 +1,32 @@ +import { Input, type InputProps } from "@/components/ui"; +import { + type Control, + type FieldValues, + type UseControllerProps, + useController, +} from "react-hook-form"; + +type Props = Omit< + UseControllerProps, + "control" | "defaultValue" | "rules" +> & + Omit & { control: Control }; + +export const FormInput = ({ + control, + name, + disabled, + shouldUnregister, + ...rest +}: Props) => { + const { + field, + fieldState: { error }, + } = useController({ + control, + name, + disabled, + shouldUnregister, + }); + return ; +}; diff --git a/frontend/src/components/ui/form/form-select.tsx b/frontend/src/components/ui/form/form-select.tsx new file mode 100644 index 0000000..64776c0 --- /dev/null +++ b/frontend/src/components/ui/form/form-select.tsx @@ -0,0 +1,34 @@ +import { Select } from "@/components/ui"; +import type { ComponentPropsWithoutRef } from "react"; +import { + type Control, + type FieldValues, + type UseControllerProps, + useController, +} from "react-hook-form"; + +type Props = Omit< + UseControllerProps, + "control" | "defaultValue" | "rules" +> & + Omit, "value" | "onValueChange"> & { + control: Control; + }; + +export const FormSelect = ({ + control, + name, + disabled, + shouldUnregister, + ...rest +}: Props) => { + const { + field: { onChange, ...field }, + } = useController({ + control, + name, + disabled, + shouldUnregister, + }); + return + {!!label && } + + {!!errorMessage && ( +

{errorMessage}

)} - ref={ref} - {...props} - /> + ); }, ); diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts new file mode 100644 index 0000000..8d8add4 --- /dev/null +++ b/frontend/src/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-auto-id"; diff --git a/frontend/src/hooks/use-auto-id.ts b/frontend/src/hooks/use-auto-id.ts new file mode 100644 index 0000000..8d0c60d --- /dev/null +++ b/frontend/src/hooks/use-auto-id.ts @@ -0,0 +1,6 @@ +import { useId } from 'react' + +export const useAutoId = (id?: string) => { + const generatedId = useId() + return id ?? generatedId +} diff --git a/frontend/src/routes/auth/login.tsx b/frontend/src/routes/auth/login.tsx index 47e3eaf..3ab04a3 100644 --- a/frontend/src/routes/auth/login.tsx +++ b/frontend/src/routes/auth/login.tsx @@ -6,9 +6,9 @@ import { CardFooter, CardHeader, CardTitle, - Input, + FormInput, + FormSelect, Label, - Select, SelectContent, SelectItem, SelectTrigger, @@ -16,30 +16,122 @@ import { ToggleGroup, ToggleGroupItem, } from "@/components/ui"; -import { useLoginMutation } from "@/services/db"; +import { type LoginArgs, useLoginMutation } from "@/services/db"; import { useSessionStore } from "@/state/db-session-store"; +import { zodResolver } from "@hookform/resolvers/zod"; import { createFileRoute } from "@tanstack/react-router"; -import { type FormEventHandler, useState } from "react"; -import { toast } from "sonner"; +import { useState } from "react"; +import { type Control, useForm } from "react-hook-form"; +import { z } from "zod"; export const Route = createFileRoute("/auth/login")({ component: LoginForm, }); -function DatabaseTypeSelector() { +const loginWithConnectionStringSchema = z.object({ + type: z.enum(["mysql", "postgres"]), + connectionString: z.string().trim().min(1, "Connection string is required"), +}); + +type LoginWithConnectionStringFields = z.infer< + typeof loginWithConnectionStringSchema +>; + +function ConnectionStringForm({ + onSubmit, +}: { + onSubmit: (values: LoginWithConnectionStringFields) => void; +}) { + const { control, handleSubmit } = useForm({ + resolver: zodResolver(loginWithConnectionStringSchema), + defaultValues: { + type: "postgres", + connectionString: "", + }, + }); + return ( -
- - -
+
+ + + + ); +} + +const loginWithConnectionFieldsSchema = z.object({ + type: z.enum(["mysql", "postgres"]), + username: z.string().min(1, "Username is required"), + password: z.string().min(1, "Password is required"), + host: z.string().min(1, "Host is required"), + port: z.string().min(1, "Port is required"), + database: z.string().min(1, "Database is required"), + ssl: z.enum(["false", "true", "require", "allow", "prefer", "verify-full"]), +}); +type LoginWithConnectionFields = z.infer< + typeof loginWithConnectionFieldsSchema +>; + +function ConnectionFieldsForm({ + onSubmit, +}: { + onSubmit: (values: LoginWithConnectionFields) => void; +}) { + const { control, handleSubmit } = useForm({ + resolver: zodResolver(loginWithConnectionFieldsSchema), + defaultValues: { + type: "postgres", + host: "", + port: "", + username: "", + password: "", + ssl: "prefer", + database: "", + }, + }); + + return ( +
+ + + + + + +
+ + + + + + + false + true + require + allow + prefer + verify-full + + +
+ ); } @@ -49,78 +141,9 @@ function LoginForm() { const { mutateAsync } = useLoginMutation(); const addSession = useSessionStore.use.addSession(); - const handleSubmit: FormEventHandler = async (e) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const connectionString = formData.get("connectionString"); - const type = formData.get("type"); - if (connectionMethod === "connectionString") { - if ( - connectionString != null && - typeof connectionString === "string" && - type != null && - typeof type === "string" - ) { - try { - await mutateAsync({ connectionString, type }); - addSession({ connectionString, type }); - } catch (error) { - console.log(error); - toast.error("Invalid connection string"); - return; - } - } else { - toast.error("Please fill all fields"); - } - return; - } - - const username = formData.get("username"); - const password = formData.get("password"); - const host = formData.get("host"); - const port = formData.get("port"); - const database = formData.get("database"); - const ssl = formData.get("ssl"); - - if ( - database == null || - host == null || - password == null || - port == null || - ssl == null || - type == null || - username == null - ) { - toast.error("Please fill all fields"); - return; - } - if ( - typeof database !== "string" || - typeof host !== "string" || - typeof password !== "string" || - typeof port !== "string" || - typeof ssl !== "string" || - typeof type !== "string" || - typeof username !== "string" - ) { - return; - } - try { - await mutateAsync({ - username, - password, - host, - type, - port, - database, - ssl, - }); - addSession({ username, password, host, type, port, database, ssl }); - } catch (error) { - console.log(error); - toast.error("Invalid connection string"); - return; - } + const onSubmit = async (args: LoginArgs) => { + await mutateAsync(args); + addSession(args); }; return ( @@ -133,11 +156,7 @@ function LoginForm() { -
+
{connectionMethod === "fields" ? ( - <> - -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- + ) : ( -
- - - -
+ )} - +