Skip to content

Commit

Permalink
add contact form and footer
Browse files Browse the repository at this point in the history
  • Loading branch information
sofiabaezzato committed Dec 2, 2023
1 parent de96659 commit 7494ed2
Show file tree
Hide file tree
Showing 21 changed files with 1,083 additions and 55 deletions.
36 changes: 36 additions & 0 deletions actions/sendEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use server"
import { error } from "console"
import { Resend } from "resend"
import { validateString } from "@/lib/utils"

const resend = new Resend(process.env.RESEND_API_KEY)

export const sendEmail = async (FormData: FormData) => {
const sender = FormData.get("email")
const message = FormData.get("message")

//server side input validation
if (!validateString(sender, 200)) {
return error('Invalid Email')
}
if (!validateString(message, 5000)) {
return error('Invalid Message')
}

let data;
try {
await resend.emails.send({
from: 'Contact Form <[email protected]>',
to: '[email protected]',
subject: 'Portfolio Form: New message',
reply_to: sender as string,
text: message + ' from ' + sender
})
} catch (error: any) {
return error.message
}

return {
data
}
}
3 changes: 3 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
@tailwind components;
@tailwind utilities;

html {
--line-color: #e5e7eb;
}
7 changes: 6 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Inter } from 'next/font/google'
import './globals.css'
import Header from '@/components/Header'
import ActiveSectionContextProvider from '@/context/ActiveSectionContext'
import { Toaster } from 'react-hot-toast'
import Footer from '@/components/Footer'

const inter = Inter({ subsets: ['latin'] })

