portfolio-page/src/components/sections/ContactSection.tsx

371 lines
13 KiB
TypeScript

import { Mail, Github, Linkedin } from 'lucide-react';
import { useState } from 'react';
import type { FormEvent } from 'react';
import { personalConfig, getObfuscatedEmail, createEmailLink } from '../../config/personal';
import { getApiUrl, ENDPOINTS } from '../../config/api';
interface ContactSectionProps {
title?: string;
subtitle?: string;
email?: string;
github?: string;
linkedin?: string;
connectTitle?: string;
connectDescription?: string;
}
// TODO Create Backend API for handling form submissions securely
export default function ContactSection({
title = "Get In Touch",
subtitle = "Let's discuss your next project or collaboration opportunity",
github = personalConfig.social.github.username,
connectTitle = "Let's Connect",
connectDescription = "I'm always interested in new opportunities and exciting projects. Whether you have a question or just want to say hi, feel free to reach out!"
}: ContactSectionProps) {
// Anti-spam honeypot state
const [honeypot, setHoneypot] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [formStatus, setFormStatus] = useState<{
type: 'success' | 'error' | null;
message: string;
}>({ type: null, message: '' });
// Email obfuscation function using config
const handleEmailClick = (e: React.MouseEvent) => {
e.preventDefault();
window.location.href = createEmailLink();
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Check honeypot - if filled, it's likely spam
if (honeypot) {
console.warn('Spam detected - honeypot field was filled');
setFormStatus({
type: 'error',
message: 'Invalid form submission detected.'
});
setIsSubmitting(false);
return;
}
// Prevent rapid submissions
if (isSubmitting) {
return;
}
setIsSubmitting(true);
setFormStatus({ type: null, message: '' }); // Clear previous status
// Get form data
const formData = new FormData(e.currentTarget);
const data = {
firstName: formData.get('firstName') as string,
lastName: formData.get('lastName') as string,
email: formData.get('email') as string,
subject: formData.get('subject') as string,
message: formData.get('message') as string,
};
// Basic validation
if (!data.firstName || !data.lastName || !data.email || !data.message) {
setFormStatus({
type: 'error',
message: 'Please fill in all required fields.'
});
setIsSubmitting(false);
return;
}
// Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
setFormStatus({
type: 'error',
message: 'Please enter a valid email address.'
});
setIsSubmitting(false);
return;
}
try {
// Submit to your own backend API
const apiUrl = `${getApiUrl()}${ENDPOINTS.contact}`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
firstName: data.firstName,
lastName: data.lastName,
email: data.email,
subject: data.subject || 'Contact from Portfolio',
message: data.message,
website: honeypot, // Include honeypot for spam detection
}),
});
const result = await response.json();
if (result.success) {
setFormStatus({
type: 'success',
message: result.message
});
e.currentTarget.reset();
setHoneypot(''); // Reset honeypot
// Clear success message after 5 seconds
setTimeout(() => {
setFormStatus({ type: null, message: '' });
}, 5000);
} else {
// Handle different types of server errors
let errorMessage = 'Form submission failed';
if (response.status === 400) {
if (result.errors && Array.isArray(result.errors)) {
// Validation errors from server
const validationErrors = result.errors.map((err: any) =>
typeof err === 'object' ? err.message : err
).join(', ');
errorMessage = `Validation errors: ${validationErrors}`;
} else if (result.message) {
errorMessage = result.message;
} else {
errorMessage = 'Please check your form data and try again.';
}
} else if (response.status === 429) {
errorMessage = 'Too many requests. Please wait a moment and try again.';
} else if (response.status === 500) {
errorMessage = 'Server error. Our team has been notified. Please try again later.';
} else if (result.message) {
errorMessage = result.message;
}
throw new Error(errorMessage);
}
} catch (error) {
console.error('Form submission error:', error);
let userMessage = 'Sorry, there was an error sending your message.';
if (error instanceof Error) {
// Network errors
if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
userMessage = 'Network error: Please check your internet connection and try again.';
}
// Timeout errors
else if (error.message.includes('timeout')) {
userMessage = 'Request timeout: The server is taking too long to respond. Please try again.';
}
// CORS errors
else if (error.message.includes('CORS')) {
userMessage = 'Connection error: Please refresh the page and try again.';
}
// Server validation errors or custom error messages
else if (error.message.length > 0 && !error.message.includes('Form submission failed')) {
userMessage = error.message;
}
// Generic fallback
else {
userMessage = 'Unexpected error occurred. Please try again or contact us directly.';
}
}
setFormStatus({
type: 'error',
message: userMessage
});
}
setIsSubmitting(false);
};
return (
<section id="contact" className="contact-section">
<div className="contact-section__container">
{/* Section Header */}
<div className="contact-section__header">
<h2 className="contact-section__title">
{title}
</h2>
<p className="contact-section__subtitle">
{subtitle}
</p>
</div>
{/* Contact Content Grid */}
<div className="contact-section__grid">
{/* Contact Info */}
<div className="contact-section__info">
<div className="contact-section__connect">
<h3 className="contact-section__connect-title">{connectTitle}</h3>
<p className="contact-section__connect-description">
{connectDescription}
</p>
</div>
<div className="contact-section__details">
<div className="contact-section__detail-item">
<Mail className="contact-section__detail-icon" />
<span className="contact-section__detail-text">{getObfuscatedEmail()}</span>
</div>
<div className="contact-section__detail-item">
<Github className="contact-section__detail-icon" />
<span className="contact-section__detail-text">{github}</span>
</div>
<div className="contact-section__detail-item">
<Linkedin className="contact-section__detail-icon" />
<span className="contact-section__detail-text">{personalConfig.social.linkedin.username}</span>
</div>
</div>
<div className="contact-section__social">
<button
className="contact-section__social-button"
onClick={() => window.open(personalConfig.social.github.url, '_blank')}
aria-label="GitHub Profile"
>
<Github className="contact-section__social-icon" />
</button>
<button
className="contact-section__social-button"
onClick={() => window.open(personalConfig.social.linkedin.url, '_blank')}
aria-label="LinkedIn Profile"
>
<Linkedin className="contact-section__social-icon" />
</button>
<button
className="contact-section__social-button"
onClick={handleEmailClick}
aria-label="Send Email"
>
<Mail className="contact-section__social-icon" />
</button>
</div>
</div>
{/* Contact Form */}
<div className="contact-section__form-card">
<div className="contact-section__form-header">
<h3 className="contact-section__form-title">Send a Message</h3>
<p className="contact-section__form-description">
I'll get back to you as soon as possible
</p>
</div>
{/* Status Message Display */}
{formStatus.type && (
<div className={`contact-section__status contact-section__status--${formStatus.type}`}>
<p className="contact-section__status-message">
{formStatus.message}
</p>
</div>
)}
<form className="contact-section__form" onSubmit={handleSubmit}>
{/* Honeypot field - hidden from users but visible to bots */}
<div className="contact-section__honeypot">
<label htmlFor="website" className="contact-section__honeypot-label">
Website (leave blank)
</label>
<input
id="website"
name="website"
type="text"
value={honeypot}
onChange={(e) => setHoneypot(e.target.value)}
className="contact-section__honeypot-input"
tabIndex={-1}
autoComplete="off"
/>
</div>
<div className="contact-section__form-row">
<div className="contact-section__form-group">
<label htmlFor="firstName" className="contact-section__form-label">
First Name *
</label>
<input
id="firstName"
name="firstName"
type="text"
placeholder="John"
className="contact-section__form-input"
required
/>
</div>
<div className="contact-section__form-group">
<label htmlFor="lastName" className="contact-section__form-label">
Last Name *
</label>
<input
id="lastName"
name="lastName"
type="text"
placeholder="Doe"
className="contact-section__form-input"
required
/>
</div>
</div>
<div className="contact-section__form-group">
<label htmlFor="email" className="contact-section__form-label">
Email *
</label>
<input
id="email"
name="email"
type="email"
placeholder="john@example.com"
className="contact-section__form-input"
required
/>
</div>
<div className="contact-section__form-group">
<label htmlFor="subject" className="contact-section__form-label">
Subject
</label>
<input
id="subject"
name="subject"
type="text"
placeholder="Project Inquiry"
className="contact-section__form-input"
/>
</div>
<div className="contact-section__form-group">
<label htmlFor="message" className="contact-section__form-label">
Message *
</label>
<textarea
id="message"
name="message"
placeholder="Tell me about your project..."
rows={5}
className="contact-section__form-textarea"
required
></textarea>
</div>
<button
type="submit"
className="contact-section__form-button"
disabled={isSubmitting}
>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
</div>
</div>
</div>
</section>
);
}