From 42fa44138dd9b262f96a4311ed029eb7f98efc9a Mon Sep 17 00:00:00 2001
From: Cavin
Date: Sun, 5 Jul 2026 23:56:37 +0300
Subject: [PATCH 1/2] feat: update contact form to integrate Formspree, add
error handling, and improve UI
---
frontend/.env | 1 -
frontend/.gitignore | 4 +
frontend/package-lock.json | 87 ++++++++-
frontend/package.json | 1 +
frontend/src/components/sections/About.tsx | 2 +-
frontend/src/components/sections/Contact.tsx | 180 ++++++++++++++-----
6 files changed, 227 insertions(+), 48 deletions(-)
delete mode 100644 frontend/.env
diff --git a/frontend/.env b/frontend/.env
deleted file mode 100644
index 95c8cdd..0000000
--- a/frontend/.env
+++ /dev/null
@@ -1 +0,0 @@
-VITE_API_BASE_URL = http://localhost:8080
\ No newline at end of file
diff --git a/frontend/.gitignore b/frontend/.gitignore
index a547bf3..670513f 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -22,3 +22,7 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+# environment
+.env
+.env.local
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 4a8d42a..9519b6e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,6 +8,7 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
+ "@formspree/react": "^3.0.0",
"@heroicons/react": "^2.2.0",
"@tabler/icons-react": "^3.44.0",
"@tailwindcss/vite": "^4.3.2",
@@ -432,6 +433,30 @@
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
+ "node_modules/@formspree/core": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@formspree/core/-/core-4.0.0.tgz",
+ "integrity": "sha512-geNlUut5nME1Ztej5Pzx1BrlQ1fFIcJYIqmF+Vm0jaUbpZxjXvt7SDOGeQVkuxn80QJiIHlwBGGjSBFjPX/KDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@stripe/stripe-js": "^5.7.0"
+ }
+ },
+ "node_modules/@formspree/react": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@formspree/react/-/react-3.0.0.tgz",
+ "integrity": "sha512-8PufBZ4l13VmCp9xTGQXwyF6mZtYSecqgTlFuMo5Fbe4Q6zUk7PMU2uKOwIaytZyTyJgFVCvdbpQElPPBKYNLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@formspree/core": "^4.0.0",
+ "@stripe/react-stripe-js": "^3.1.1",
+ "@stripe/stripe-js": "^5.7.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0"
+ }
+ },
"node_modules/@heroicons/react": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
@@ -845,6 +870,29 @@
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"license": "MIT"
},
+ "node_modules/@stripe/react-stripe-js": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.10.0.tgz",
+ "integrity": "sha512-UPqHZwMwDzGSax0ZI7XlxR3tZSpgIiZdk3CiwjbTK978phwR/fFXeAXQcN/h8wTAjR4ZIAzdlI9DbOqJhuJdeg==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.7.2"
+ },
+ "peerDependencies": {
+ "@stripe/stripe-js": ">=1.44.1 <8.0.0",
+ "react": ">=16.8.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@stripe/stripe-js": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-5.10.0.tgz",
+ "integrity": "sha512-PTigkxMdMUP6B5ISS7jMqJAKhgrhZwjprDqR1eATtFfh0OpKVNp110xiH+goeVdrJ29/4LeZJR4FaHHWstsu0A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.16"
+ }
+ },
"node_modules/@tabler/icons": {
"version": "3.44.0",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.44.0.tgz",
@@ -2130,7 +2178,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/jsesc": {
@@ -2481,6 +2528,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -2567,6 +2626,15 @@
"node": ">=18"
}
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2693,6 +2761,17 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2733,6 +2812,12 @@
"react": "*"
}
},
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
"node_modules/react-router": {
"version": "7.18.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 4f2eff4..cf75644 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "@formspree/react": "^3.0.0",
"@heroicons/react": "^2.2.0",
"@tabler/icons-react": "^3.44.0",
"@tailwindcss/vite": "^4.3.2",
diff --git a/frontend/src/components/sections/About.tsx b/frontend/src/components/sections/About.tsx
index 60de8c3..5d7deed 100644
--- a/frontend/src/components/sections/About.tsx
+++ b/frontend/src/components/sections/About.tsx
@@ -7,7 +7,7 @@ export default function About() {
About Gatelog
- Built with simplicity in mind
+ Built after watching a receptionist manage 30 visitors a day with a paper logbook
diff --git a/frontend/src/components/sections/Contact.tsx b/frontend/src/components/sections/Contact.tsx
index 12f494f..d29e661 100644
--- a/frontend/src/components/sections/Contact.tsx
+++ b/frontend/src/components/sections/Contact.tsx
@@ -22,10 +22,14 @@ const siteTypes = [
'Other',
]
+
+const FORMSPREE_ENDPOINT = import.meta.env.VITE_FORMSPREE_ENDPOINT
+
export default function Contact() {
const [form, setForm] = useState
(initialForm)
const [submitted, setSubmitted] = useState(false)
const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
const set = (field: keyof FormState) =>
(e: React.ChangeEvent) =>
@@ -34,12 +38,44 @@ export default function Contact() {
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
setLoading(true)
- // Simulated — wire to your backend or Formspree later
- await new Promise(r => setTimeout(r, 1000))
- setLoading(false)
- setSubmitted(true)
+ setError(null)
+
+ try {
+ const response = await fetch(FORMSPREE_ENDPOINT, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+ body: JSON.stringify({
+ name: form.name,
+ email: form.email,
+ organization: form.org,
+ siteType: form.siteType,
+ message: form.message,
+ _subject: `Demo Request from ${form.name}`,
+ _replyto: form.email,
+ _gotcha: '',
+ }),
+ })
+
+ if (response.ok) {
+ setSubmitted(true)
+ setLoading(false)
+ setForm(initialForm)
+ } else {
+ const data = await response.json()
+ setError(data.error || 'Something went wrong. Please try again.')
+ }
+ } catch (_) {
+ setError('Network error. Please check your connection and try again.')
+ } finally {
+ setLoading(false)
+ }
}
+ // ─── Success Page ──────────────────────────────────────────────
+
if (submitted) {
return (