Files
egfh-website/src/components/ContactForm.astro
T
2026-06-26 07:38:46 -04:00

315 lines
8.7 KiB
Plaintext

---
const configuredApiBase = import.meta.env.PUBLIC_PPR_API_BASE ?? 'https://ppr.swansea-airport.wales/api/v1';
const pprApiBase = configuredApiBase.replace(/\/$/, '');
const contactRequestEndpoint = `${pprApiBase}/contact-requests/public`;
---
<section class="contact-shell surface" aria-labelledby="contact-form-heading">
<div class="contact-head">
<p class="eyebrow">General enquiries</p>
<h2 id="contact-form-heading" class="section-title">Send us a message</h2>
<p class="section-copy">
Use this form for general airport, business, visiting, and community enquiries. For flight
requests, please use the dedicated PPR or drone request forms.
</p>
</div>
<form id="contact-form" class="contact-form">
<div class="contact-honeypot" aria-hidden="true">
<label for="website">Website</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off" />
</div>
<div class="contact-grid">
<div class="contact-field">
<label for="contact-name">Name <span aria-hidden="true">*</span></label>
<input type="text" id="contact-name" name="name" autocomplete="name" required />
</div>
<div class="contact-field">
<label for="contact-email">Email Address <span aria-hidden="true">*</span></label>
<input type="email" id="contact-email" name="email" autocomplete="email" required />
</div>
<div class="contact-field">
<label for="contact-phone">Phone Number</label>
<input type="tel" id="contact-phone" name="phone" autocomplete="tel" />
</div>
<div class="contact-field">
<label for="contact-enquiry-type">Enquiry Type <span aria-hidden="true">*</span></label>
<select id="contact-enquiry-type" name="enquiry_type" required>
<option value="">Select enquiry type</option>
<option value="general">General enquiry</option>
<option value="pilot">Pilot or visiting aircraft / Fuel</option>
<option value="aviation_business">Aviation business / basing</option>
<option value="events">Events and visits</option>
<option value="community">Community or local resident</option>
</select>
</div>
<div class="contact-field contact-full">
<label for="contact-subject">Subject <span aria-hidden="true">*</span></label>
<input type="text" id="contact-subject" name="subject" required />
</div>
<div class="contact-field contact-full">
<label for="contact-message">Message <span aria-hidden="true">*</span></label>
<textarea id="contact-message" name="message" rows="6" required></textarea>
</div>
</div>
<div class="contact-actions">
<button type="submit" class="button primary" id="contact-submit-btn">Send Message</button>
</div>
</form>
<div class="contact-loading" id="contact-loading" role="status" aria-live="polite">
<span class="contact-spinner" aria-hidden="true"></span>
Sending your message...
</div>
<div class="contact-success notice" id="contact-success-message" role="status" aria-live="polite">
<h3>Message sent.</h3>
<p>Thanks for getting in touch. The airport team will review your message and respond where needed.</p>
</div>
</section>
<div id="contact-notification" class="contact-notification" role="status" aria-live="polite"></div>
<script define:vars={{ contactRequestEndpoint }}>
(() => {
const CONTACT_REQUEST_ENDPOINT = contactRequestEndpoint;
const get = (id) => document.getElementById(id);
function showNotification(message, isError = false) {
const notification = get('contact-notification');
notification.textContent = message;
notification.classList.toggle('error', isError);
notification.classList.add('show');
window.setTimeout(() => notification.classList.remove('show'), 5000);
}
function buildContactRequestData(form) {
const formData = new FormData(form);
return {
name: String(formData.get('name') || '').trim(),
email: String(formData.get('email') || '').trim(),
phone: String(formData.get('phone') || '').trim(),
enquiry_type: String(formData.get('enquiry_type') || '').trim(),
subject: String(formData.get('subject') || '').trim(),
message: String(formData.get('message') || '').trim(),
source_page: window.location.pathname,
};
}
async function handleSubmit(event) {
event.preventDefault();
const form = event.currentTarget;
const submitButton = get('contact-submit-btn');
const loading = get('contact-loading');
const successMessage = get('contact-success-message');
if (form.website?.value) {
form.reset();
successMessage.style.display = 'block';
form.style.display = 'none';
return;
}
submitButton.disabled = true;
submitButton.textContent = 'Sending...';
loading.style.display = 'flex';
try {
const response = await fetch(CONTACT_REQUEST_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(buildContactRequestData(form)),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || `Request failed with status ${response.status}`);
}
form.style.display = 'none';
successMessage.style.display = 'block';
showNotification('Message sent successfully.');
} catch (error) {
console.error('Error submitting contact request:', error);
showNotification(`Error sending message: ${error.message}`, true);
} finally {
loading.style.display = 'none';
submitButton.disabled = false;
submitButton.textContent = 'Send Message';
}
}
document.addEventListener('DOMContentLoaded', () => {
get('contact-form').addEventListener('submit', handleSubmit);
});
})();
</script>
<style>
.contact-shell {
display: grid;
gap: 1.25rem;
margin-block: 1.5rem 0;
}
.contact-head,
.contact-form {
display: grid;
gap: 1.2rem;
}
.contact-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.contact-field {
display: grid;
gap: 0.4rem;
align-content: start;
}
.contact-full {
grid-column: 1 / -1;
}
.contact-field label {
color: var(--text);
font-size: 0.92rem;
font-weight: 800;
}
.contact-field label span {
color: var(--critical);
}
.contact-field input,
.contact-field select,
.contact-field textarea {
width: 100%;
min-height: 2.85rem;
border: 1px solid rgba(16, 34, 51, 0.16);
border-radius: 0.7rem;
background: rgba(255, 255, 255, 0.88);
color: var(--text);
font: inherit;
font-size: 1rem;
padding: 0.7rem 0.82rem;
}
.contact-field textarea {
min-height: 9rem;
resize: vertical;
}
.contact-field input:focus,
.contact-field select:focus,
.contact-field textarea:focus {
outline: none;
border-color: var(--brand-2);
box-shadow: 0 0 0 0.2rem rgba(29, 118, 184, 0.16);
}
.contact-actions {
display: flex;
justify-content: flex-start;
padding-top: 0.4rem;
}
.contact-actions .button {
border: 0;
cursor: pointer;
}
.contact-actions .button:disabled {
cursor: wait;
opacity: 0.7;
}
.contact-loading {
display: none;
align-items: center;
gap: 0.7rem;
color: var(--brand);
font-weight: 800;
}
.contact-spinner {
width: 1.45rem;
height: 1.45rem;
border: 3px solid rgba(29, 118, 184, 0.18);
border-top-color: var(--brand-2);
border-radius: 50%;
animation: contact-spin 0.8s linear infinite;
}
.contact-success {
display: none;
border-left-color: #257b4c;
}
.contact-notification {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 60;
max-width: min(24rem, calc(100vw - 2rem));
padding: 0.85rem 1rem;
border-radius: 0.8rem;
background: #257b4c;
color: white;
box-shadow: var(--shadow);
font-weight: 800;
opacity: 0;
pointer-events: none;
transform: translateY(-0.65rem);
transition: opacity 0.18s ease, transform 0.18s ease;
}
.contact-notification.show {
opacity: 1;
transform: translateY(0);
}
.contact-notification.error {
background: var(--critical);
}
.contact-honeypot {
position: absolute;
left: -100vw;
width: 1px;
height: 1px;
overflow: hidden;
}
@keyframes contact-spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 720px) {
.contact-grid {
grid-template-columns: 1fr;
}
.contact-actions,
.contact-actions .button {
width: 100%;
}
}
</style>