feat(marketing): add contact form submission and testing setup

- Add contact API endpoint for demo requests and sales enquiries
- Implement functional contact forms on Demo and Pricing pages with honeypot spam protection
- Update footer layout: remove Company section, add contact email
- Update self-hosted FAQ to mention open source with GitHub links
- Add vitest and testing-library dependencies
- Add tests for MarketingLayout and PricingPage components
- Remove placeholder external-auth test file
This commit is contained in:
2026-04-18 00:33:02 +09:30
parent 2baf6cd51a
commit cb62079b4f
12 changed files with 1794 additions and 343 deletions
+4 -12
View File
@@ -125,7 +125,7 @@ return (
{/* Footer */}
<footer className="border-t bg-muted/30">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-12">
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8">
{/* Brand */}
<div className="col-span-2 lg:col-span-1">
<Link to="/" className="flex items-center gap-2.5">
@@ -135,6 +135,9 @@ return (
<p className="mt-4 text-sm text-muted-foreground max-w-xs">
Enterprise identity and access management. Secure by design, simple by choice.
</p>
<p className="mt-3 text-sm text-muted-foreground">
Contact us: <a href="mailto:info@secuird.tech" className="text-foreground hover:underline transition-colors">info@secuird.tech</a>
</p>
</div>
{/* Product */}
@@ -160,17 +163,6 @@ return (
</ul>
</div>
{/* Company */}
<div>
<h3 className="text-sm font-semibold text-foreground mb-3">Company</h3>
<ul className="space-y-2">
<li><a href="#" className="text-sm text-muted-foreground hover:text-foreground transition-colors">About</a></li>
<li><a href="#" className="text-sm text-muted-foreground hover:text-foreground transition-colors">Blog</a></li>
<li><a href="#" className="text-sm text-muted-foreground hover:text-foreground transition-colors">Careers</a></li>
<li><a href="#" className="text-sm text-muted-foreground hover:text-foreground transition-colors">Contact</a></li>
</ul>
</div>
{/* Legal */}
<div>
<h3 className="text-sm font-semibold text-foreground mb-3">Legal</h3>
+16
View File
@@ -1223,6 +1223,22 @@ export const api = {
),
},
contact: {
submit: (data: {
email: string;
name?: string;
company?: string;
enquiry_type: string;
message?: string;
interest_area?: string;
_hp?: string;
}) =>
request<void>('/contact', {
method: 'POST',
body: JSON.stringify(data),
}, false),
},
ssh: {
// List all SSH keys for the current user
listKeys: (requestConfig?: RequestConfig) =>
+1 -1
View File
@@ -120,7 +120,7 @@ export default function OIDCLoginPage() {
setErrorMsg(err.message);
setStep("error");
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [oidcSessionId]);
// ── Determine step once both context and auth state are ready ───────────────
+102 -35
View File
@@ -1,15 +1,18 @@
import { Link } from "react-router-dom";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Play,
ArrowRight,
Users,
Lock,
Terminal,
Shield,
MonitorPlay,
Play,
ArrowRight,
Users,
Lock,
Terminal,
Shield,
MonitorPlay,
} from "lucide-react";
import { api } from "@/lib/api";
import { useToast } from "@/hooks/use-toast";
const demos = [
{
@@ -55,6 +58,41 @@ icon: MonitorPlay,
];
export default function DemoPage() {
const [email, setEmail] = useState("");
const [company, setCompany] = useState("");
const [interestArea, setInterestArea] = useState("SSO / Social Login");
const [hp, setHp] = useState("");
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const { toast } = useToast();
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setSuccess(false);
try {
await api.contact.submit({
email,
company,
enquiry_type: "demo_request",
interest_area: interestArea,
_hp: hp,
});
setSuccess(true);
setEmail("");
setCompany("");
setInterestArea("SSO / Social Login");
setHp("");
} catch (err) {
toast({
variant: "destructive",
title: "Submission failed",
description: err instanceof Error ? err.message : "Something went wrong.",
});
} finally {
setLoading(false);
}
}
return (
<>
{/* Hero */}
@@ -187,38 +225,67 @@ return (
</div>
<div className="bg-card rounded-lg p-6 border">
<h3 className="text-lg font-semibold text-foreground mb-4">Request Demo</h3>
<form className="space-y-4">
<div>
<label className="block text-sm font-medium text-foreground mb-1">Work Email</label>
<input
type="email"
className="w-full px-3 py-2 bg-background border border-input rounded-md text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="you@company.com"
/>
{success ? (
<div className="text-center py-8">
<div className="h-12 w-12 rounded-full bg-accent/10 flex items-center justify-center mx-auto mb-4">
<Play className="h-6 w-6 text-accent" />
</div>
<h3 className="text-lg font-semibold text-foreground mb-2">Request Received!</h3>
<p className="text-muted-foreground text-sm">Thanks! We'll be in touch shortly.</p>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">Company</label>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
{/* Honeypot — hidden from real users */}
<input
type="text"
className="w-full px-3 py-2 bg-background border border-input rounded-md text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="Your company name"
name="_hp"
value={hp}
onChange={(e) => setHp(e.target.value)}
autoComplete="off"
aria-hidden="true"
tabIndex={-1}
className="hidden"
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">What are you looking for?</label>
<select className="w-full px-3 py-2 bg-background border border-input rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-ring">
<option>SSO / Social Login</option>
<option>SSH Certificate Management</option>
<option>MFA / Security</option>
<option>OIDC Provider</option>
<option>Full IAM Solution</option>
</select>
</div>
<Button className="w-full">
Request Demo
</Button>
</form>
<div>
<label className="block text-sm font-medium text-foreground mb-1">Work Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-3 py-2 bg-background border border-input rounded-md text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="you@company.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">Company</label>
<input
type="text"
value={company}
onChange={(e) => setCompany(e.target.value)}
className="w-full px-3 py-2 bg-background border border-input rounded-md text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="Your company name"
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-1">What are you looking for?</label>
<select
value={interestArea}
onChange={(e) => setInterestArea(e.target.value)}
className="w-full px-3 py-2 bg-background border border-input rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
>
<option>SSO / Social Login</option>
<option>SSH Certificate Management</option>
<option>MFA / Security</option>
<option>OIDC Provider</option>
<option>Full IAM Solution</option>
</select>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "Sending..." : "Request Demo"}
</Button>
</form>
)}
</div>
</div>
</CardContent>
+130 -14
View File
@@ -1,17 +1,20 @@
import { Link } from "react-router-dom";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import {
CreditCard,
CheckCircle2,
ArrowRight,
Users,
Server,
Shield,
Zap,
Building2,
HelpCircle,
CreditCard,
CheckCircle2,
ArrowRight,
Users,
Server,
Shield,
Zap,
Building2,
HelpCircle,
} from "lucide-react";
import { api } from "@/lib/api";
import { useToast } from "@/hooks/use-toast";
const plans = [
{
@@ -97,7 +100,7 @@ answer: "Yes! We offer 50% off for qualified startups and non-profit organizatio
},
{
question: "Is there a self-hosted option?",
answer: "Yes, our Enterprise plan includes self-hosted deployment options. This is ideal for organizations with strict data residency requirements.",
answer: <>Yes, we are open source! You can find the UI at <a href="https://github.com/CoryHawkless/gatehouse-ui" target="_blank" rel="noopener noreferrer" className="text-foreground hover:underline transition-colors">https://github.com/CoryHawkless/gatehouse-ui</a> and the API at <a href="https://github.com/CoryHawkless/gatehouse-api" target="_blank" rel="noopener noreferrer" className="text-foreground hover:underline transition-colors">https://github.com/CoryHawkless/gatehouse-api</a>.</>,
},
{
question: "What payment methods do you accept?",
@@ -106,6 +109,46 @@ answer: "We accept all major credit cards, ACH transfers (US), and wire transfer
];
export default function PricingPage() {
const [contactOpen, setContactOpen] = useState(false);
const [email, setEmail] = useState("");
const [company, setCompany] = useState("");
const [message, setMessage] = useState("");
const [hp, setHp] = useState("");
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const { toast } = useToast();
async function handleContactSubmit(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setSuccess(false);
try {
await api.contact.submit({
email,
company,
enquiry_type: "sales_enquiry",
message,
_hp: hp,
});
setSuccess(true);
setEmail("");
setCompany("");
setMessage("");
setHp("");
setTimeout(() => {
setSuccess(false);
setContactOpen(false);
}, 3000);
} catch (err) {
toast({
variant: "destructive",
title: "Submission failed",
description: err instanceof Error ? err.message : "Something went wrong.",
});
} finally {
setLoading(false);
}
}
return (
<>
{/* Hero */}
@@ -233,10 +276,83 @@ return (
<p className="text-sm text-muted-foreground mb-4">
Talk to our team about your requirements
</p>
<Button variant="outline" className="gap-2">
Contact Sales
<ArrowRight className="h-4 w-4" />
</Button>
{contactOpen ? (
<div className="bg-card rounded-lg p-4 border text-left w-full max-w-sm">
{success ? (
<div className="text-center py-4">
<div className="h-10 w-10 rounded-full bg-accent/10 flex items-center justify-center mx-auto mb-3">
<CheckCircle2 className="h-5 w-5 text-accent" />
</div>
<p className="text-sm font-medium text-foreground">Request Sent!</p>
<p className="text-xs text-muted-foreground mt-1">Thanks! Our sales team will reach out soon.</p>
</div>
) : (
<form onSubmit={handleContactSubmit} className="space-y-3">
<input
type="text"
name="_hp"
value={hp}
onChange={(e) => setHp(e.target.value)}
autoComplete="off"
aria-hidden="true"
tabIndex={-1}
className="hidden"
/>
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
placeholder="Work email"
className="w-full px-3 py-2 bg-background border border-input rounded-md text-foreground placeholder:text-muted-foreground text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
<div>
<input
type="text"
value={company}
onChange={(e) => setCompany(e.target.value)}
placeholder="Company"
className="w-full px-3 py-2 bg-background border border-input rounded-md text-foreground placeholder:text-muted-foreground text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
<div>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="How can we help?"
rows={3}
className="w-full px-3 py-2 bg-background border border-input rounded-md text-foreground placeholder:text-muted-foreground text-sm focus:outline-none focus:ring-2 focus:ring-ring resize-none"
/>
</div>
<div className="flex gap-2">
<Button
type="submit"
size="sm"
className="flex-1"
disabled={loading}
>
{loading ? "Sending..." : "Send"}
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => { setContactOpen(false); setSuccess(false); }}
>
Cancel
</Button>
</div>
</form>
)}
</div>
) : (
<Button variant="outline" className="gap-2" onClick={() => setContactOpen(true)}>
Contact Sales
<ArrowRight className="h-4 w-4" />
</Button>
)}
</div>
</div>
</div>