Enable live Formspree contact submissions
Deploy to Production / deploy (push) Successful in 13s
Details
Deploy to Production / deploy (push) Successful in 13s
Details
This commit is contained in:
parent
a68a6a917c
commit
199264c0a6
|
|
@ -62,7 +62,9 @@
|
||||||
<article class="form-card" data-animate>
|
<article class="form-card" data-animate>
|
||||||
<h2 class="form-card__title">Fill out to connect</h2>
|
<h2 class="form-card__title">Fill out to connect</h2>
|
||||||
<div class="form-card__accent"></div>
|
<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__grid">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-field__label" for="full-name">Full Name</label>
|
<label class="form-field__label" for="full-name">Full Name</label>
|
||||||
|
|
@ -92,7 +94,7 @@
|
||||||
</div>
|
</div>
|
||||||
<button class="button button--dark button--block" type="submit">Send Message</button>
|
<button class="button button--dark button--block" type="submit">Send Message</button>
|
||||||
<div class="form-status" aria-live="polite" data-form-status></div>
|
<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>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
|
@ -139,7 +141,7 @@
|
||||||
<a class="site-footer__link" href="../cv/">CV</a>
|
<a class="site-footer__link" href="../cv/">CV</a>
|
||||||
<a class="site-footer__link" href="mailto:popovskik02@yahoo.com">Email</a>
|
<a class="site-footer__link" href="mailto:popovskik02@yahoo.com">Email</a>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -477,6 +477,12 @@ textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
.button__icon,
|
.button__icon,
|
||||||
.text-link__icon {
|
.text-link__icon {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
|
|
@ -1420,6 +1426,10 @@ textarea {
|
||||||
color: #0d6b3f;
|
color: #0d6b3f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-status.is-error {
|
||||||
|
color: #ba1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
.link-grid {
|
.link-grid {
|
||||||
margin-top: var(--space-10);
|
margin-top: var(--space-10);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
89
js/script.js
89
js/script.js
|
|
@ -136,14 +136,37 @@ function setupContactForm() {
|
||||||
|
|
||||||
const fields = Array.from(form.querySelectorAll("[data-required], [type='email']"));
|
const fields = Array.from(form.querySelectorAll("[data-required], [type='email']"));
|
||||||
const status = form.querySelector("[data-form-status]");
|
const status = form.querySelector("[data-form-status]");
|
||||||
|
const submitButton = form.querySelector("button[type='submit']");
|
||||||
const action = (form.getAttribute("action") || "").trim();
|
const action = (form.getAttribute("action") || "").trim();
|
||||||
const hasExternalEndpoint = /^https?:\/\//i.test(action);
|
const hasExternalEndpoint = /^https?:\/\//i.test(action);
|
||||||
const hasPlaceholderEndpoint = action.includes("YOUR_ID_HERE");
|
const hasPlaceholderEndpoint = action.includes("YOUR_ID_HERE");
|
||||||
|
const method = (form.getAttribute("method") || "post").toUpperCase();
|
||||||
|
const buttonText = submitButton ? submitButton.textContent : "";
|
||||||
|
|
||||||
const validators = {
|
const validators = {
|
||||||
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
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 setFieldError = (field, message) => {
|
||||||
const wrapper = field.closest(".form-field");
|
const wrapper = field.closest(".form-field");
|
||||||
const error = wrapper ? wrapper.querySelector(".form-field__error") : null;
|
const error = wrapper ? wrapper.querySelector(".form-field__error") : null;
|
||||||
|
|
@ -197,11 +220,7 @@ function setupContactForm() {
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
setStatus("Please review the highlighted fields and try again.", "error");
|
||||||
if (status) {
|
|
||||||
status.textContent = "Please review the highlighted fields and try again.";
|
|
||||||
status.classList.remove("is-success");
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstInvalid = form.querySelector("[aria-invalid='true']");
|
const firstInvalid = form.querySelector("[aria-invalid='true']");
|
||||||
|
|
||||||
|
|
@ -212,30 +231,50 @@ function setupContactForm() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status) {
|
|
||||||
status.classList.remove("is-success");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasExternalEndpoint || hasPlaceholderEndpoint) {
|
if (!hasExternalEndpoint || hasPlaceholderEndpoint) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
setStatus("Contact form is currently unavailable. Please try again shortly.", "error");
|
||||||
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, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status) {
|
event.preventDefault();
|
||||||
status.textContent = "Submitting your message...";
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue