- Introduced a new spamReview property in the Form interface to handle manual review requests for messages flagged as spam. - Updated the Form component to display a spam warning and a manual review form with dynamic labels and placeholders based on translations. - Enhanced the Tailwind CSS styles for responsive subtitles across various components. - Added corresponding translations for the spam review feature in English, Dutch, German, and French, ensuring consistency and clarity in messaging. - Updated various components to integrate the new spamReview functionality, improving user experience and interaction.
292 lines
11 KiB
Plaintext
292 lines
11 KiB
Plaintext
---
|
|
import type { Form as Props } from '~/types';
|
|
import Button from '~/components/ui/Button.astro';
|
|
|
|
const { inputs, textarea, disclaimer, button = 'Contact us', description = '', spamReview } = 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" name="contact" method="POST" action="/api/contact" class="needs-validation" novalidate>
|
|
<!-- Form status messages -->
|
|
<div id="form-success" class="hidden mb-6 p-4 bg-green-100 border border-green-200 text-green-700 rounded-lg">
|
|
Your message has been sent successfully. We will get back to you soon!
|
|
</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 check all fields and try again.
|
|
</div>
|
|
|
|
<!-- CSRF token field (will be filled by JS) -->
|
|
<input type="hidden" name="csrf_token" id="csrf_token" value="" />
|
|
|
|
<!-- Honeypot field to prevent spam -->
|
|
<p class="hidden">
|
|
<label>Don't fill this out if you're human: <input name="bot-field" /></label>
|
|
</p>
|
|
{
|
|
inputs &&
|
|
inputs.map(
|
|
({ type = 'text', name, label = '', autocomplete = 'on', placeholder = '', required = true }) =>
|
|
name && (
|
|
<div class="mb-6">
|
|
{label && (
|
|
<label for={name} class="block text-sm font-medium">
|
|
{label}
|
|
{required && <span class="text-red-600">*</span>}
|
|
</label>
|
|
)}
|
|
<input
|
|
type={type}
|
|
name={name}
|
|
id={name}
|
|
autocomplete={autocomplete}
|
|
placeholder={placeholder}
|
|
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
|
required={required}
|
|
aria-describedby={`invalid-feedback-${name}`}
|
|
/>
|
|
<div id={`invalid-feedback-${name}`} class="invalid-feedback hidden text-red-600 text-sm mt-1" />
|
|
</div>
|
|
)
|
|
)
|
|
}
|
|
|
|
{
|
|
textarea && (
|
|
<div class="mb-6">
|
|
<label for="textarea" class="block text-sm font-medium">
|
|
{textarea.label}
|
|
<span class="text-red-600">*</span>
|
|
</label>
|
|
<textarea
|
|
id="textarea"
|
|
name={textarea.name ? textarea.name : 'message'}
|
|
rows={textarea.rows ? textarea.rows : 4}
|
|
placeholder={textarea.placeholder}
|
|
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
|
required
|
|
aria-describedby="invalid-feedback-textarea"
|
|
/>
|
|
<div id="invalid-feedback-textarea" class="invalid-feedback hidden text-red-600 text-sm mt-1" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
{
|
|
disclaimer && (
|
|
<div class="mt-3 flex items-start mb-6">
|
|
<div class="flex mt-0.5">
|
|
<input
|
|
id="disclaimer"
|
|
name="disclaimer"
|
|
type="checkbox"
|
|
class="cursor-pointer mt-1 py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
|
required
|
|
aria-describedby="invalid-feedback-disclaimer"
|
|
/>
|
|
</div>
|
|
<div class="ml-3">
|
|
<label for="disclaimer" class="cursor-pointer select-none text-sm text-gray-600 dark:text-gray-400">
|
|
{disclaimer.label}
|
|
<span class="text-red-600">*</span>
|
|
</label>
|
|
<div id="invalid-feedback-disclaimer" class="invalid-feedback hidden text-red-600 text-sm mt-1" />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
{
|
|
button && (
|
|
<div class="mt-10 grid">
|
|
<Button variant="primary" type="submit">
|
|
{button}
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
{
|
|
description && (
|
|
<div class="mt-3 text-center">
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">{description}</p>
|
|
</div>
|
|
)
|
|
}
|
|
</form>
|
|
|
|
<!-- Manual Review UI -->
|
|
<div
|
|
id="spam-warning"
|
|
style="display:none;"
|
|
class="max-w-xl mx-auto rounded-lg backdrop-blur-sm bg-white/15 dark:bg-slate-900 border border-gray-200 dark:border-gray-700 shadow-md p-4 sm:p-6 lg:p-8 w-full mb-6"
|
|
>
|
|
<p class="mb-4 text-lg font-medium text-gray-800 dark:text-gray-100">
|
|
{spamReview?.title}<br />
|
|
<span class="text-base font-normal text-gray-600 dark:text-gray-300">{spamReview?.description}</span>
|
|
</p>
|
|
<form id="manual-review-form" class="space-y-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="manual-email"
|
|
>{spamReview?.emailLabel}</label
|
|
>
|
|
<input
|
|
type="email"
|
|
id="manual-email"
|
|
required
|
|
placeholder={spamReview?.emailPlaceholder}
|
|
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
|
/>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="manual-justification"
|
|
>{spamReview?.justificationLabel} <span class="text-gray-400">{spamReview?.justificationOptional}</span></label
|
|
>
|
|
<textarea
|
|
id="manual-justification"
|
|
placeholder={spamReview?.justificationPlaceholder}
|
|
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
|
></textarea>
|
|
<input type="hidden" id="manual-token" />
|
|
<button
|
|
type="submit"
|
|
class="mt-2 w-full py-3 px-4 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold transition-colors"
|
|
>{spamReview?.button}</button
|
|
>
|
|
</form>
|
|
<div id="manual-review-result" class="mt-4 text-center text-green-700 dark:text-green-400 font-medium"></div>
|
|
</div>
|
|
|
|
<script>
|
|
// TypeScript: declare the property on window
|
|
declare global {
|
|
interface Window {
|
|
__originalEmail?: string;
|
|
}
|
|
}
|
|
async function setCsrfToken() {
|
|
try {
|
|
const res = await fetch('/api/contact?csrf=true');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
const csrfInput = document.getElementById('csrf_token');
|
|
if (csrfInput && data.csrfToken) {
|
|
(csrfInput as HTMLInputElement).value = data.csrfToken;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to fetch CSRF token', e);
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', setCsrfToken);
|
|
|
|
const form = document.getElementById('contact-form') as HTMLFormElement | null;
|
|
|
|
if (form) {
|
|
form.addEventListener('submit', async (event) => {
|
|
event.preventDefault();
|
|
const formData = new FormData(form);
|
|
let response, result;
|
|
try {
|
|
response = await fetch('/api/contact', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
result = await response.json();
|
|
console.log('Contact API response:', result);
|
|
} catch (_err) {
|
|
const errorElement = document.getElementById('form-error');
|
|
if (errorElement) errorElement.classList.remove('hidden');
|
|
const successElement = document.getElementById('form-success');
|
|
if (successElement) successElement.classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
// Show manual review UI if spam detected, even if status is 422
|
|
if (result.spam && result.token) {
|
|
console.log('Spam detected, showing manual review UI.');
|
|
form.style.display = 'none';
|
|
const spamWarning = document.getElementById('spam-warning');
|
|
const manualEmail = document.getElementById('manual-email') as HTMLInputElement | null;
|
|
const manualToken = document.getElementById('manual-token') as HTMLInputElement | null;
|
|
// Store the original email in a variable (not in the input)
|
|
window.__originalEmail = String(formData.get('email'));
|
|
if (spamWarning && manualEmail && manualToken) {
|
|
spamWarning.style.display = 'block';
|
|
manualEmail.value = '';
|
|
manualToken.value = result.token;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const errorElement = document.getElementById('form-error');
|
|
if (errorElement) errorElement.classList.remove('hidden');
|
|
const successElement = document.getElementById('form-success');
|
|
if (successElement) successElement.classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
// Success
|
|
const successElement = document.getElementById('form-success');
|
|
if (successElement) successElement.classList.remove('hidden');
|
|
const errorElement = document.getElementById('form-error');
|
|
if (errorElement) errorElement.classList.add('hidden');
|
|
form.reset();
|
|
setCsrfToken();
|
|
});
|
|
}
|
|
|
|
// Manual Review Form Logic
|
|
const manualReviewForm = document.getElementById('manual-review-form');
|
|
if (manualReviewForm) {
|
|
manualReviewForm.onsubmit = async function (e) {
|
|
e.preventDefault();
|
|
const manualEmail = document.getElementById('manual-email') as HTMLInputElement | null;
|
|
const manualJustification = document.getElementById('manual-justification') as HTMLTextAreaElement | null;
|
|
const manualToken = document.getElementById('manual-token') as HTMLInputElement | null;
|
|
const resultDiv = document.getElementById('manual-review-result');
|
|
const form = document.getElementById('contact-form') as HTMLFormElement | null;
|
|
const spamWarning = document.getElementById('spam-warning');
|
|
if (!manualEmail || !manualJustification || !manualToken || !resultDiv) return;
|
|
const email = manualEmail.value;
|
|
const justification = manualJustification.value;
|
|
const token = manualToken.value;
|
|
// Check if the entered email matches the original
|
|
if (typeof window.__originalEmail !== 'undefined' && email !== window.__originalEmail) {
|
|
resultDiv.textContent = 'Email addresses do not match.';
|
|
return;
|
|
}
|
|
const res = await fetch('/api/contact/manual-review', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, justification, token }),
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
resultDiv.textContent =
|
|
spamReview?.resultSuccess || 'Your request for manual review has been submitted. Thank you!';
|
|
// Hide spam-warning and show the normal form again after a short delay
|
|
setTimeout(() => {
|
|
if (spamWarning) spamWarning.style.display = 'none';
|
|
if (form) {
|
|
form.style.display = '';
|
|
form.reset();
|
|
}
|
|
resultDiv.textContent = '';
|
|
}, 2000);
|
|
} else {
|
|
resultDiv.textContent =
|
|
data.error || spamReview?.resultError || 'There was an error submitting your manual review request.';
|
|
}
|
|
};
|
|
}
|
|
</script>
|