Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- Added Zod validation schemas for newsletter and contact forms
- Added Faith to 'about us'
- Updated Mariana's title in 'about us'
- Updated outdated dependencies
Expand All @@ -157,16 +158,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Fixed

- Fixed next-pwa import syntax for v2.x compatibility
- Updated husky script to avoid warning
- Resolved incorrect meta tag rendering for nested routes
- Prevent horizontal page scroll caused by overflowing long titles
- Fixed hero images' max-height to align with WDP mockup
- Fixed contact us form position to maintain structure on bigger displays
- Fixed non-interactive form input field bug on Contact Us page
- Bumped Next.js from v15.3.2 to v15.3.8 to fix React server component's vulnerability
- Fixed XSS vulnerability and added client-side email format validation in NewsletterForm
- Escaped user inputs to prevent HTML injection
- Strengthened server-side validation to block malformed inputs before reCAPTCHA

### Changed

- Replaced manual server-side validation in validateReCaptcha with shared Zod schemas
- Migrating styles from Styled Components to CSS Modules
- ContactUsCards
- ContactUsForm
Expand Down
82 changes: 21 additions & 61 deletions components/ContactUs/ContactUsForm/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import Container from '@/components/containers/Container';
import RevealContentContainer from '@/components/containers/RevealContentContainer';
import { SubmitButton } from '@/components/buttons/SubmitButton';
import { contactSchema } from '@/utils/schemas/contact';
import styles from './ContactUsForm.module.scss';

function ContactUsForm({ subject, setResponseMessage, getReCaptchaToken }) {
Expand All @@ -11,11 +13,12 @@ function ContactUsForm({ subject, setResponseMessage, getReCaptchaToken }) {
reset,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(contactSchema),
defaultValues: {
Name: '',
Email: '',
Subject: subject || '',
Message: '',
name: '',
email: '',
subject: subject || '',
message: '',
},
});

Expand All @@ -33,11 +36,11 @@ function ContactUsForm({ subject, setResponseMessage, getReCaptchaToken }) {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: data.Name,
email: data.Email,
subject: data.Subject,
message: data.Message,
subscribe: data.Subscribe,
name: data.name,
email: data.email,
subject: data.subject,
message: data.message,
subscribe: data.subscribe,
gReCaptchaToken,
}),
});
Expand Down Expand Up @@ -65,78 +68,35 @@ function ContactUsForm({ subject, setResponseMessage, getReCaptchaToken }) {
className={styles.input}
type='text'
placeholder='name'
{...register('Name', {
required: true,
minLength: 2,
maxLength: 80,
//no white space pattern
pattern: /[^\s-]/i,
})}
{...register('name')}
/>
<p className={styles['error-msg']}>
{errors.Name?.type === 'required'
? 'Name is required'
: errors.Name?.type === 'pattern'
? 'No whitespace'
: errors.Name?.type === 'minLength'
? 'Must be more than 1 character'
: undefined}
</p>
<p className={styles['error-msg']}>{errors.name?.message}</p>
<input
className={styles.input}
type='email'
placeholder='email'
{...register('Email', {
required: true,
pattern: /^\S+@\S+$/i,
})}
{...register('email')}
/>
<p className={styles['error-msg']}>
{errors.Email?.type === 'required' && 'Email is required'}
</p>
<p className={styles['error-msg']}>{errors.email?.message}</p>
<input
className={styles.input}
type='text'
placeholder='subject'
{...register('Subject', {
required: true,
minLength: 2,
pattern: /[^\s-]/i,
})}
{...register('subject')}
/>
<p className={styles['error-msg']}>
{errors.Subject?.type === 'required'
? 'Subject is required'
: errors.Subject?.type === 'pattern'
? 'No whitespace'
: errors.Subject?.type === 'minLength'
? 'Must be more than 1 character'
: undefined}
</p>
<p className={styles['error-msg']}>{errors.subject?.message}</p>
<textarea
className={styles.textarea}
{...register('Message', {
required: true,
minLength: 2,
pattern: /[^\s-]/i,
})}
{...register('message')}
placeholder='Write your message here'
/>
<p className={styles['error-msg']}>
{errors.Message?.type === 'required'
? 'Message is required'
: errors.Message?.type === 'pattern'
? 'No whitespace'
: errors.Message?.type === 'minLength'
? 'Must be more than 1 character'
: undefined}
</p>
<p className={styles['error-msg']}>{errors.message?.message}</p>
<label className={styles['subscribe-wrapper']}>
<input
className={styles['subscribe-input']}
type='checkbox'
placeholder='Subscribe to our DevNews!'
{...register('Subscribe', {})}
{...register('subscribe')}
/>
Subscribe to our DevNews!
</label>
Expand Down
32 changes: 12 additions & 20 deletions components/NewsletterSubscribe/NewsletterForm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { decode } from 'html-entities';
import { NewsLetterSubmitButton } from '@/components/buttons/SubmitButton';
import styles from './NewsletterForm.module.scss';
import Container from '@/components/containers/Container';
import { newsletterSchema } from '@/utils/schemas/newsletter';

const NewsletterForm = ({ getReCaptchaToken }) => {
const [error, setError] = useState(null);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to have: consider using RHF's register/reset here like ContactUsForm does, but out of scope for this PR — approving as-is.

Expand Down Expand Up @@ -61,13 +62,9 @@ const NewsletterForm = ({ getReCaptchaToken }) => {

setError(null);

if (!name) {
setError('Please enter a name');
return null;
}

if (!email) {
setError('Please enter a valid email address');
const result = newsletterSchema.safeParse({ name, email });
if (!result.success) {
setError(result.error.issues[0].message);
return null;
}

Expand Down Expand Up @@ -152,19 +149,14 @@ const NewsletterForm = ({ getReCaptchaToken }) => {
{status === 'sending' && (
<div className={styles.formSending}>Sending...</div>
)}
{status === 'error' || error ? (
<div
className={styles.formError}
dangerouslySetInnerHTML={{
__html: error || getMessage(message),
}}
/>
) : null}
{status === 'success' && status !== 'error' && !error && (
<div
className={styles.formSuccess}
dangerouslySetInnerHTML={{ __html: decode(message) }}
/>
{(status === 'error' || error) && (
<div className={styles.formError}>
{error || getMessage(message)}
</div>
)}

{status === 'success' && !error && (
<div className={styles.formSuccess}>{decode(message)}</div>
)}
Comment thread
shayla-develops-webs marked this conversation as resolved.
</div>
</div>
Expand Down
9 changes: 4 additions & 5 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],
"@/styles/*": ["styles/*"],
"@/hooks/*": ["hooks/*"],
"@/utils/*": ["utils/*"]
"@/components/*": ["./components/*"],
"@/styles/*": ["./styles/*"],
"@/hooks/*": ["./hooks/*"],
"@/utils/*": ["./utils/*"]
},
"types": []
}
Expand Down
13 changes: 7 additions & 6 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
});
const withPWA = require('next-pwa');

module.exports = withPWA({
pwa: {
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
},
compiler: {
styledComponents: {
ssr: true,
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,19 @@
]
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@sendgrid/mail": "^8.1.5",
"html-entities": "^2.3.2",
"next": "15.3.8",
"next-pwa": "^5.6.0",
"next": "^15.5.14",
"next-pwa": "^2.0.2",
"node-mailjet": "^6.0.9",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-google-recaptcha": "^3.1.0",
"react-hook-form": "^7.35.0",
"sass": "^1.35.1",
"swiper": "^11.2.6"
"swiper": "^12.1.3",
"zod": "^4.3.6"
},
"devDependencies": {
"husky": "^9.1.7",
Expand Down
16 changes: 11 additions & 5 deletions pages/api/sendEmail.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Sends email to hello@webdevpath.co when user submit the form in "Contact Us" page

import { Client } from 'node-mailjet';
import { encode } from 'html-entities';

const mailjet = new Client({
apiKey: process.env.MAILJET_API_KEY,
Expand All @@ -17,6 +18,11 @@ export default async (email, name, subject, message, subscribe) => {
const mailjetEmail = 'support@webdevpath.co';

try {
const safeName = encode(name);
const safeEmail = encode(email);
const safeSubject = encode(subject);
const safeMessage = encode(message);

const data = {
Messages: [
{
Expand All @@ -29,12 +35,12 @@ export default async (email, name, subject, message, subscribe) => {
Email: receiverEmail,
},
],
Subject: `New message from ${name} via webdevpath.co 'Contact Us' Form`,
Subject: `New message from ${safeName} via webdevpath.co 'Contact Us' Form`,
HTMLPart: `
<b>Name:</b> ${name} <br/>
<b>Email:</b> <a href='mailto:${email}'>${email}</a><br/><br/>
<u><b>Subject:</b> ${subject}</u><br/>
<b>Message:</b> ${message} <br/>
<b>Name:</b> ${safeName} <br/>
<b>Email:</b> <a href='mailto:${safeEmail}'>${safeEmail}</a><br/><br/>
<u><b>Subject:</b> ${safeSubject}</u><br/>
<b>Message:</b> ${safeMessage} <br/>
<b>Subscribe?:</b> ${subscribe ? 'Yes' : 'No'}
`,
},
Expand Down
18 changes: 16 additions & 2 deletions pages/api/validateReCaptcha.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import sendEmail from './sendEmail.js';
import { subscribeToMailchimp } from '../../lib/mailchimp';
import { contactSchema } from '../../utils/schemas/contact';
import { newsletterSchema } from '../../utils/schemas/newsletter';

export default async function handler(req, res) {
const { method } = req;
Expand All @@ -12,13 +14,25 @@ export default async function handler(req, res) {
const { name, email, subject, message, subscribe, gReCaptchaToken } =
req.body;

// If email or captcha are missing return an error
if (!email || !name || !gReCaptchaToken) {
if (!gReCaptchaToken) {
return res.status(422).json({
message: 'Unprocessable request, please provide the required fields',
});
}

const isContactForm = subject || message;
const schema = isContactForm ? contactSchema : newsletterSchema;
const parseData = isContactForm
? { name, email, subject, message, subscribe }
: { name, email };

const validationResult = schema.safeParse(parseData);
if (!validationResult.success) {
return res.status(422).json({
message: validationResult.error.issues[0].message,
});
}

try {
// Ping the google recaptcha verify API to verify the captcha code you received
const response = await fetch(
Expand Down
11 changes: 11 additions & 0 deletions utils/schemas/contact.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { z } from 'zod';
import { newsletterSchema } from './newsletter';

export const contactSchema = newsletterSchema.extend({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My personal choice.

I feel better to use z.object instead of newsletterSchema.extend as we can see all the fields in one file.

subject: z.string().min(2, 'Subject must be at least 2 characters'),
message: z
.string()
.min(2, 'Message must be at least 2 characters')
.max(5000, 'Message must be 5000 characters or less'),
subscribe: z.boolean().optional(),
});
9 changes: 9 additions & 0 deletions utils/schemas/newsletter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from 'zod';

export const newsletterSchema = z.object({
name: z
.string()
.min(2, 'Name must be at least 2 characters')
.max(80, 'Name must be 80 characters or less'),
email: z.string().email('Please enter a valid email address'),
});
Loading