Skip to content

Commit

Permalink
feat: replace input component with nextui
Browse files Browse the repository at this point in the history
  • Loading branch information
mrevanzak committed May 5, 2024
1 parent f7b8de7 commit b9c68e4
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 1,418 deletions.
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
"with-env": "dotenv -e ../../.env --"
},
"dependencies": {
"@tanya.in/ui": "*",
"@t3-oss/env-nextjs": "^0.10.0",
"@tanstack/react-query": "^5.32.0",
"@tanya.in/ui": "*",
"geist": "^1.3.0",
"next": "^14.2.0",
"next-auth": "beta",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-icons": "^5.2.0",
"superjson": "2.2.1",
"zod": "^3.23.0"
},
Expand Down
73 changes: 46 additions & 27 deletions apps/web/src/components/signin-form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"use client";

import * as React from "react";
import Link from "next/link";
import { signInFormSchema } from "@/lib/validation/sign-in";
import { FaEye, FaEyeSlash } from "react-icons/fa";

import { Button } from "@tanya.in/ui/button";
import {
Expand All @@ -10,10 +13,17 @@ import {
CardHeader,
CardTitle,
} from "@tanya.in/ui/card";
import { Input } from "@tanya.in/ui/input";
import { Label } from "@tanya.in/ui/label";
import { Form, FormInput, useForm } from "@tanya.in/ui/form";

export function SigninForm() {
const [isVisible, setIsVisible] = React.useState(false);

const methods = useForm({
schema: signInFormSchema,
mode: "onTouched",
});
const { handleSubmit, control } = methods;

return (
<Card className="mx-auto max-w-sm">
<CardHeader>
Expand All @@ -23,32 +33,41 @@ export function SigninForm() {
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="[email protected]"
required
<Form {...methods}>
<form
className="space-y-4"
onSubmit={handleSubmit((data) => {
console.log(data);
})}
>
<FormInput name="email" type="email" control={control} />
<FormInput
name="password"
control={control}
endContent={
<button
className="focus:outline-none"
type="button"
onClick={() => setIsVisible(!isVisible)}
>
{isVisible ? (
<FaEyeSlash className="pointer-events-none text-2xl text-default-400" />
) : (
<FaEye className="pointer-events-none text-2xl text-default-400" />
)}
</button>
}
type={isVisible ? "text" : "password"}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Link href="#" className="ml-auto inline-block text-sm underline">
Forgot your password?
</Link>
</div>
<Input id="password" type="password" required />
</div>
<Button color="primary" type="submit" className="w-full">
Login
</Button>
<Button variant="shadow" color="warning" className="w-full">
Login with MyITS
</Button>
</div>

<Button color="primary" type="submit" className="w-full">
Login
</Button>
<Button variant="shadow" color="warning" className="w-full">
Login with MyITS
</Button>
</form>
</Form>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{" "}
<Link href="#" className="underline">
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/lib/validation/sign-in.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { z } from "zod";

export const signInFormSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
Binary file modified bun.lockb
Binary file not shown.
3 changes: 1 addition & 2 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@
"@hookform/resolvers": "^3.3.4",
"@nextui-org/button": "^2.0.31",
"@nextui-org/dropdown": "^2.1.23",
"@nextui-org/input": "^2.1.21",
"@nextui-org/system": "^2.1.2",
"@nextui-org/theme": "^2.2.3",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"framer-motion": "^11.1.7",
"next-themes": "^0.3.0",
Expand Down
178 changes: 19 additions & 159 deletions packages/ui/src/form.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
"use client";

import type * as LabelPrimitive from "@radix-ui/react-label";
import type {
ControllerProps,
FieldPath,
FieldValues,
UseControllerProps,
UseFormProps,
} from "react-hook-form";
import type { ZodType, ZodTypeDef } from "zod";
import * as React from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { Slot } from "@radix-ui/react-slot";
import { Input, InputProps } from "@nextui-org/input";
import {
useForm as __useForm,
Controller,
FormProvider,
useFormContext,
useController,
} from "react-hook-form";

import { cn } from "@tanya.in/ui";

import { Label } from "./label";

const useForm = <TOut, TDef extends ZodTypeDef, TIn extends FieldValues>(
props: Omit<UseFormProps<TIn>, "resolver"> & {
schema: ZodType<TOut, TDef, TIn>;
Expand All @@ -37,165 +31,31 @@ const useForm = <TOut, TDef extends ZodTypeDef, TIn extends FieldValues>(

const Form = FormProvider;

interface FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> {
name: TName;
}

const FormFieldContext = React.createContext<FormFieldContextValue | null>(
null,
);

const FormField = <
const FormInput = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};

const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();

if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const fieldState = getFieldState(fieldContext.name, formState);

const { id } = itemContext;

return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};

interface FormItemContextValue {
id: string;
}

const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
);

const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();

return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";

const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();

return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";

const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();

return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";

const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
}: UseControllerProps<TFieldValues, TName> & InputProps) => {
const { field, fieldState } = useController({
name: props.name,
control: props.control,
});

return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
<Input
{...field}
//automatically capitalize the first letter of the label and replace camel case with normal case
label={props.name
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase())}
isInvalid={fieldState.invalid}
errorMessage={fieldState?.error?.message}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";

const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error.message) : children;

if (!body) {
return null;
}

return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";

export {
useForm,
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

export { useForm, Form, FormInput };

export { useFieldArray } from "react-hook-form";
24 changes: 0 additions & 24 deletions packages/ui/src/input.tsx

This file was deleted.

25 changes: 0 additions & 25 deletions packages/ui/src/label.tsx

This file was deleted.

Loading

0 comments on commit b9c68e4

Please sign in to comment.