Skip to content

Commit

Permalink
Implement contact form
Browse files Browse the repository at this point in the history
  • Loading branch information
cesalberca committed Aug 6, 2024
1 parent b83200e commit 5118264
Show file tree
Hide file tree
Showing 14 changed files with 447 additions and 48 deletions.
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
NEXT_PUBLIC_URL=http://localhost:3000
NEXT_PUBLIC_FORMSPREE_API_KEY=mjkbwabp
1 change: 1 addition & 0 deletions .env.production
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
NEXT_PUBLIC_URL=http://www.cesalberca.com
NEXT_PUBLIC_FORMSPREE_API_KEY=mjkbwabp
106 changes: 101 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
"prepare": "husky install"
},
"dependencies": {
"@formspree/react": "2.5.1",
"@hookform/resolvers": "3.9.0",
"@icons-pack/react-simple-icons": "9.6.0",
"@radix-ui/react-dropdown-menu": "2.1.1",
"@radix-ui/react-label": "2.1.0",
"@radix-ui/react-popover": "1.1.1",
"@radix-ui/react-slot": "1.1.0",
"@tailwindcss/typography": "0.5.13",
Expand All @@ -34,14 +37,16 @@
"postcss": "8.4.39",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hook-form": "7.52.1",
"react-markdown": "9.0.1",
"react-syntax-highlighter": "15.5.0",
"reflect-metadata": "0.2.2",
"sugar-high": "0.7.0",
"tailwind-merge": "2.4.0",
"tailwindcss": "3.4.4",
"tailwindcss-animate": "1.0.7",
"tsyringe": "4.8.0"
"tsyringe": "4.8.0",
"zod": "3.23.8"
},
"devDependencies": {
"@types/jest": "29.5.12",
Expand Down
2 changes: 1 addition & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
}

.wrapper {
@apply mx-auto max-w-xl;
@apply mx-auto max-w-screen-sm;
}
}

Expand Down
138 changes: 138 additions & 0 deletions src/components/ui/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
'use client'

import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { Slot } from '@radix-ui/react-slot'
import {
Controller,
type ControllerProps,
type FieldPath,
type 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 }
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'

export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(({ 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-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium 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',
className,
)}
ref={ref}
{...props}
/>
)
})
Input.displayName = 'Input'

export { Input }
Loading

0 comments on commit 5118264

Please sign in to comment.