Skip to content

Commit

Permalink
feat: add initial email contact form
Browse files Browse the repository at this point in the history
  • Loading branch information
NedcloarBR committed Jan 25, 2025
1 parent f02969e commit 88f8c55
Show file tree
Hide file tree
Showing 11 changed files with 443 additions and 2 deletions.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@
"check": "npx @biomejs/biome check ./"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@vercel/analytics": "^1.4.1",
Expand All @@ -43,9 +45,11 @@
"next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"simple-icons": "^14.2.0",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.1"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
Expand Down
13 changes: 13 additions & 0 deletions public/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,19 @@
"Description": "Configuring a linter and formatter, such as BiomeJS, is crucial for maintaining consistent, readable, and error-free code. Linters ensure uniform coding standards and early error detection, saving debugging time. Formatters automate code structuring, enhancing readability and maintenance. These tools increase productivity by allowing developers to focus on critical tasks. They also enforce quality standards, creating a professional development environment. In CI/CD workflows, they ensure code adherence to quality standards, preserving codebase integrity."
}
},
"Contact": {
"Title": "Contact",
"Form": {
"Name": "Name",
"NameDescription": "What should I call you?",
"Subject": "Subject",
"SubjectDescription": "What would you like to talk about?",
"Message": "Message",
"MessageDescription": "Write your message here...",
"Submit": "Submit",
"NameTooShort": "Name must have at least 3 characters"
}
},
"Error": {
"Copy": "Copy",
"Reload": "Click here to back to main page",
Expand Down
13 changes: 13 additions & 0 deletions public/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,19 @@
"Description": "Configurar um linter e um formatador, como o BiomeJS, é essencial para manter um código consistente, legível e livre de erros. Linters garantem padrões uniformes de codificação e a detecção precoce de erros, economizando tempo de depuração. Formatadores automatizam a estruturação do código, melhorando sua legibilidade e manutenção. Essas ferramentas aumentam a produtividade, permitindo que os desenvolvedores se concentrem em tarefas críticas. Além disso, elas reforçam padrões de qualidade, criando um ambiente de desenvolvimento profissional. Em fluxos de trabalho CI/CD, garantem a conformidade do código com os padrões de qualidade, preservando a integridade da base de código."
}
},
"Contact": {
"Title": "Contato",
"Form": {
"Name": "Nome",
"NameDescription": "Como posso te chamar?",
"Subject": "Assunto",
"SubjectDescription": "Sobre o que você gostaria de falar?",
"Message": "Mensagem",
"MessageDescription": "Escreva sua mensagem aqui...",
"Submit": "Enviar",
"NameTooShort": "Nome deve ter no mínimo 3 caracteres"
}
},
"Error": {
"Copy": "Copiar",
"Reload": "Clique aqui para voltar para a página principal",
Expand Down
114 changes: 113 additions & 1 deletion src/components/contact.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,115 @@
"use client";

import { useTranslations } from "next-intl";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";

import {
Button,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
Textarea,
} from "@/components/ui";

export function Contact() {
return <section id="contact" className="h-screen"></section>;
const t = useTranslations("Contact");

const formSchema = z.object({
name: z
.string()
.min(3, {
message: t("Form.NameTooShort"),
})
.nonempty({}),
subject: z.string().nonempty(),
message: z.string().nonempty(),
});

type formType = z.infer<typeof formSchema>;

const form = useForm<formType>({
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:[email protected]?subject=${subject}&body=${body}`;
window.open(mailtoLink, "_blank");
}

return (
<section id="contact" className="grid justify-center h-screen">
<h1 className="mt-8 flex justify-center items-center text-4xl">
{t("Title")}
</h1>
<div className="flex justify-center items-center gap-4">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Form.Name")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>{t("Form.NameDescription")}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="subject"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Form.Subject")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t("Form.SubjectDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Form.Message")}</FormLabel>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormDescription>
{t("Form.MessageDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
</div>
</section>
);
}
Empty file.
178 changes: 178 additions & 0 deletions src/components/ui/form.tsx
Original file line number Diff line number Diff line change
@@ -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<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}

const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)

const FormField = <
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()

const fieldState = getFieldState(fieldContext.name, formState)

if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}

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<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()

return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...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-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"

export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}
3 changes: 3 additions & 0 deletions src/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
22 changes: 22 additions & 0 deletions src/components/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from "react"

import { cn } from "@/lib/utils"

const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"

export { Input }
Loading

0 comments on commit 88f8c55

Please sign in to comment.