updated contact form logic
This commit is contained in:
@@ -465,12 +465,22 @@ import { UI } from 'astrowind:config';
|
||||
|
||||
// Fetch CSRF token from the server
|
||||
async function fetchCsrfToken() {
|
||||
console.log('Fetching CSRF token');
|
||||
try {
|
||||
const response = await fetch('/api/contact?csrf=true');
|
||||
console.log('CSRF response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('CSRF request failed:', response.statusText);
|
||||
return '';
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('CSRF token received:', data.csrfToken ? 'yes' : 'no');
|
||||
return data.csrfToken;
|
||||
} catch (error) {
|
||||
console.error('Error fetching CSRF token:', error);
|
||||
console.error('Error details:', error.message);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -489,15 +499,19 @@ import { UI } from 'astrowind:config';
|
||||
// Form validation and submission
|
||||
contactForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
console.log('Form submitted');
|
||||
|
||||
// Reset previous error messages
|
||||
resetFormErrors();
|
||||
|
||||
// Client-side validation
|
||||
if (!validateForm(contactForm)) {
|
||||
console.log('Form validation failed');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Form validation passed');
|
||||
|
||||
// Show loading state
|
||||
const submitButton = contactForm.querySelector('button[type="submit"]');
|
||||
const originalButtonText = submitButton.innerHTML;
|
||||
@@ -507,9 +521,16 @@ import { UI } from 'astrowind:config';
|
||||
try {
|
||||
const formData = new FormData(contactForm);
|
||||
|
||||
// Log form data
|
||||
console.log('Form data:');
|
||||
for (const [key, value] of formData.entries()) {
|
||||
console.log(`${key}: ${value}`);
|
||||
}
|
||||
|
||||
// Add timestamp to help prevent duplicate submissions
|
||||
formData.append('timestamp', Date.now().toString());
|
||||
|
||||
console.log('Sending form data to /api/contact');
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
@@ -518,23 +539,40 @@ import { UI } from 'astrowind:config';
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
const result = await response.json();
|
||||
console.log('Response data:', result);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Form submission successful');
|
||||
// Show success message
|
||||
document.getElementById('form-success').classList.remove('hidden');
|
||||
contactForm.reset();
|
||||
|
||||
// Get a new CSRF token for next submission
|
||||
console.log('Getting new CSRF token');
|
||||
const newToken = await fetchCsrfToken();
|
||||
console.log('New CSRF token:', newToken ? 'received' : 'not received');
|
||||
if (csrfTokenInput) {
|
||||
csrfTokenInput.value = newToken;
|
||||
}
|
||||
} else {
|
||||
// Show error messages
|
||||
document.getElementById('form-error').classList.remove('hidden');
|
||||
|
||||
console.log('Form submission failed:', result);
|
||||
// Show specific error messages instead of generic form error
|
||||
if (result.errors) {
|
||||
console.log('Form errors:', result.errors);
|
||||
|
||||
// Only show the generic error if there are no field-specific errors
|
||||
// or if there's a server error not related to a specific field
|
||||
const hasFieldErrors = Object.keys(result.errors).some(field =>
|
||||
['name', 'email', 'message', 'disclaimer', 'csrf'].includes(field)
|
||||
);
|
||||
|
||||
if (!hasFieldErrors) {
|
||||
document.getElementById('form-error').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Display field-specific errors
|
||||
Object.keys(result.errors).forEach(field => {
|
||||
const inputElement = contactForm.querySelector(`[name="${field}"]`);
|
||||
if (inputElement) {
|
||||
@@ -543,13 +581,25 @@ import { UI } from 'astrowind:config';
|
||||
feedbackElement.textContent = result.errors[field];
|
||||
feedbackElement.classList.remove('hidden');
|
||||
inputElement.classList.add('border-red-500');
|
||||
|
||||
// Special handling for checkbox
|
||||
if (field === 'disclaimer') {
|
||||
const checkboxContainer = inputElement.closest('.flex.items-start');
|
||||
if (checkboxContainer) {
|
||||
checkboxContainer.classList.add('checkbox-error');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If no specific errors, show the generic error
|
||||
document.getElementById('form-error').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// If CSRF token is invalid, get a new one
|
||||
if (result.errors && result.errors.csrf) {
|
||||
console.log('CSRF token invalid, getting new token');
|
||||
const newToken = await fetchCsrfToken();
|
||||
if (csrfTokenInput) {
|
||||
csrfTokenInput.value = newToken;
|
||||
@@ -558,6 +608,7 @@ import { UI } from 'astrowind:config';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
console.error('Error details:', error.message);
|
||||
document.getElementById('form-error').classList.remove('hidden');
|
||||
} finally {
|
||||
// Restore button state
|
||||
@@ -608,6 +659,30 @@ import { UI } from 'astrowind:config';
|
||||
}
|
||||
input.classList.remove('border-red-500');
|
||||
|
||||
// Checkbox validation (special case for disclaimer)
|
||||
if (input.type === 'checkbox') {
|
||||
if (input.required && !input.checked) {
|
||||
isValid = false;
|
||||
if (feedbackElement) {
|
||||
feedbackElement.textContent = 'Please check the required consent box before submitting';
|
||||
feedbackElement.classList.remove('hidden');
|
||||
}
|
||||
input.classList.add('border-red-500');
|
||||
// Add red border to the checkbox container for better visibility
|
||||
const checkboxContainer = input.closest('.flex.items-start');
|
||||
if (checkboxContainer) {
|
||||
checkboxContainer.classList.add('checkbox-error');
|
||||
}
|
||||
} else {
|
||||
// Remove error styling when checkbox is checked
|
||||
const checkboxContainer = input.closest('.flex.items-start');
|
||||
if (checkboxContainer) {
|
||||
checkboxContainer.classList.remove('checkbox-error');
|
||||
}
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
// Check if empty
|
||||
if (input.required && !input.value.trim()) {
|
||||
isValid = false;
|
||||
@@ -655,6 +730,11 @@ import { UI } from 'astrowind:config';
|
||||
input.classList.remove('border-red-500');
|
||||
});
|
||||
|
||||
// Remove checkbox container error styling
|
||||
document.querySelectorAll('.flex.items-start').forEach(container => {
|
||||
container.classList.remove('checkbox-error');
|
||||
});
|
||||
|
||||
// Hide form-level messages
|
||||
document.getElementById('form-success')?.classList.add('hidden');
|
||||
document.getElementById('form-error')?.classList.add('hidden');
|
||||
|
@@ -5,6 +5,16 @@ import Button from '~/components/ui/Button.astro';
|
||||
const { inputs, textarea, disclaimer, button = 'Contact us', description = '' } = Astro.props;
|
||||
---
|
||||
|
||||
<style>
|
||||
/* Checkbox error styling */
|
||||
.checkbox-error {
|
||||
border: 2px solid #ef4444;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background-color: rgba(239, 68, 68, 0.05);
|
||||
}
|
||||
</style>
|
||||
|
||||
<form id="contact-form" action="/api/contact" method="POST" class="needs-validation" novalidate>
|
||||
|
||||
<!-- Form status messages -->
|
||||
@@ -13,7 +23,7 @@ const { inputs, textarea, disclaimer, button = 'Contact us', description = '' }
|
||||
</div>
|
||||
|
||||
<div id="form-error" class="hidden mb-6 p-4 bg-red-100 border border-red-200 text-red-700 rounded-lg">
|
||||
There was an error sending your message. Please try again.
|
||||
There was an error sending your message. Please check all fields and try again.
|
||||
</div>
|
||||
|
||||
<!-- CSRF Token - Will be populated via JavaScript -->
|
||||
|
@@ -8,7 +8,7 @@ interface AdminNotificationProps {
|
||||
}
|
||||
|
||||
export function getAdminNotificationSubject(): string {
|
||||
return 'New Contact Form Submission from bergsma.it';
|
||||
return `New Contact Form Submission from ${process.env.WEBSITE_NAME || '365devnet.eu'}`;
|
||||
}
|
||||
|
||||
export function getAdminNotificationHtml(props: AdminNotificationProps): string {
|
||||
|
@@ -7,17 +7,17 @@ interface UserConfirmationProps {
|
||||
contactEmail?: string;
|
||||
}
|
||||
|
||||
export function getUserConfirmationSubject(websiteName: string = 'bergsma.it'): string {
|
||||
export function getUserConfirmationSubject(websiteName: string = process.env.WEBSITE_NAME || '365devnet.eu'): string {
|
||||
return `Thank you for contacting ${websiteName}`;
|
||||
}
|
||||
|
||||
export function getUserConfirmationHtml(props: UserConfirmationProps): string {
|
||||
const {
|
||||
name,
|
||||
message,
|
||||
submittedAt,
|
||||
websiteName = 'bergsma.it',
|
||||
contactEmail = 'richard@bergsma.it'
|
||||
const {
|
||||
name,
|
||||
message,
|
||||
submittedAt,
|
||||
websiteName = process.env.WEBSITE_NAME || '365devnet.eu',
|
||||
contactEmail = process.env.ADMIN_EMAIL || 'richard@bergsma.it'
|
||||
} = props;
|
||||
|
||||
return `
|
||||
@@ -92,12 +92,12 @@ export function getUserConfirmationHtml(props: UserConfirmationProps): string {
|
||||
}
|
||||
|
||||
export function getUserConfirmationText(props: UserConfirmationProps): string {
|
||||
const {
|
||||
name,
|
||||
message,
|
||||
submittedAt,
|
||||
websiteName = 'bergsma.it',
|
||||
contactEmail = 'richard@bergsma.it'
|
||||
const {
|
||||
name,
|
||||
message,
|
||||
submittedAt,
|
||||
websiteName = process.env.WEBSITE_NAME || '365devnet.eu',
|
||||
contactEmail = process.env.ADMIN_EMAIL || 'richard@bergsma.it'
|
||||
} = props;
|
||||
|
||||
return `
|
||||
|
@@ -162,20 +162,26 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
errors.csrf = 'Invalid or expired security token. Please refresh the page and try again.';
|
||||
}
|
||||
|
||||
if (!name || name.length < 2) {
|
||||
errors.name = 'Name is required and must be at least 2 characters';
|
||||
if (!name) {
|
||||
errors.name = 'Please enter your name';
|
||||
} else if (name.length < 2) {
|
||||
errors.name = 'Your name must be at least 2 characters long';
|
||||
}
|
||||
|
||||
if (!email || !isValidEmail(email)) {
|
||||
errors.email = 'A valid email address is required';
|
||||
if (!email) {
|
||||
errors.email = 'Please enter your email address';
|
||||
} else if (!isValidEmail(email)) {
|
||||
errors.email = 'Please enter a valid email address (e.g., name@example.com)';
|
||||
}
|
||||
|
||||
if (!message || message.length < 10) {
|
||||
errors.message = 'Message is required and must be at least 10 characters';
|
||||
if (!message) {
|
||||
errors.message = 'Please enter your message';
|
||||
} else if (message.length < 10) {
|
||||
errors.message = 'Your message must be at least 10 characters long';
|
||||
}
|
||||
|
||||
if (!disclaimer) {
|
||||
errors.disclaimer = 'You must agree to the disclaimer';
|
||||
errors.disclaimer = 'Please check the required consent box before submitting';
|
||||
}
|
||||
|
||||
// Check for spam
|
||||
|
@@ -13,11 +13,12 @@ const {
|
||||
SMTP_PASS = '',
|
||||
ADMIN_EMAIL = 'richard@bergsma.it',
|
||||
WEBSITE_NAME = 'bergsma.it',
|
||||
NODE_ENV = 'development'
|
||||
NODE_ENV = 'production'
|
||||
} = process.env;
|
||||
|
||||
// Email configuration
|
||||
const isProduction = NODE_ENV === 'production';
|
||||
// Force production mode for testing
|
||||
const isProduction = true; // NODE_ENV === 'production';
|
||||
|
||||
// Create a transporter for sending emails
|
||||
let transporter: nodemailer.Transporter;
|
||||
@@ -31,6 +32,15 @@ function initializeTransporter() {
|
||||
// ProtonMail often requires using their Bridge application for SMTP
|
||||
const isProtonMail = SMTP_HOST.includes('protonmail');
|
||||
|
||||
// Log the email configuration
|
||||
console.log('Initializing email transporter with:');
|
||||
console.log(`SMTP Host: ${SMTP_HOST}`);
|
||||
console.log(`SMTP Port: ${SMTP_PORT}`);
|
||||
console.log(`SMTP User: ${SMTP_USER}`);
|
||||
console.log(`Admin Email: ${ADMIN_EMAIL}`);
|
||||
console.log(`Website Name: ${WEBSITE_NAME}`);
|
||||
console.log(`Environment: ${NODE_ENV}`);
|
||||
|
||||
transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: parseInt(SMTP_PORT, 10),
|
||||
@@ -179,8 +189,13 @@ export async function sendEmail(
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure from address matches SMTP_USER for ProtonMail
|
||||
const fromAddress = isProduction ?
|
||||
`"${WEBSITE_NAME}" <${SMTP_USER}>` :
|
||||
`"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`;
|
||||
|
||||
const mailOptions = {
|
||||
from: `"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`,
|
||||
from: fromAddress,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
@@ -188,7 +203,7 @@ export async function sendEmail(
|
||||
};
|
||||
|
||||
console.log('Mail options:', {
|
||||
from: `"${WEBSITE_NAME}" <${ADMIN_EMAIL}>`,
|
||||
from: fromAddress,
|
||||
to,
|
||||
subject,
|
||||
textLength: text.length,
|
||||
@@ -199,6 +214,15 @@ export async function sendEmail(
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
console.log('Email sent, info:', info.messageId);
|
||||
|
||||
// Log additional information in production mode
|
||||
if (isProduction) {
|
||||
console.log('Email delivery details:', {
|
||||
messageId: info.messageId,
|
||||
response: info.response,
|
||||
envelope: info.envelope
|
||||
});
|
||||
}
|
||||
|
||||
if (!isProduction) {
|
||||
// In development, log the email content
|
||||
console.log('Email sent (development mode):');
|
||||
|
Reference in New Issue
Block a user