diff --git a/website/app/services/support/contact/page.tsx b/website/app/services/support/contact/page.tsx
index 14215e8a268..061b5211caf 100644
--- a/website/app/services/support/contact/page.tsx
+++ b/website/app/services/support/contact/page.tsx
@@ -1,9 +1,7 @@
import React from "react";
-import { getRecentBlogPostTeasers } from "@/lib/blog";
import ContactPage from "@/page-components/services/support/contact";
export default function Page() {
- const recentPosts = getRecentBlogPostTeasers();
- return ;
+ return ;
}
diff --git a/website/lib/analytics.tsx b/website/lib/analytics.tsx
new file mode 100644
index 00000000000..e8ea8660298
--- /dev/null
+++ b/website/lib/analytics.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import { usePathname } from "next/navigation";
+import { useEffect } from "react";
+
+declare global {
+ interface Window {
+ gtag?: (...args: unknown[]) => void;
+ }
+}
+
+function getContentGroup(pathname: string): string {
+ if (pathname.startsWith("/docs")) {
+ return "Documentation";
+ }
+ if (pathname.startsWith("/blog")) {
+ return "Blog";
+ }
+ if (pathname.startsWith("/products")) {
+ return "Products";
+ }
+ if (pathname.startsWith("/platform")) {
+ return "Platform";
+ }
+ if (pathname.startsWith("/services")) {
+ return "Services";
+ }
+ if (pathname.startsWith("/pricing")) {
+ return "Pricing";
+ }
+ if (pathname.startsWith("/legal") || pathname.startsWith("/licensing")) {
+ return "Legal";
+ }
+ if (pathname === "/") {
+ return "Home";
+ }
+ return "Other";
+}
+
+export function AnalyticsContentGroup() {
+ const pathname = usePathname();
+
+ useEffect(() => {
+ if (window.gtag) {
+ window.gtag("set", { content_group: getContentGroup(pathname) });
+ }
+ }, [pathname]);
+
+ return null;
+}
+
+export function AnalyticsClickTracker() {
+ const pathname = usePathname();
+
+ useEffect(() => {
+ function handleClick(e: MouseEvent) {
+ if (!(e.target instanceof Element)) {
+ return;
+ }
+
+ const el = e.target.closest("[data-track]");
+ if (!el || !window.gtag) {
+ return;
+ }
+
+ window.gtag("event", el.dataset.track, {
+ event_label: el.dataset.trackLabel || el.textContent?.trim(),
+ link_url: el.getAttribute("href") || undefined,
+ page_path: pathname,
+ });
+ }
+
+ document.addEventListener("click", handleClick);
+ return () => document.removeEventListener("click", handleClick);
+ }, [pathname]);
+
+ return null;
+}
diff --git a/website/lib/providers.tsx b/website/lib/providers.tsx
index f60e244235e..503809893da 100644
--- a/website/lib/providers.tsx
+++ b/website/lib/providers.tsx
@@ -6,6 +6,7 @@ import { Provider } from "react-redux";
import createStore from "@/state";
import { initialState as workshopsInitialState } from "@/state/workshops/workshops.state";
import { StyledComponentsRegistry } from "./registry";
+import { AnalyticsClickTracker, AnalyticsContentGroup } from "./analytics";
export interface LatestBlogPost {
title: string;
@@ -46,6 +47,8 @@ export function Providers({ children, latestBlogPost }: ProvidersProps) {
{children}
+
+
);
diff --git a/website/src/components/layout/site/header.tsx b/website/src/components/layout/site/header.tsx
index 5e9045c1009..cf55d810a5a 100644
--- a/website/src/components/layout/site/header.tsx
+++ b/website/src/components/layout/site/header.tsx
@@ -921,12 +921,14 @@ const DemoAndLaunch: FC = ({ tools }) => {
return (
<>
- Request a Demo
+ Contact Us
- Launch
+
+ Launch
+
>
);
};
diff --git a/website/src/components/misc/contact-form.tsx b/website/src/components/misc/contact-form.tsx
new file mode 100644
index 00000000000..1f4d76c5f82
--- /dev/null
+++ b/website/src/components/misc/contact-form.tsx
@@ -0,0 +1,418 @@
+"use client";
+
+import { FONT_FAMILY_HEADING, THEME_COLORS } from "@/style";
+import { CONTACT_SUBJECTS, ContactSubject } from "@/types/support";
+import React, { FC, FormEvent, useCallback, useState } from "react";
+import styled from "styled-components";
+import { Button } from "./button";
+
+interface ContactFormData {
+ name: string;
+ email: string;
+ company: string;
+ subject: ContactSubject;
+ message: string;
+}
+
+interface ContactFormErrors {
+ name?: string;
+ email?: string;
+ company?: string;
+ message?: string;
+}
+
+interface ContactFormProps {
+ readonly initialSubject?: ContactSubject;
+}
+
+const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+export const ContactForm: FC = ({
+ initialSubject = "Schedule a Demo",
+}) => {
+ const [formData, setFormData] = useState({
+ name: "",
+ email: "",
+ company: "",
+ subject: initialSubject,
+ message: "",
+ });
+ const [errors, setErrors] = useState({});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const updateField = useCallback(
+ (field: K, value: ContactFormData[K]) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ if (errors[field as keyof ContactFormErrors]) {
+ setErrors((prev) => {
+ const next = { ...prev };
+ delete next[field as keyof ContactFormErrors];
+ return next;
+ });
+ }
+ },
+ [errors]
+ );
+
+ const validateForm = useCallback((): boolean => {
+ const newErrors: ContactFormErrors = {};
+
+ if (!formData.name.trim() || formData.name.trim().length < 2) {
+ newErrors.name = "Name is required";
+ }
+ if (!formData.email.trim() || !EMAIL_REGEX.test(formData.email)) {
+ newErrors.email = "Please enter a valid email address";
+ }
+ if (!formData.company.trim() || formData.company.trim().length < 2) {
+ newErrors.company = "Company is required";
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ }, [formData]);
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault();
+
+ if (!validateForm()) {
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ const response = await fetch(
+ "https://forms.chillicream.com/api/SupportForm",
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ Name: formData.name,
+ Email: formData.email,
+ Company: formData.company,
+ SupportPlan: formData.subject,
+ Message: formData.message,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ if (window.gtag) {
+ window.gtag("event", "contact_form_submit", {
+ event_label: formData.subject,
+ page_path: window.location.pathname,
+ });
+ }
+
+ window.location.href = "/services/support/thank-you";
+ } catch {
+ alert("There was an error submitting your request. Please try again.");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
+ Contact Us
+
+ Whether you're evaluating GraphQL or scaling your API platform,
+ we can help you build and ship faster.
+
+
+
+ ●
+ Discuss your use case
+
+
+ ●
+ Schedule a demo
+
+
+ ●
+ Understand our plans
+
+
+ ●
+ Get technical guidance
+
+
+
+
+
+
+
+
+ );
+};
+
+const PageLayout = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 48px;
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 1100px;
+ margin: 0 auto;
+ padding: 60px 16px;
+
+ @media only screen and (min-width: 992px) {
+ flex-direction: row;
+ gap: 80px;
+ align-items: flex-start;
+ padding: 100px 16px;
+ }
+`;
+
+const ValueProps = styled.div`
+ display: flex;
+ flex-direction: column;
+ flex: 0 0 auto;
+
+ @media only screen and (min-width: 992px) {
+ flex: 0 0 360px;
+ position: sticky;
+ top: 120px;
+ }
+`;
+
+const Title = styled.h1`
+ margin: 0 0 24px 0;
+ color: ${THEME_COLORS.heading};
+ font-family: ${FONT_FAMILY_HEADING};
+ font-size: 2.5rem;
+ font-weight: 700;
+`;
+
+const Subtitle = styled.p.attrs({
+ className: "text-2",
+})`
+ margin: 0 0 40px 0;
+ color: ${THEME_COLORS.text};
+ line-height: 1.6;
+`;
+
+const PropList = styled.ul`
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+`;
+
+const PropItem = styled.li`
+ display: flex;
+ align-items: center;
+ gap: 12px;
+`;
+
+const PropIcon = styled.span`
+ color: ${THEME_COLORS.primary};
+ font-size: 0.5rem;
+`;
+
+const PropText = styled.span`
+ color: ${THEME_COLORS.text};
+ font-size: 1rem;
+`;
+
+const FormCard = styled.div`
+ flex: 1 1 auto;
+ box-sizing: border-box;
+ border: 1px solid ${THEME_COLORS.boxBorder};
+ border-radius: var(--box-border-radius);
+ padding: 24px;
+ backdrop-filter: blur(2px);
+ background-image: linear-gradient(
+ to right bottom,
+ #379dc83d,
+ #2b80ad3d,
+ #2263903d,
+ #1a48743d,
+ #112f573d
+ );
+
+ @media only screen and (min-width: 600px) {
+ padding: 40px;
+ }
+`;
+
+const Form = styled.form`
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+`;
+
+const FormRow = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+
+ @media only screen and (min-width: 600px) {
+ flex-direction: row;
+ gap: 16px;
+
+ & > * {
+ flex: 1;
+ }
+ }
+`;
+
+const FormGroup = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+const Label = styled.label`
+ margin-bottom: 8px;
+ color: ${THEME_COLORS.text};
+ font-weight: 500;
+ font-size: 0.875rem;
+`;
+
+const inputStyles = `
+ padding: 12px 16px;
+ border: 1px solid ${THEME_COLORS.boxBorder};
+ border-radius: var(--border-radius);
+ background-color: rgba(255, 255, 255, 0.05);
+ color: ${THEME_COLORS.text};
+ font-size: 1rem;
+ transition: border-color 0.2s ease-in-out, background-color 0.2s ease-in-out;
+
+ &:focus {
+ outline: none;
+ border-color: ${THEME_COLORS.primary};
+ background-color: rgba(255, 255, 255, 0.08);
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ &::placeholder {
+ color: ${THEME_COLORS.text}80;
+ }
+`;
+
+const Input = styled.input<{ $hasError?: boolean }>`
+ ${inputStyles}
+ ${({ $hasError }) =>
+ $hasError && `border-color: #ef4444; &:focus { border-color: #ef4444; }`}
+`;
+
+const TextArea = styled.textarea<{ $hasError?: boolean }>`
+ ${inputStyles}
+ resize: vertical;
+ min-height: 120px;
+ ${({ $hasError }) =>
+ $hasError && `border-color: #ef4444; &:focus { border-color: #ef4444; }`}
+`;
+
+const Select = styled.select`
+ ${inputStyles}
+ cursor: pointer;
+`;
+
+const ErrorText = styled.span`
+ margin-top: 4px;
+ color: #ef4444;
+ font-size: 0.875rem;
+`;
+
+const SubmitButton = styled(Button)`
+ align-self: flex-start;
+ padding: 12px 32px;
+ font-size: 1rem;
+ font-weight: 600;
+ margin-top: 8px;
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+`;
diff --git a/website/src/components/misc/index.ts b/website/src/components/misc/index.ts
index 19e8540a14e..911cf98c707 100644
--- a/website/src/components/misc/index.ts
+++ b/website/src/components/misc/index.ts
@@ -1,6 +1,7 @@
export * from "./button";
export * from "./cards";
export * from "./close";
+export * from "./contact-form";
export * from "./content-section";
export * from "./feature-matrix";
export * from "./hero-elements";
diff --git a/website/src/components/misc/next-steps-content-section.tsx b/website/src/components/misc/next-steps-content-section.tsx
index c62940ff098..7af5475c46b 100644
--- a/website/src/components/misc/next-steps-content-section.tsx
+++ b/website/src/components/misc/next-steps-content-section.tsx
@@ -13,8 +13,10 @@ export interface NextStepsContentSectionProps {
readonly text: ReactNode;
readonly primaryLink: string;
readonly primaryLinkText: string;
+ readonly primaryTrack?: string;
readonly secondaryLink?: string;
readonly secondaryLinkText?: string;
+ readonly secondaryTrack?: string;
readonly dense?: boolean;
}
@@ -23,8 +25,10 @@ export function NextStepsContentSection({
text,
primaryLink,
primaryLinkText,
+ primaryTrack,
secondaryLink,
secondaryLinkText,
+ secondaryTrack,
dense,
}: NextStepsContentSectionProps): ReactElement {
return (
@@ -35,9 +39,11 @@ export function NextStepsContentSection({
{text}
- {primaryLinkText}
+
+ {primaryLinkText}
+
{secondaryLink && secondaryLinkText && (
-
+
{secondaryLinkText}
diff --git a/website/src/components/misc/plan.tsx b/website/src/components/misc/plan.tsx
index 9b6f20cecb9..76a6b7513c8 100644
--- a/website/src/components/misc/plan.tsx
+++ b/website/src/components/misc/plan.tsx
@@ -32,6 +32,7 @@ export interface PlanProps {
readonly features: readonly string[];
readonly ctaText: string;
readonly ctaLink: string;
+ readonly ctaTrack?: string;
}
function Plan({
@@ -43,6 +44,7 @@ function Plan({
features,
ctaText,
ctaLink,
+ ctaTrack,
}: PlanProps): ReactElement {
return (
<>
@@ -76,7 +78,9 @@ function Plan({
))}
- {ctaText}
+
+ {ctaText}
+
>
);
diff --git a/website/src/page-components/index.tsx b/website/src/page-components/index.tsx
index 72efa2d9b83..75dfb585f81 100644
--- a/website/src/page-components/index.tsx
+++ b/website/src/page-components/index.tsx
@@ -58,7 +58,9 @@ const IndexPage: FC = ({ recentPosts }) => {
data accessibility and enhancing integration. Transform the way you
manage and interact with your data.
- Get Started
+
+ Get Started
+
= ({ recentPosts }) => {
Book a demo to see it in action, or dive right in and start now.
We're here to help you revolutionize your API strategy.
"
- primaryLink="mailto:contact@chillicream.com?subject=Demo"
+ primaryLink="/services/support/contact?subject=Schedule+a+Demo"
primaryLinkText="Book a Demo"
+ primaryTrack="book_demo_click"
secondaryLink="https://nitro.chillicream.com"
secondaryLinkText="Launch"
+ secondaryTrack="launch_click"
/>
diff --git a/website/src/page-components/platform/analytics.tsx b/website/src/page-components/platform/analytics.tsx
index 377ec060ec3..84c60e6481f 100644
--- a/website/src/page-components/platform/analytics.tsx
+++ b/website/src/page-components/platform/analytics.tsx
@@ -108,10 +108,12 @@ const AnalyticsPage: FC = ({ recentPosts }) => {
now and transform your API experience.
>
}
- primaryLink="mailto:contact@chillicream.com?subject=Demo"
+ primaryLink="/services/support/contact?subject=Schedule+a+Demo"
primaryLinkText="Book a Demo"
+ primaryTrack="book_demo_click"
secondaryLink="https://nitro.chillicream.com"
secondaryLinkText="Launch"
+ secondaryTrack="launch_click"
/>
diff --git a/website/src/page-components/platform/continuous-integration.tsx b/website/src/page-components/platform/continuous-integration.tsx
index 74c00474b8a..390a93052bf 100644
--- a/website/src/page-components/platform/continuous-integration.tsx
+++ b/website/src/page-components/platform/continuous-integration.tsx
@@ -153,8 +153,9 @@ const ContinuousIntegrationPage: FC = ({
and precision. Ensure your project is ready for a smooth rollout
and impactful launch.
"
- primaryLink="mailto:contact@chillicream.com?subject=Demo"
+ primaryLink="/services/support/contact?subject=Schedule+a+Demo"
primaryLinkText="Book a Demo"
+ primaryTrack="book_demo_click"
/>
diff --git a/website/src/page-components/pricing.tsx b/website/src/page-components/pricing.tsx
index 7af1c01ad97..4d507e89d8e 100644
--- a/website/src/page-components/pricing.tsx
+++ b/website/src/page-components/pricing.tsx
@@ -53,6 +53,7 @@ const PricingPage: FC = ({ recentPosts }) => {
],
ctaText: "Start for Free",
ctaLink: "https://nitro.chillicream.com",
+ ctaTrack: "start_free_click",
},
{
title: "Scale",
@@ -72,7 +73,8 @@ const PricingPage: FC = ({ recentPosts }) => {
"BYOC*",
],
ctaText: "Contact Sales",
- ctaLink: "mailto:contact@chillicream.com?subject=Scale%20Plan",
+ ctaTrack: "contact_sales_click",
+ ctaLink: "/services/support/contact?subject=Pricing+%26+Plans",
},
{
title: "Enterprise",
@@ -86,8 +88,8 @@ const PricingPage: FC = ({ recentPosts }) => {
"24/7 Support",
],
ctaText: "Contact Sales",
- ctaLink:
- "mailto:contact@chillicream.com?subject=Enterprise%20Plan",
+ ctaTrack: "contact_sales_click",
+ ctaLink: "/services/support/contact?subject=Pricing+%26+Plans",
},
]}
/>
@@ -118,8 +120,8 @@ const PricingPage: FC = ({ recentPosts }) => {
"Access to expert engineers",
],
ctaText: "Contact Sales",
- ctaLink:
- "mailto:contact@chillicream.com?subject=Professional%20Support",
+ ctaTrack: "contact_sales_click",
+ ctaLink: "/services/support/contact?subject=Pricing+%26+Plans",
},
{
title: "Business",
@@ -137,8 +139,8 @@ const PricingPage: FC = ({ recentPosts }) => {
"Email Support",
],
ctaText: "Contact Sales",
- ctaLink:
- "mailto:contact@chillicream.com?subject=Business%20Support",
+ ctaTrack: "contact_sales_click",
+ ctaLink: "/services/support/contact?subject=Pricing+%26+Plans",
},
{
title: "Enterprise",
@@ -159,8 +161,8 @@ const PricingPage: FC = ({ recentPosts }) => {
"Nitro License included",
],
ctaText: "Contact Sales",
- ctaLink:
- "mailto:contact@chillicream.com?subject=Enterprise%20Support",
+ ctaTrack: "contact_sales_click",
+ ctaLink: "/services/support/contact?subject=Pricing+%26+Plans",
},
]}
/>
@@ -184,8 +186,8 @@ const PricingPage: FC = ({ recentPosts }) => {
"Custom solution design",
],
ctaText: "Contact Sales",
- ctaLink:
- "mailto:contact@chillicream.com?subject=Advisory%20Services",
+ ctaTrack: "contact_sales_click",
+ ctaLink: "/services/support/contact?subject=Pricing+%26+Plans",
},
{
title: "Private Workshops",
@@ -201,8 +203,8 @@ const PricingPage: FC = ({ recentPosts }) => {
"Materials included",
],
ctaText: "Contact Sales",
- ctaLink:
- "mailto:contact@chillicream.com?subject=Private%20Workshops",
+ ctaTrack: "contact_sales_click",
+ ctaLink: "/services/support/contact?subject=Pricing+%26+Plans",
},
{
title: "Monthly Sessions",
@@ -219,8 +221,8 @@ const PricingPage: FC = ({ recentPosts }) => {
"Knowledge transfer",
],
ctaText: "Contact Sales",
- ctaLink:
- "mailto:contact@chillicream.com?subject=Monthly%20Collaboration%20Sessions",
+ ctaTrack: "contact_sales_click",
+ ctaLink: "/services/support/contact?subject=Pricing+%26+Plans",
},
]}
/>
diff --git a/website/src/page-components/services/support/contact.tsx b/website/src/page-components/services/support/contact.tsx
index ca8f6a92c91..ed9c80d291c 100644
--- a/website/src/page-components/services/support/contact.tsx
+++ b/website/src/page-components/services/support/contact.tsx
@@ -1,40 +1,28 @@
"use client";
import { SiteLayout } from "@/components/layout";
-import { ContentSection, SEO, SupportForm } from "@/components/misc";
-import {
- MostRecentBlogPostsSection,
- NewsletterSection,
-} from "@/components/widgets";
-import { RecentBlogPost } from "@/components/widgets/most-recent-blog-posts-section";
-import { SUPPORT_PLANS, SupportPlan } from "@/types/support";
+import { ContactForm } from "@/components/misc/contact-form";
+import { SEO } from "@/components/misc";
+import { CONTACT_SUBJECTS, ContactSubject } from "@/types/support";
import { getValidatedQueryParam } from "@/utils/url-helpers";
import React, { FC, useEffect, useState } from "react";
-interface SupportContactPageProps {
- recentPosts?: RecentBlogPost[];
-}
-
-const SupportContactPage: FC = ({ recentPosts }) => {
- const [selectedPlan, setSelectedPlan] = useState("Startup");
+const ContactPage: FC = () => {
+ const [subject, setSubject] = useState("Schedule a Demo");
useEffect(() => {
- const planFromUrl = getValidatedQueryParam("plan", SUPPORT_PLANS);
- if (planFromUrl) {
- setSelectedPlan(planFromUrl);
+ const subjectFromUrl = getValidatedQueryParam("subject", CONTACT_SUBJECTS);
+ if (subjectFromUrl) {
+ setSubject(subjectFromUrl);
}
}, []);
return (
-
-
-
-
-
-
+
+
);
};
-export default SupportContactPage;
+export default ContactPage;
diff --git a/website/src/types/support.ts b/website/src/types/support.ts
index 5a9540e0776..9b381510e35 100644
--- a/website/src/types/support.ts
+++ b/website/src/types/support.ts
@@ -1,3 +1,13 @@
export const SUPPORT_PLANS = ["Startup", "Business", "Enterprise"] as const;
export type SupportPlan = (typeof SUPPORT_PLANS)[number];
+
+export const CONTACT_SUBJECTS = [
+ "Schedule a Demo",
+ "Pricing & Plans",
+ "Technical Support",
+ "Partnership",
+ "Other",
+] as const;
+
+export type ContactSubject = (typeof CONTACT_SUBJECTS)[number];