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:
@@ -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 ───────────────
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user