Compare commits

...

4 Commits

Author SHA1 Message Date
7a010a4f8e - working on appointments block 2023-11-29 16:35:05 -05:00
a568ae5e35 - remove commented code
All checks were successful
continuous-integration/drone Build is passing
- slight cleanup
2023-11-29 16:34:46 -05:00
f06d4af1d5 - fix env reference in client code 2023-11-29 16:28:42 -05:00
8b97fcb73e - add recaptcha boilerplate for contact form
- still need to add api key to both project and ci/cd
2023-11-29 09:35:12 -05:00
8 changed files with 148 additions and 40 deletions

View File

@ -32,19 +32,10 @@ const ContentCarousel = ({
<div className=" w-fit self-center pb-1 border-b-2 border-white">{portfolioData.title}</div> <div className=" w-fit self-center pb-1 border-b-2 border-white">{portfolioData.title}</div>
<div>{portfolioData.description}</div> <div>{portfolioData.description}</div>
{portfolioData.prototypeIframeURL && <div style={{minHeight: '100%'}} className="my-4 h-fit flex flex-row justify-center"> {portfolioData.prototypeIframeURL && <div style={{minHeight: '100%'}} className="my-4 h-fit flex flex-row justify-center">
{/* {<embed {portfolioData.heroImgSrc && <Link className="rounded-md w-fit h-full align-middle flex flex-row justify-center" href={portfolioData.prototypeIframeURL} target="#">
src={portfolioData.prototypeIframeURL}
style={{
width: "100%",
minWidth: "100%",
minHeight: '100%',
height: '1px'
}}
/>} */}
{portfolioData.heroImgSrc && <Link className="w-full h-full align-middle flex flex-row justify-center" href={portfolioData.prototypeIframeURL} target="#">
<div className="flex flex-row justify-center"> <div className="flex flex-row justify-center">
<span> <span>
<div className="mb-8 w-full flex-row flex justify-center bg-white"><Image height={300} width={300} src={portfolioData.heroImgSrc} alt=""/></div> <div className="mb-8 w-fit flex-row flex justify-center bg-white"><Image height={200} width={200} src={portfolioData.heroImgSrc} alt=""/></div>
</span> </span>
</div> </div>
</Link>} </Link>}

View File

@ -1,31 +1,49 @@
import { useState } from 'react'; import { RefObject, useRef, useState } from 'react';
import { useForm, SubmitHandler, FieldValues } from 'react-hook-form'; import { useForm, SubmitHandler, FieldValues } from 'react-hook-form';
import ReCAPTCHA from 'react-google-recaptcha';
export interface ContactFormData extends FieldValues {
firstName: string,
lastName: string,
email: string,
subject: string,
summary: string,
honeypot_xyz: string
};
const ContactForm = () => { const ContactForm = () => {
const { const {
register, register,
resetField,
handleSubmit, handleSubmit,
setValue,
formState: { errors }, formState: { errors },
} = useForm(); } = useForm();
const [submitted, setSubmitted] = useState(false); const [submitted, setSubmitted] = useState(false);
const handleFormSubmission: SubmitHandler<FieldValues> = async (data) => { const handleFormSubmission: SubmitHandler<FieldValues> = async (data: FieldValues) => {
console.log(data) const submissionData = data as ContactFormData;
console.log(submissionData)
const res = await fetch('/api/contact', { if (submissionData.honeypot_xyz !== "") {
// Form submission is spam
return;
}
const res = await fetch('/api/contact', {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json, text/plain, */*', 'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(data) body: JSON.stringify(submissionData)
}); });
console.log('Response received') console.log('Response received')
if (res.status === 200) { if (res.status === 200) {
console.log('Response succeeded!') console.log('Response succeeded!')
setSubmitted(true) setSubmitted(true)
resetField('captchaToken')
} }
}; };
@ -33,6 +51,12 @@ const ContactForm = () => {
console.log(error) console.log(error)
}; };
const onCaptchaChange = (token: string | null) => {
// Set the captcha token when the user completes the reCAPTCHA
if (token) {
setValue('captchaToken', token);
}
};
return ( return (
<div className='w-full flex flex-row justify-center'> <div className='w-full flex flex-row justify-center'>
<form className=" w-1/2" onSubmit={handleSubmit(handleFormSubmission, handleFormError)}> <form className=" w-1/2" onSubmit={handleSubmit(handleFormSubmission, handleFormError)}>
@ -62,11 +86,22 @@ const ContactForm = () => {
</select> </select>
{errors.subject && <p>Subject is required.</p>} {errors.subject && <p>Subject is required.</p>}
</div> </div>
<div className="flex flex-col w-full">
<input type='hidden' value="" {...register('honeypot_xyz', { required: true })}/>
</div>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<textarea placeholder='Summary...' className="flex grow h-52" {...register('summary')} /> <textarea placeholder='Summary...' className="flex grow h-52" {...register('summary')} />
{errors.summary && <p>Please enter a message for your Project Inquiry.</p>} {errors.summary && <p>Please enter a message for your Project Inquiry.</p>}
</div> </div>
<div className="pb-20px">
<ReCAPTCHA
size="normal"
sitekey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ? process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY : 'ENTER_API_KEY'}
{...register('captchaToken', { required: true })}
onChange={onCaptchaChange}
/>
</div>
</div> </div>
<input className="bg-blue-600 text-white rounded-sm cursor-pointer" type="submit" /> <input className="bg-blue-600 text-white rounded-sm cursor-pointer" type="submit" />
</div> </div>

View File

@ -0,0 +1,16 @@
const GoogleAppointmentBlock = () => {
const {GOOGLE_APPOINTMENTS_LINK} = process.env;
console.log(GOOGLE_APPOINTMENTS_LINK)
return (
<div className='w-full h-full flex flex-row justify-center bg-white border-solid'>
<embed
src={GOOGLE_APPOINTMENTS_LINK!}
style={{border: 1}}
width="100%"
height="600"
></embed>
</div>
);
}
export default GoogleAppointmentBlock;

View File

@ -27,10 +27,10 @@ const navigationContent = [
description={contentValues.work.description} description={contentValues.work.description}
innerChildren={ innerChildren={
<div> <div>
<embed style={{height: 2850}} className='w-full mt-10' src={`${resumePdf}#toolbar=0&navpanes=0&scrollbar=0`} type="application/pdf"/>
<ContentCarousel <ContentCarousel
data={portfolioValues} data={portfolioValues}
/> />
<embed style={{height: 2850}} className='w-full mt-10' src={`${resumePdf}#toolbar=0&navpanes=0&scrollbar=0`} type="application/pdf"/>
</div> </div>
} }
/> />

View File

@ -16,12 +16,14 @@
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@types/nodemailer": "^6.4.14", "@types/nodemailer": "^6.4.14",
"@types/react-google-recaptcha": "^2.1.8",
"iframe-resizer-react": "^1.1.0", "iframe-resizer-react": "^1.1.0",
"next": "latest", "next": "latest",
"nodemailer": "^6.9.7", "nodemailer": "^6.9.7",
"pdfjs-dist": "^4.0.269", "pdfjs-dist": "^4.0.269",
"react": "latest", "react": "latest",
"react-dom": "latest", "react-dom": "latest",
"react-google-recaptcha": "^3.1.0",
"react-hook-form": "^7.47.0", "react-hook-form": "^7.47.0",
"react-modal": "^3.16.1", "react-modal": "^3.16.1",
"react-pdf": "^7.4.0", "react-pdf": "^7.4.0",

View File

@ -18,7 +18,7 @@ type MailData = {
html?: string html?: string
} }
export default function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse<Data> res: NextApiResponse<Data>
) { ) {
@ -29,12 +29,20 @@ export default function handler(
lastName, lastName,
subject, subject,
email, email,
summary summary,
captchaToken
} = req.body; } = req.body;
const {SMTP_PROXY_EMAIL, SMTP_RECIPIENT_EMAIL, SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD} = process.env; const {
SMTP_PROXY_EMAIL,
SMTP_RECIPIENT_EMAIL,
SMTP_HOST,
SMTP_PORT,
SMTP_USERNAME,
SMTP_PASSWORD,
RECAPTCHA_SECRET_KEY
} = process.env;
console.log(JSON.stringify({SMTP_PROXY_EMAIL, SMTP_RECIPIENT_EMAIL, SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD}));
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
port: parseInt(SMTP_PORT!), port: parseInt(SMTP_PORT!),
host: SMTP_HOST!, host: SMTP_HOST!,
@ -45,21 +53,45 @@ export default function handler(
secure: true, secure: true,
}); });
const mailData: MailData = { console.log(JSON.stringify({SMTP_PROXY_EMAIL, SMTP_RECIPIENT_EMAIL, SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD}));
from: SMTP_PROXY_EMAIL!,
to: SMTP_RECIPIENT_EMAIL!,
subject: `Message From ${firstName} ${lastName}: ${subject}`,
text: summary + " | Sent from: " + email,
html: `<div>${summary}</div><p>Sent from:
${email}</p>`
}
transporter.sendMail(mailData, function (err, info) {
if(err)
console.log(err)
else
console.log(info)
})
res.status(200).json(req.body); try {
const response = await fetch(
`https://www.google.com/recaptcha/api/siteverify?secret=${RECAPTCHA_SECRET_KEY}&response=${captchaToken}`
);
if ((await response.json()).success) {
//reCaptcha verification successfull
const mailData: MailData = {
from: SMTP_PROXY_EMAIL!,
to: SMTP_RECIPIENT_EMAIL!,
subject: `Message From ${firstName} ${lastName}: ${subject}`,
text: summary + " | Sent from: " + email,
html: `<div>${summary}</div><p>Sent from:
${email}</p>`
}
transporter.sendMail(mailData, function (err, info) {
if(err) {
console.log(err);
res.status(500).send('Internal Server Error');
}
else {
console.log('successful');
console.log(info);
res.status(200).end();
}
})
res.status(200).json(req.body);
} else {
// reCAPTCHA verification failed
res.status(400).send('reCAPTCHA verification failed.');
}
} catch (error) {
console.error(error);
res.status(500).send('Internal server error');
}
} }

View File

@ -8,6 +8,7 @@ import navigationContent from '../lib/navigationContent'
import ContactForm from '@/components/form/ContactForm'; import ContactForm from '@/components/form/ContactForm';
import Footer from '@/components/footer'; import Footer from '@/components/footer';
import Link from 'next/link'; import Link from 'next/link';
import GoogleAppointmentBlock from '@/components/form/GoogleAppointmentBlock';
const inter = Inter({ subsets: ['latin'] }) const inter = Inter({ subsets: ['latin'] })
export default function Home() { export default function Home() {
@ -119,6 +120,7 @@ export default function Home() {
<div className='flex flex-row h-full w-full justify-center'> <div className='flex flex-row h-full w-full justify-center'>
<div className='flex flex-col h-full w-full justify-center'> <div className='flex flex-col h-full w-full justify-center'>
<ContactForm/> <ContactForm/>
{/* <GoogleAppointmentBlock/> */}
</div> </div>
<span className="cursor-pointer self-end" onClick={() => { <span className="cursor-pointer self-end" onClick={() => {
executeScrollToLandiing(); executeScrollToLandiing();

View File

@ -283,6 +283,13 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-google-recaptcha@^2.1.8":
version "2.1.8"
resolved "https://registry.yarnpkg.com/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.8.tgz#a76065be49b22851914b539c32601402fde17bbd"
integrity sha512-nYI3ZDoteZ0g4FYusyKWqz7AZqRdu70R3wDkosCcN0peb2WLn57i0Alm4IPiCRIx59yTUVPTiOELZH08gV1wXA==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@latest": "@types/react@*", "@types/react@latest":
version "18.2.23" version "18.2.23"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.23.tgz#60ad6cf4895e93bed858db0e03bcc4ff97d0410e" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.23.tgz#60ad6cf4895e93bed858db0e03bcc4ff97d0410e"
@ -1529,6 +1536,13 @@ has@^1.0.3:
dependencies: dependencies:
function-bind "^1.1.1" function-bind "^1.1.1"
hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
https-proxy-agent@^5.0.0: https-proxy-agent@^5.0.0:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
@ -2386,7 +2400,7 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: prop-types@^15.5.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -2428,6 +2442,14 @@ rc@^1.2.7:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
react-async-script@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.2.0.tgz#ab9412a26f0b83f5e2e00de1d2befc9400834b21"
integrity sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==
dependencies:
hoist-non-react-statics "^3.3.0"
prop-types "^15.5.0"
react-dom@latest: react-dom@latest:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@ -2436,12 +2458,20 @@ react-dom@latest:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.0"
react-google-recaptcha@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz#44aaab834495d922b9d93d7d7a7fb2326315b4ab"
integrity sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==
dependencies:
prop-types "^15.5.0"
react-async-script "^1.2.0"
react-hook-form@^7.47.0: react-hook-form@^7.47.0:
version "7.47.0" version "7.47.0"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.47.0.tgz#a42f07266bd297ddf1f914f08f4b5f9783262f31" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.47.0.tgz#a42f07266bd297ddf1f914f08f4b5f9783262f31"
integrity sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg== integrity sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==
react-is@^16.13.1: react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==