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..c0c80a5 100644 --- a/frontend/src/components/sections/Contact.tsx +++ b/frontend/src/components/sections/Contact.tsx @@ -2,11 +2,11 @@ import { useState, type FormEvent } from 'react' import { Send, CheckCircle2 } from 'lucide-react' interface FormState { - name: string - email: string - org: string + name: string + email: string + org: string siteType: string - message: string + message: string } const initialForm: FormState = { @@ -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 [form, setForm] = useState(initialForm) const [submitted, setSubmitted] = useState(false) - const [loading, setLoading] = 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) + setForm(initialForm) + } else { + const data = await response.json() + setError(data.error || 'Something went wrong. Please try again.') + } + } catch (err) { + console.error('Form submission error:', err) + setError('Network error. Please check your connection and try again.') + } finally { + setLoading(false) + } } + // ─── Success Page ────────────────────────────────────────────── + if (submitted) { return (
@@ -64,6 +100,8 @@ export default function Contact() { ) } + // ─── Contact Form ────────────────────────────────────────────── + return (
@@ -78,55 +116,115 @@ export default function Contact() { Fill in your details and I'll get back to you with access to the live system.

+ {/* Error Alert */} + {error && ( +
+ + + + {error} +
+ )} +
+ {/* ─── Honeypot: hidden _gotcha field ────────────────── */} + +
- +
+ - - +
+ +
+ - +
- +
+ - - - - - - - +
+ +
+ +
+ +
+
+ +
+