({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ name: "",
+ subject: "",
+ message: "",
+ },
+ });
+
+ function onSubmit(values: formType) {
+ const { name, subject, message } = values;
+
+ const body = `Name: ${name}\n\nMessage: ${message}`;
+
+ const mailtoLink = `mailto:nedcloar1@hotmail.com?subject=${subject}&body=${body}`;
+ window.open(mailtoLink, "_blank");
+ }
+
+ return (
+
+ );
}
diff --git a/src/components/email-emplate.tsx b/src/components/email-emplate.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx
new file mode 100644
index 0000000..ce264ae
--- /dev/null
+++ b/src/components/ui/form.tsx
@@ -0,0 +1,178 @@
+"use client"
+
+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/src/components/ui/index.ts b/src/components/ui/index.ts
index 6b42710..302307b 100644
--- a/src/components/ui/index.ts
+++ b/src/components/ui/index.ts
@@ -3,5 +3,8 @@ export * from "./card";
export * from "./carousel";
export * from "./dialog";
export * from "./dropdown-menu";
+export * from "./form";
+export * from "./input";
+export * from "./textarea";
export * from "./separator";
export * from "./sheet";
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..68551b9
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
new file mode 100644
index 0000000..5341821
--- /dev/null
+++ b/src/components/ui/label.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..4d858bb
--- /dev/null
+++ b/src/components/ui/textarea.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Textarea = React.forwardRef<
+ HTMLTextAreaElement,
+ React.ComponentProps<"textarea">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+Textarea.displayName = "Textarea"
+
+export { Textarea }
diff --git a/yarn.lock b/yarn.lock
index 6541710..d19e9af 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -208,6 +208,15 @@ __metadata:
languageName: node
linkType: hard
+"@hookform/resolvers@npm:^3.10.0":
+ version: 3.10.0
+ resolution: "@hookform/resolvers@npm:3.10.0"
+ peerDependencies:
+ react-hook-form: ^7.0.0
+ checksum: 10c0/7ee44533b4cdc28c4fa2a94894c735411e5a1f830f4a617c580533321a9b901df0cc8c1e2fad81ad8d55154ebc5cb844cf9c116a3148ffae2bc48758c33cbb8e
+ languageName: node
+ linkType: hard
+
"@img/sharp-darwin-arm64@npm:0.33.5":
version: 0.33.5
resolution: "@img/sharp-darwin-arm64@npm:0.33.5"
@@ -781,6 +790,25 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-label@npm:^2.1.1":
+ version: 2.1.1
+ resolution: "@radix-ui/react-label@npm:2.1.1"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.0.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/902628dc2c05610462a264feedc8c548d7ecad7f000efb9a4190e365ee2b7f75eccf98b43925fac6e1fa940c437abbce03ecc6868e06e0a197c779973ccc839d
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-menu@npm:2.1.4":
version: 2.1.4
resolution: "@radix-ui/react-menu@npm:2.1.4"
@@ -2376,9 +2404,11 @@ __metadata:
resolution: "portfolio@workspace:."
dependencies:
"@biomejs/biome": "npm:^1.9.4"
+ "@hookform/resolvers": "npm:^3.10.0"
"@nedcloarbr/biome-config": "npm:^1.6.1"
"@radix-ui/react-dialog": "npm:^1.1.4"
"@radix-ui/react-dropdown-menu": "npm:^2.1.4"
+ "@radix-ui/react-label": "npm:^2.1.1"
"@radix-ui/react-separator": "npm:^1.1.1"
"@radix-ui/react-slot": "npm:^1.1.1"
"@types/node": "npm:^22.10.5"
@@ -2397,11 +2427,13 @@ __metadata:
postcss: "npm:^8.4.49"
react: "npm:^19.0.0"
react-dom: "npm:^19.0.0"
+ react-hook-form: "npm:^7.54.2"
simple-icons: "npm:^14.2.0"
tailwind-merge: "npm:^2.6.0"
tailwindcss: "npm:^3.4.17"
tailwindcss-animate: "npm:^1.0.7"
typescript: "npm:^5.7.3"
+ zod: "npm:^3.24.1"
languageName: unknown
linkType: soft
@@ -2539,6 +2571,15 @@ __metadata:
languageName: node
linkType: hard
+"react-hook-form@npm:^7.54.2":
+ version: 7.54.2
+ resolution: "react-hook-form@npm:7.54.2"
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+ checksum: 10c0/6eebead2900e3d369a989e7a20429f390dc75b3897142aa3107f1f6dabb9ae64fed201ea98cdcd8676e40466c97748aeb0c0d83264f5bd3a84dbc0b8e4863415
+ languageName: node
+ linkType: hard
+
"react-remove-scroll-bar@npm:^2.3.7":
version: 2.3.8
resolution: "react-remove-scroll-bar@npm:2.3.8"
@@ -3258,3 +3299,10 @@ __metadata:
checksum: 10c0/b4a9dea34265f000402c909144ac310be42c4526dfd16dff1aee2b04a0d94051713651c0cd2b0a3d8109266997422120f16a7934629d12f22dc215839ebbeccf
languageName: node
linkType: hard
+
+"zod@npm:^3.24.1":
+ version: 3.24.1
+ resolution: "zod@npm:3.24.1"
+ checksum: 10c0/0223d21dbaa15d8928fe0da3b54696391d8e3e1e2d0283a1a070b5980a1dbba945ce631c2d1eccc088fdbad0f2dfa40155590bf83732d3ac4fcca2cc9237591b
+ languageName: node
+ linkType: hard