Expand All @@ -24,7 +26,10 @@ export default function RootLayout({

<ActiveSectionContextProvider>
<Header />
{children}
{children}

<Toaster position='bottom-right'/>
<Footer />
</ActiveSectionContextProvider>

</body>
Expand Down
4 changes: 4 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Intro from "@/components/Intro";
import SectionDivider from "@/components/SectionDivider";
import Projects from "@/components/Projects"
import Skills from "@/components/Skills";
import Experience from "@/components/Experience";
import Contact from "@/components/Contact";

export default function Home() {
return (
Expand All @@ -14,6 +16,8 @@ export default function Home() {
<Projects />
<SectionDivider />
<Skills />
<SectionDivider />
<Contact />
</main>
)
}
69 changes: 69 additions & 0 deletions components/Contact.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client"

import React from 'react'
import SectionHeading from './SectionHeading'
import { useSectionInView } from '@/lib/hooks'
import { motion } from 'framer-motion'
import { sendEmail } from '@/actions/sendEmail'
import SubmitButton from './SubmitButton'
import toast from 'react-hot-toast'

const Contact = () => {
const { ref } = useSectionInView('Contact', 0.5)

return (
<motion.section
id='contact'
className='mb-20 sm:mb-28 w-[min(100%,38rem)] text-center'
ref={ref}
initial={{
opacity: 0,
}}
whileInView={{
opacity: 1,
}}
transition={{
duration: 1,
}}
viewport={{
once: true,
}}
>
<SectionHeading>Contact me</SectionHeading>
<p className='text-gray-700 -mt-6'>Feel free to contact me through this form.</p>

<form
className='flex flex-col gap-1 mt-10'
action={async (FormData) => {
const { data, error } = await sendEmail(FormData)

if (error) {
toast.error(error)
return
}

toast.success('Email sent!')
}}
>
<input
name='email'
type="email"
className='h-14 rounded-lg border border-black/5 p-4'
placeholder='Your email'
maxLength={200}
required
/>
<textarea
name='message'
className='h-52 my-3 rounded-lg border border-black/5 p-4'
placeholder='Your message'
maxLength={5000}
required
></textarea>
<SubmitButton />
</form>
</motion.section>
)
}

export default Contact
20 changes: 20 additions & 0 deletions components/Experience.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client"

import React from 'react'
import SectionHeading from './SectionHeading'
import { experiencesData } from '@/lib/data';

const Experience = () => {
return (
<section
id='experience'
>
<SectionHeading>My Experience</SectionHeading>
{experiencesData.map((item, index) => (

))}
</section>
)
}

export default Experience
16 changes: 16 additions & 0 deletions components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react'

const Footer = () => {
return (
<footer className='mb-10 px-4 text-center text-gray-500'>
<small className='mb-2 text-xs'>
&copy; {new Date().getFullYear()} Sofia - made with ❤️
</small>
<p className='text-xs'>
<span className='font-semibold'>About this website:</span> built with React (Next.js), Typescript, Framer Motion and Resend. Hosted in Vercel.
</p>
</footer>
)
}

export default Footer
14 changes: 12 additions & 2 deletions components/Intro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import Link from 'next/link'
import { BsArrowRight, BsLinkedin } from 'react-icons/bs'
import { FaGithubSquare } from 'react-icons/fa'
import { useSectionInView } from '@/lib/hooks'
import { useActiveSectionContext } from '@/context/ActiveSectionContext'

const Intro = () => {
const { ref } = useSectionInView('Home', 0.5)
const { setActiveSection, setTimeLastClick } = useActiveSectionContext()


return (
<section
Expand Down Expand Up @@ -70,8 +73,15 @@ const Intro = () => {

<Link
href='#contact'
className='group bg-[#ffd500] text-gray-600 px-7 py-3 flex items-center gap-2 rounded-full outline-none hover:bg-[#ffd500] hover:text-gray-950 hover:scale-105 active:scale-95 transition-transform'
>Contact me<BsArrowRight className='opacity-70 group-hover:translate-x-1 transition-transform'/></Link>
className='group bg-[#ffd500] text-gray-600 px-7 py-3 flex items-center gap-2 rounded-full outline-none hover:bg-[#ffd500] hover:text-gray-950 hover:scale-105
active:scale-95 transition-transform'
onClick={() => {
setActiveSection("Contact")
setTimeLastClick(Date.now())
}}
>
Contact me<BsArrowRight className='opacity-70 group-hover:translate-x-1 transition-transform'/>
</Link>

<a
href="https://www.linkedin.com/in/sofia-baezzato"
Expand Down
2 changes: 1 addition & 1 deletion components/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const ProjectCard = ({
href={repoUrl}
target='_blank'
className='group/button text-xs font-semibold bg-white text-gray-600 px-4 py-2 flex items-center gap-2 rounded-full outline-none focus:scale-105 hover:border-gray-950 hover:text-gray-950 hover:scale-105 active:scale-95 transition-transform'
>Repo<BsArrowRight className='opacity-70 group-hover/button:translate-x-1 transition-transform'/></Link>
>Code<BsArrowRight className='opacity-70 group-hover/button:translate-x-1 transition-transform'/></Link>
</div>

<p className="mt-2 leading-relaxed text-slate-700">{description}</p>
Expand Down
8 changes: 6 additions & 2 deletions components/Skills.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import SectionHeading from './SectionHeading'
import { skillsData } from '@/lib/data'
import { useSectionInView } from '@/lib/hooks'
import { motion } from 'framer-motion'
import DevIcon from 'devicon-react-svg'
import Image from 'next/image'

const fadeInAnimationVariants = {
initial: {
Expand All @@ -27,8 +29,9 @@ const Skills = () => {
<section
className='mb-28 max-w-[53rem] scroll-mt-28 text-center sm:mb-40'
ref={ref}
id='skills'
>
<SectionHeading>My Skills</SectionHeading>
<SectionHeading>My Toolbox</SectionHeading>
<ul
className='flex flex-wrap justify-center gap-2 text-md text-gray-800'
>
Expand All @@ -44,7 +47,8 @@ const Skills = () => {
}}
custom={index}
>
{skill.icon}
{skill.name === "Tailwind" || skill.name === "Next.js" || skill.name === "Framer Motion" ? <Image src={skill.icon} alt='dev-icon' width={20} height={20} className='text-gray-600'/> :
<DevIcon icon={skill.icon} className='text-gray-600' width={20} height={20}/>}
{skill.name}
</motion.li>
))}
Expand Down
29 changes: 29 additions & 0 deletions components/SubmitButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react'
import { FaPaperPlane } from 'react-icons/fa'

import {
// @ts-expect-error
experimental_useFormState as useFormState,
// @ts-expect-error
experimental_useFormStatus as useFormStatus,
} from "react-dom";

const SubmitButton = () => {
const { pending } = useFormStatus()

return (
<button
type='submit'
className='group h-[3rem] w-[8rem] bg-[#ffd500] text-gray-600 px-7 py-3 flex items-center justify-center gap-2 rounded-full outline-none hover:bg-[#ffd500] hover:text-gray-950 hover:scale-105 active:scale-95 transition-transform disabled:scale-100 disabled:bg-opacity-50'
disabled={pending}
>
{pending ? <div className='h-5 w-5 animate-spin rounded-full border-b-2 border-white'></div> : (
<>
Submit <FaPaperPlane className="opacity-70 group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform" />
</>
)}
</button>
)
}

export default SubmitButton
Loading

0 comments on commit 7494ed2

Please sign in to comment.