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 + + + + + +
+ + + + updateField("name", e.target.value)} + $hasError={!!errors.name} + disabled={isSubmitting} + /> + {errors.name && {errors.name}} + + + + + updateField("email", e.target.value)} + $hasError={!!errors.email} + disabled={isSubmitting} + /> + {errors.email && {errors.email}} + + + + + + + updateField("company", e.target.value)} + $hasError={!!errors.company} + disabled={isSubmitting} + /> + {errors.company && {errors.company}} + + + + + + + + + + +