
- 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.
428 lines
17 KiB
Plaintext
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> |