RTSDA-Website/astro-church-website/src/pages/contact.astro
Benjamin Slingo 756a755ba6 Fix PO BOX display and security vulnerability
- Add getChurchPhysicalAddress and getChurchPoBox functions to church-core
- Update UniFFI interface to expose new functions
- Add NAPI wrappers in astro-church-website for new functions
- Update Footer and contact page to use separate address fields
- Rebuild native bindings with new functions
- Display physical address and PO BOX on separate lines properly
- Fix Astro security vulnerability (GHSA-xf8x-j4p2-f749)

Resolves the missing PO BOX issue that was caused by newline character
handling problems between Rust and JavaScript in production environments.
2025-08-26 17:22:25 -04:00

428 lines
17 KiB
Plaintext

---
import MainLayout from '../layouts/MainLayout.astro';
import { SERVICE_TIMES } from '../lib/constants.js';
import {
getChurchName,
getChurchPhysicalAddress,
getChurchPoBox,
getContactPhone,
getContactEmail
} from '../lib/bindings.js';
let churchName = 'Church';
let physicalAddress = '';
let poBox = '';
let phone = '';
let email = '';
try {
churchName = getChurchName();
physicalAddress = getChurchPhysicalAddress();
poBox = getChurchPoBox();
phone = getContactPhone();
// Get the base email from church-core and make it dynamic based on current domain
const baseEmail = getContactEmail();
const currentUrl = Astro.url.hostname;
// Extract the domain part and create dynamic email
if (currentUrl && currentUrl !== 'localhost') {
email = `admin@${currentUrl}`;
} else {
// Fallback to the original email from church-core
email = baseEmail;
}
} catch (e) {
console.error('Failed to get contact info:', e);
}
---
<MainLayout title={`Contact - ${churchName}`} description="Get in touch with us for questions, prayer requests, or to learn more about our faith">
<!-- Contact Hero -->
<section class="py-16 bg-heavenly-gradient text-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 class="text-4xl lg:text-6xl font-bold mb-6">Contact Us</h1>
<p class="text-xl text-blue-100 max-w-2xl mx-auto">
We'd love to hear from you. Reach out with questions, prayer requests, or to learn more about our faith community.
</p>
</div>
</section>
<!-- Contact Content -->
<section class="py-16 bg-white dark:bg-gray-900">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid lg:grid-cols-2 gap-12">
<!-- Contact Form -->
<div class="bg-gradient-to-br from-gray-50 to-blue-50 dark:from-gray-800 dark:to-gray-700 rounded-2xl p-8">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-6">Send us a message</h2>
<form id="contactForm" class="space-y-6">
<div>
<label for="name" class="block text-sm font-medium text-gray-900 dark:text-white mb-2">Name *</label>
<input
type="text"
id="name"
name="name"
required
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="Your full name"
>
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-900 dark:text-white mb-2">Email *</label>
<input
type="email"
id="email"
name="email"
required
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="your.email@example.com"
>
</div>
<div>
<label for="phone" class="block text-sm font-medium text-gray-900 dark:text-white mb-2">Phone</label>
<input
type="tel"
id="phone"
name="phone"
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="(555) 123-4567"
>
</div>
<div>
<label for="subject" class="block text-sm font-medium text-gray-900 dark:text-white mb-2">Subject *</label>
<select
id="subject"
name="subject"
required
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
>
<option value="">Select a subject</option>
<option value="General Inquiry">General Inquiry</option>
<option value="Prayer Request">Prayer Request</option>
<option value="Bible Study">Bible Study Questions</option>
<option value="Three Angels Message">Three Angels' Message</option>
<option value="Pastoral Care">Pastoral Care</option>
<option value="Events">Events & Activities</option>
<option value="Other">Other</option>
</select>
</div>
<div>
<label for="message" class="block text-sm font-medium text-gray-900 dark:text-white mb-2">Message *</label>
<textarea
id="message"
name="message"
required
rows="5"
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="How can we help you today?"
></textarea>
</div>
<button
type="submit"
class="w-full bg-primary-600 text-white px-8 py-4 rounded-2xl font-semibold hover:bg-primary-700 transition-colors flex items-center justify-center space-x-2"
>
<i data-lucide="send" class="w-5 h-5"></i>
<span>Send Message</span>
</button>
</form>
<div id="formMessage" class="mt-4 hidden"></div>
</div>
<!-- Contact Information -->
<div class="space-y-8">
<div>
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-6">Get in touch</h2>
<p class="text-gray-600 dark:text-gray-300 text-lg leading-relaxed">
Whether you're seeking spiritual guidance, have questions about our faith, or want to join our community, we're here to help.
</p>
</div>
<!-- Contact Details -->
<div class="space-y-6">
{(physicalAddress || poBox) && (
<div class="flex items-start space-x-4">
<div class="w-12 h-12 bg-primary-500 rounded-xl flex items-center justify-center flex-shrink-0">
<i data-lucide="map-pin" class="w-6 h-6 text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Visit Us</h3>
<div class="text-gray-600 dark:text-gray-300">
{physicalAddress && (
<p class="mb-1">{physicalAddress}</p>
)}
{poBox && (
<p class="mb-0">{poBox}</p>
)}
</div>
</div>
</div>
)}
{phone && (
<div class="flex items-start space-x-4">
<div class="w-12 h-12 bg-gold-500 rounded-xl flex items-center justify-center flex-shrink-0">
<i data-lucide="phone" class="w-6 h-6 text-black"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Call Us</h3>
<a href={`tel:${phone}`} class="text-primary-600 dark:text-primary-400 hover:underline">{phone}</a>
</div>
</div>
)}
{email && (
<div class="flex items-start space-x-4">
<div class="w-12 h-12 bg-purple-500 rounded-xl flex items-center justify-center flex-shrink-0">
<i data-lucide="mail" class="w-6 h-6 text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Email Us</h3>
<a href={`mailto:${email}`} class="text-primary-600 dark:text-primary-400 hover:underline">{email}</a>
</div>
</div>
)}
<div class="flex items-start space-x-4">
<div class="w-12 h-12 bg-green-500 rounded-xl flex items-center justify-center flex-shrink-0">
<i data-lucide="clock" class="w-6 h-6 text-white"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">Service Times</h3>
<div class="text-gray-600 dark:text-gray-300 space-y-1">
<p>Sabbath School: {SERVICE_TIMES.SABBATH_SCHOOL}</p>
<p>Divine Service: {SERVICE_TIMES.DIVINE_SERVICE}</p>
<p>Prayer Meeting: {SERVICE_TIMES.PRAYER_MEETING}</p>
</div>
</div>
</div>
</div>
<!-- Quick Links -->
<div class="bg-gradient-to-br from-primary-50 to-blue-50 dark:from-gray-800 dark:to-gray-700 rounded-2xl p-6">
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Quick Links</h3>
<div class="space-y-3">
<a href="/three-angels" class="flex items-center space-x-2 text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 transition-colors">
<i data-lucide="users" class="w-4 h-4"></i>
<span>Learn about the Three Angels' Message</span>
</a>
<a href="/events" class="flex items-center space-x-2 text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 transition-colors">
<i data-lucide="calendar" class="w-4 h-4"></i>
<span>View upcoming events</span>
</a>
<a href="/sermons" class="flex items-center space-x-2 text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 transition-colors">
<i data-lucide="play" class="w-4 h-4"></i>
<span>Listen to recent sermons</span>
</a>
</div>
</div>
</div>
</div>
</div>
</section>
</MainLayout>
<script>
let validationTimeout: number;
// Real-time validation function
async function validateForm() {
const form = document.getElementById('contactForm') as HTMLFormElement;
if (!form) return;
const formData = new FormData(form);
const data = {
name: formData.get('name'),
email: formData.get('email'),
phone: formData.get('phone'),
subject: formData.get('subject'),
message: formData.get('message')
};
try {
const response = await fetch('/api/contact/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
const result = await response.json();
// Clear previous validation messages
document.querySelectorAll('.field-error').forEach(el => el.remove());
document.querySelectorAll('.border-red-500').forEach(el => {
el.classList.remove('border-red-500');
el.classList.add('border-gray-300', 'dark:border-gray-600');
});
if (!result.is_valid && result.errors.length > 0) {
// Show field-specific errors
result.errors.forEach((error: string) => {
let fieldName = '';
if (error.toLowerCase().includes('name')) fieldName = 'name';
else if (error.toLowerCase().includes('email')) fieldName = 'email';
else if (error.toLowerCase().includes('phone')) fieldName = 'phone';
else if (error.toLowerCase().includes('subject')) fieldName = 'subject';
else if (error.toLowerCase().includes('message')) fieldName = 'message';
if (fieldName) {
const field = document.getElementById(fieldName);
if (field) {
field.classList.remove('border-gray-300', 'dark:border-gray-600');
field.classList.add('border-red-500');
const errorDiv = document.createElement('div');
errorDiv.className = 'field-error text-red-600 dark:text-red-400 text-sm mt-1';
errorDiv.textContent = error;
field.parentNode?.appendChild(errorDiv);
}
}
});
}
} catch (error) {
console.error('Validation error:', error);
}
}
// Phone number formatting
function formatPhoneNumber(value: string): string {
// Remove all non-digits
const phoneNumber = value.replace(/\D/g, '');
// Format based on length
if (phoneNumber.length === 10) {
return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3, 6)}-${phoneNumber.slice(6)}`;
} else if (phoneNumber.length === 11 && phoneNumber[0] === '1') {
return `+1 (${phoneNumber.slice(1, 4)}) ${phoneNumber.slice(4, 7)}-${phoneNumber.slice(7)}`;
}
return value; // Return original if not a standard format
}
function unformatPhoneNumber(value: string): string {
return value.replace(/\D/g, '');
}
// Add real-time validation to form fields
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contactForm');
if (!form) return;
const phoneField = document.getElementById('phone') as HTMLInputElement;
let isTyping = false;
let typingTimeout: number;
const fields = ['name', 'email', 'phone', 'subject', 'message'];
fields.forEach(fieldName => {
const field = document.getElementById(fieldName);
if (field) {
field.addEventListener('input', () => {
clearTimeout(validationTimeout);
validationTimeout = setTimeout(validateForm, 500);
});
field.addEventListener('blur', validateForm);
}
});
// Special handling for phone field
if (phoneField) {
phoneField.addEventListener('input', () => {
isTyping = true;
clearTimeout(typingTimeout);
// If user is typing, remove formatting
if (isTyping) {
const unformatted = unformatPhoneNumber(phoneField.value);
if (phoneField.value !== unformatted && phoneField.value.length > unformatted.length) {
phoneField.value = unformatted;
}
}
// Set timeout to format when user stops typing
typingTimeout = setTimeout(() => {
isTyping = false;
if (phoneField.value.trim()) {
phoneField.value = formatPhoneNumber(phoneField.value);
}
}, 1000);
});
phoneField.addEventListener('blur', () => {
isTyping = false;
clearTimeout(typingTimeout);
if (phoneField.value.trim()) {
phoneField.value = formatPhoneNumber(phoneField.value);
}
});
phoneField.addEventListener('focus', () => {
isTyping = true;
clearTimeout(typingTimeout);
});
}
});
document.getElementById('contactForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const messageDiv = document.getElementById('formMessage');
if (!messageDiv) return;
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: formData.get('name'),
email: formData.get('email'),
phone: formData.get('phone'),
subject: formData.get('subject'),
message: formData.get('message')
})
});
const result = await response.json();
if (response.ok && result.success) {
messageDiv.className = 'mt-4 p-4 bg-green-100 dark:bg-green-900 border border-green-400 dark:border-green-600 text-green-700 dark:text-green-300 rounded-xl';
messageDiv.textContent = 'Thank you! Your message has been sent successfully.';
(e.target as HTMLFormElement).reset();
// Clear validation errors on successful submit
document.querySelectorAll('.field-error').forEach(el => el.remove());
document.querySelectorAll('.border-red-500').forEach(el => {
el.classList.remove('border-red-500');
el.classList.add('border-gray-300', 'dark:border-gray-600');
});
} else {
messageDiv.className = 'mt-4 p-4 bg-red-100 dark:bg-red-900 border border-red-400 dark:border-red-600 text-red-700 dark:text-red-300 rounded-xl';
messageDiv.textContent = result.error || result.message || 'There was an error sending your message. Please try again.';
}
messageDiv.classList.remove('hidden');
setTimeout(() => {
messageDiv.classList.add('hidden');
}, 5000);
} catch (error) {
messageDiv.className = 'mt-4 p-4 bg-red-100 dark:bg-red-900 border border-red-400 dark:border-red-600 text-red-700 dark:text-red-300 rounded-xl';
messageDiv.textContent = 'Network error. Please check your connection and try again.';
messageDiv.classList.remove('hidden');
}
});
</script>