Skip to content

Commit

Permalink
Merge pull request #25 from firehawk89/add-contact-section
Browse files Browse the repository at this point in the history
Add Contact Section
  • Loading branch information
firehawk89 authored Feb 8, 2024
2 parents 2b0d1ed + 35177c1 commit 87d7fcf
Show file tree
Hide file tree
Showing 19 changed files with 458 additions and 13 deletions.
55 changes: 54 additions & 1 deletion 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 @@ -15,6 +15,8 @@
"prepare": "husky install"
},
"dependencies": {
"@emailjs/nodejs": "^2.2.0",
"@hookform/resolvers": "^3.3.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"gray-matter": "^4.0.3",
Expand All @@ -23,8 +25,11 @@
"next-themes": "^0.2.1",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.50.1",
"react-icons": "^5.0.1",
"tailwind-merge": "^2.2.1"
"sonner": "^1.4.0",
"tailwind-merge": "^2.2.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20",
Expand Down
4 changes: 3 additions & 1 deletion project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ Bochkovskyi
clsx
Nextdotjs
Tailwindcss
Skauna
Skauna
hookform
sonner
37 changes: 37 additions & 0 deletions src/app/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use server'

import { ContactFormInputs, ContactFormSchema } from '@/utils'
import emailjs, { EmailJSResponseStatus } from '@emailjs/nodejs'

export const sendEmail = async (data: ContactFormInputs) => {
const dataParseResult = ContactFormSchema.safeParse(data)

if (dataParseResult.success) {
const parsedData = dataParseResult.data

try {
const response = await emailjs.send(
process.env.EMAILJS_SERVICE_ID!,
process.env.EMAILJS_TEMPLATE_ID!,
parsedData,
{
privateKey: process.env.EMAILJS_PRIVATE_KEY!,
publicKey: process.env.EMAILJS_PUBLIC_KEY!,
}
)

return { message: response.text, success: true }
} catch (error) {
if (error instanceof EmailJSResponseStatus) {
return { message: error.text, success: false }
}
}
}

if (!dataParseResult.success) {
return {
message: JSON.stringify(dataParseResult.error.format()),
success: false,
}
}
}
2 changes: 2 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import About from '@/components/sections/about'
import Contact from '@/components/sections/contact/contact'
import Hero from '@/components/sections/hero/hero'
import Projects from '@/components/sections/projects/projects'

Expand All @@ -8,6 +9,7 @@ export default function Home() {
<Hero />
<About className="py-16 md:py-24" id="about-me" />
<Projects className="py-16 md:py-24" id="my-projects" />
<Contact className="py-16 md:py-24" id="contact-me" />
</>
)
}
105 changes: 105 additions & 0 deletions src/components/sections/contact/contact-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
'use client'

