Enable live Formspree contact submissions
Deploy to Production / deploy (push) Successful in 13s Details

This commit is contained in:
Kristijan Popovski 2026-04-09 14:44:29 +02:00
parent a68a6a917c
commit 199264c0a6
3 changed files with 79 additions and 28 deletions

View File

@ -62,7 +62,9 @@
<article class="form-card" data-animate>
<h2 class="form-card__title">Fill out to connect</h2>
<div class="form-card__accent"></div>
<form class="form" action="https://formspree.io/f/YOUR_ID_HERE" method="post" novalidate data-contact-form>
<form class="form" action="https://formspree.io/f/mreovogz" method="post" novalidate data-contact-form>
<input type="hidden" name="_subject" value="New contact form submission from popovskik.com">
<input class="visually-hidden" type="text" name="_gotcha" tabindex="-1" autocomplete="off" aria-hidden="true">
<div class="form__grid">
<div class="form-field">
<label class="form-field__label" for="full-name">Full Name</label>
@ -92,7 +94,7 @@
</div>
<button class="button button--dark button--block" type="submit">Send Message</button>
<div class="form-status" aria-live="polite" data-form-status></div>
<p class="form-note">Replace the placeholder Formspree action with your real form ID to enable live submissions.</p>
<p class="form-note">Your message is sent securely and reviewed personally. You will only see a confirmation after sending.</p>
</form>
</article>
@ -139,7 +141,7 @@
<a class="site-footer__link" href="../cv/">CV</a>
<a class="site-footer__link" href="mailto:popovskik02@yahoo.com">Email</a>
</div>
<div class="site-footer__meta">Static contact form ready for future integration</div>
<div class="site-footer__meta">Live contact form with secure delivery</div>
</div>
</footer>
</div>

View File

@ -477,6 +477,12 @@ textarea {
width: 100%;
}
.button:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
.button__icon,
.text-link__icon {
width: 1rem;
@ -1420,6 +1426,10 @@ textarea {
color: #0d6b3f;
}
.form-status.is-error {
color: #ba1a1a;
}
.link-grid {
margin-top: var(--space-10);
}

View File

@ -136,14 +136,37 @@ function setupContactForm() {
const fields = Array.from(form.querySelectorAll("[data-required], [type='email']"));
const status = form.querySelector("[data-form-status]");
const submitButton = form.querySelector("button[type='submit']");
const action = (form.getAttribute("action") || "").trim();
const hasExternalEndpoint = /^https?:\/\//i.test(action);
const hasPlaceholderEndpoint = action.includes("YOUR_ID_HERE");
const method = (form.getAttribute("method") || "post").toUpperCase();
const buttonText = submitButton ? submitButton.textContent : "";
const validators = {
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
};
const setStatus = (message, state = "") => {
if (!status) {
return;
}
status.textContent = message;
status.classList.toggle("is-success", state === "success");
status.classList.toggle("is-error", state === "error");
};
const setSubmitting = (isSubmitting) => {
if (!submitButton) {
return;
}
submitButton.disabled = isSubmitting;
submitButton.setAttribute("aria-busy", String(isSubmitting));
submitButton.textContent = isSubmitting ? "Sending..." : buttonText;
};
const setFieldError = (field, message) => {
const wrapper = field.closest(".form-field");
const error = wrapper ? wrapper.querySelector(".form-field__error") : null;
@ -197,11 +220,7 @@ function setupContactForm() {
if (!isValid) {
event.preventDefault();
if (status) {
status.textContent = "Please review the highlighted fields and try again.";
status.classList.remove("is-success");
}
setStatus("Please review the highlighted fields and try again.", "error");
const firstInvalid = form.querySelector("[aria-invalid='true']");
@ -212,30 +231,50 @@ function setupContactForm() {
return;
}
if (status) {
status.classList.remove("is-success");
}
if (!hasExternalEndpoint || hasPlaceholderEndpoint) {
event.preventDefault();
if (status) {
status.textContent = hasPlaceholderEndpoint
? "Replace the Formspree form ID to enable live message delivery."
: "Thanks. Your message structure is validated and ready for backend integration.";
status.classList.toggle("is-success", !hasPlaceholderEndpoint);
}
if (!hasPlaceholderEndpoint) {
form.reset();
fields.forEach((field) => setFieldError(field, ""));
}
setStatus("Contact form is currently unavailable. Please try again shortly.", "error");
return;
}
if (status) {
status.textContent = "Submitting your message...";
}
event.preventDefault();
setSubmitting(true);
setStatus("Sending your message...");
fetch(action, {
method,
headers: {
Accept: "application/json"
},
body: new FormData(form)
})
.then(async (response) => {
if (response.ok) {
form.reset();
fields.forEach((field) => setFieldError(field, ""));
setStatus("Thank you. Your contact request was sent and noted.", "success");
return;
}
let errorMessage = "Sorry, your message could not be sent. Please try again shortly.";
try {
const payload = await response.json();
if (payload && Array.isArray(payload.errors) && payload.errors.length > 0) {
errorMessage = payload.errors.map((item) => item.message).join(" ");
}
} catch (parseError) {
// Keep fallback message when response body is not JSON.
}
throw new Error(errorMessage);
})
.catch((error) => {
setStatus(error.message, "error");
})
.finally(() => {
setSubmitting(false);
});
});
}