import { sendEmail } from '@/app/actions'
import Button from '@/components/ui/button'
import FormItem from '@/components/ui/form-item'
import Input from '@/components/ui/input'
import Label from '@/components/ui/label'
import Textarea from '@/components/ui/textarea'
import { ContactFormInputs, ContactFormSchema, cn } from '@/utils'
import { zodResolver } from '@hookform/resolvers/zod'
import { FC, FormHTMLAttributes, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'

interface ContactFormProps extends FormHTMLAttributes<HTMLFormElement> {}

const ContactForm: FC<ContactFormProps> = ({ className, ...props }) => {
const [isSubmitting, setIsSubmitting] = useState<boolean>(false)
const {
formState: { errors },
handleSubmit,
register,
reset,
} = useForm<ContactFormInputs>({
resolver: zodResolver(ContactFormSchema),
})

const onSubmit: SubmitHandler<ContactFormInputs> = async (data) => {
setIsSubmitting(true)

const loadingToast = toast.loading('Sending a message...')
const result = await sendEmail(data)

if (result?.success) {
setIsSubmitting(false)

toast.dismiss(loadingToast)
toast.success('Your message has been sent!', {
closeButton: true,
})

reset()
return
}

setIsSubmitting(false)
toast.dismiss(loadingToast)
toast.error('Oops! Something went wrong!', {
closeButton: true,
})
}

return (
<form
className={cn(
'grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-6 sm:gap-y-8 md:gap-x-10',
className
)}
onSubmit={handleSubmit(onSubmit)}
{...props}
>
<FormItem error={errors.name?.message}>
<Label htmlFor="name">Enter your name</Label>
<Input
disabled={isSubmitting}
id="name"
name="name"
placeholder="Name"
register={register}
required
type="text"
/>
</FormItem>
<FormItem error={errors.email?.message}>
<Label htmlFor="email">Enter your email</Label>
<Input
disabled={isSubmitting}
id="email"
name="email"
placeholder="Email"
register={register}
required
type="email"
/>
</FormItem>
<FormItem className="sm:col-span-2" error={errors.message?.message}>
<Label htmlFor="message">Enter your message</Label>
<Textarea
disabled={isSubmitting}
id="message"
name="message"
placeholder="Message"
register={register}
required
rows={4}
/>
</FormItem>
<Button className="w-full sm:w-fit" disabled={isSubmitting} type="submit">
Submit
</Button>
</form>
)
}

export default ContactForm
32 changes: 32 additions & 0 deletions src/components/sections/contact/contact.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import ContactForm from '@/components/sections/contact/contact-form'
import Content from '@/components/ui/content'
import Heading from '@/components/ui/heading'
import { cn } from '@/utils'
import { FC, HTMLAttributes } from 'react'

interface ContactProps extends HTMLAttributes<HTMLDivElement> {}

const Contact: FC<ContactProps> = ({ className, ...props }) => {
return (
<section className={cn('bg-light dark:bg-black', className)} {...props}>
<Content className="max-w-4xl">
<Heading size="h2" variant="underline">
Get In Touch
</Heading>
<p className="mt-6 md:text-xl">
Drop me a message using the form below or reach out via my email{' '}
<a
className="font-semibold transition-colors hover:text-accent"
href="mailto:[email protected]"
>
[email protected]
</a>
.
</p>
<ContactForm className="mt-6" />
</Content>
</section>
)
}

export default Contact
12 changes: 8 additions & 4 deletions src/components/sections/hero/hero.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Button from '@/components/ui/button'
import Button, { buttonVariants } from '@/components/ui/button'
import Content from '@/components/ui/content'
import Heading from '@/components/ui/heading'
import Socials from '@/components/ui/socials/socials'
import { cn } from '@/utils'
import Link from 'next/link'
import { FC, HTMLAttributes } from 'react'

import styles from './hero.module.css'
Expand All @@ -29,9 +30,12 @@ const Hero: FC<HeroProps> = ({ className, ...props }) => {
elegant and responsive user interfaces.
</p>
</article>
<Button className="mt-5" variant="outline">
My Work
</Button>
<Link
className={cn('mt-5', buttonVariants({ variant: 'outline' }))}
href="/#my-projects"
>
My Projects
</Link>
</Content>
<Socials
className="absolute bottom-8 left-8 hidden md:flex"
Expand Down
4 changes: 2 additions & 2 deletions src/components/sections/projects/project-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ const ProjectCard: FC<ProjectCardProps> = ({
/>
{technologies && (
<div className="flex flex-wrap gap-2">
{technologies.map((technology, index) => (
{technologies.map((technology) => (
<span
className="rounded-xl bg-accent bg-opacity-75 px-2 py-1 text-sm font-semibold"
key={index + 1}
key={technology}
>
{technology}
</span>
Expand Down
2 changes: 1 addition & 1 deletion src/components/sections/projects/projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const Projects: FC<ProjectsProps> = async ({ className, ...props }) => {
explore other sections of the site.
</p>
) : (
<section className="mx-auto mt-8 grid max-w-2xl grid-cols-1 gap-8 md:mt-14 lg:max-w-none lg:grid-cols-2 xl:gap-14">
<section className="mx-auto mt-8 grid max-w-2xl grid-cols-1 gap-8 md:mt-14 lg:max-w-none lg:grid-cols-2 xl:gap-12">
{projects.map((project) => (
<ProjectCard
data={project}
Expand Down
6 changes: 3 additions & 3 deletions src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { VariantProps, cva } from 'class-variance-authority'
import { ButtonHTMLAttributes, FC } from 'react'

export const buttonVariants = cva(
'font-medium transition-all active:scale-95 md:text-lg',
'font-medium transition-all active:scale-95 md:text-lg disabled:pointer-events-none',
{
defaultVariants: {
size: 'default',
Expand All @@ -16,10 +16,10 @@ export const buttonVariants = cva(
},
variant: {
default:
'border-2 border-accent bg-accent text-light md:hover:bg-accent-light md:hover:border-accent-light',
'border-2 border-accent bg-accent text-light md:hover:bg-accent-light disabled:border-accent-light disabled:bg-accent-light md:hover:border-accent-light',
icon: 'bg-transparent md:hover:text-accent',
outline:
'border-2 border-accent bg-transparent text-accent md:hover:bg-accent md:hover:text-light',
'border-2 border-accent bg-transparent text-accent disabled:border-accent-light disabled:text-accent-light md:hover:bg-accent md:hover:text-light',
},
},
}
Expand Down
Loading

0 comments on commit 87d7fcf

Please sign in to comment.