From cb0ac7f41dbf6c295a3bde60e5a7ed96a74bc40b Mon Sep 17 00:00:00 2001
From: Paul Wackerow <54227730+wackerow@users.noreply.github.com>
Date: Thu, 24 Jul 2025 13:54:24 -0700
Subject: [PATCH 01/64] feat: initialize enterprise contact form
---
.../_components/ContactForm/index.tsx | 37 ++++++++++++++++++
.../_components/ContactForm/lazy.tsx | 5 +++
.../_components/ContactForm/loading.tsx | 10 +++++
app/[locale]/enterprise/page.tsx | 17 ++++-----
src/components/ui/textarea.tsx | 38 +++++++++++++++++++
src/intl/en/common.json | 2 +
src/intl/en/page-enterprise.json | 5 ++-
7 files changed, 102 insertions(+), 12 deletions(-)
create mode 100644 app/[locale]/enterprise/_components/ContactForm/index.tsx
create mode 100644 app/[locale]/enterprise/_components/ContactForm/lazy.tsx
create mode 100644 app/[locale]/enterprise/_components/ContactForm/loading.tsx
create mode 100644 src/components/ui/textarea.tsx
diff --git a/app/[locale]/enterprise/_components/ContactForm/index.tsx b/app/[locale]/enterprise/_components/ContactForm/index.tsx
new file mode 100644
index 00000000000..421f7b5e7e8
--- /dev/null
+++ b/app/[locale]/enterprise/_components/ContactForm/index.tsx
@@ -0,0 +1,37 @@
+"use client"
+
+import { Button } from "@/components/ui/buttons/Button"
+import Input from "@/components/ui/input"
+import { Textarea } from "@/components/ui/textarea"
+
+type EnterpriseContactFormProps = {
+ emailPlaceholder: string
+ bodyPlaceholder: string
+ buttonLabel: string
+}
+
+const EnterpriseContactForm = ({
+ emailPlaceholder,
+ bodyPlaceholder,
+ buttonLabel,
+}: EnterpriseContactFormProps) => (
+
+
+
+
+
+)
+
+export default EnterpriseContactForm
diff --git a/app/[locale]/enterprise/_components/ContactForm/lazy.tsx b/app/[locale]/enterprise/_components/ContactForm/lazy.tsx
new file mode 100644
index 00000000000..c960f08e647
--- /dev/null
+++ b/app/[locale]/enterprise/_components/ContactForm/lazy.tsx
@@ -0,0 +1,5 @@
+import dynamic from "next/dynamic"
+
+import Loading from "./loading"
+
+export default dynamic(() => import("."), { ssr: false, loading: Loading })
diff --git a/app/[locale]/enterprise/_components/ContactForm/loading.tsx b/app/[locale]/enterprise/_components/ContactForm/loading.tsx
new file mode 100644
index 00000000000..bc019341894
--- /dev/null
+++ b/app/[locale]/enterprise/_components/ContactForm/loading.tsx
@@ -0,0 +1,10 @@
+import { Skeleton } from "@/components/ui/skeleton"
+
+const Loading = () => (
+
+
+
+
+)
+
+export default Loading
diff --git a/app/[locale]/enterprise/page.tsx b/app/[locale]/enterprise/page.tsx
index f28a4941ca9..77cf82ed551 100644
--- a/app/[locale]/enterprise/page.tsx
+++ b/app/[locale]/enterprise/page.tsx
@@ -48,6 +48,7 @@ import { getMetadata } from "@/lib/utils/metadata"
import { BASE_TIME_UNIT } from "@/lib/constants"
import CasesColumn from "./_components/CasesColumn"
+import EnterpriseContactForm from "./_components/ContactForm/lazy"
import FeatureCard from "./_components/FeatureCard"
import { ENTERPRISE_MAILTO } from "./constants"
import type { Case, EcosystemPlayer, Feature } from "./types"
@@ -95,6 +96,7 @@ const Page = async ({ params }: { params: { locale: Lang } }) => {
const { locale } = params
const t = await getTranslations({ locale, namespace: "page-enterprise" })
+ const tCommon = await getTranslations({ locale, namespace: "common" })
const [
{ txCount, txCostsMedianUsd },
@@ -492,16 +494,11 @@ const Page = async ({ params }: { params: { locale: Lang } }) => {
{t("page-enterprise-team-description")}
-
- {t("page-enterprise-hero-cta")}
-
+
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
new file mode 100644
index 00000000000..a2c21393a23
--- /dev/null
+++ b/src/components/ui/textarea.tsx
@@ -0,0 +1,38 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils/cn"
+
+const textareaVariants = cva(
+ "focus-visible:ring-ring flex min-h-[200px] w-full rounded border border-body bg-background px-3 py-2 text-base ring-offset-background placeholder:text-disabled focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+ {
+ variants: {
+ size: {
+ md: "p-2",
+ sm: "p-1 text-sm",
+ },
+ },
+ defaultVariants: {
+ size: "md",
+ },
+ }
+)
+
+export interface TextareaProps
+ extends Omit, "size">,
+ VariantProps {}
+
+const Textarea = React.forwardRef(
+ ({ className, size, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Textarea.displayName = "Textarea"
+
+export { Textarea }
diff --git a/src/intl/en/common.json b/src/intl/en/common.json
index 9f7b0a15275..19bd75cf405 100644
--- a/src/intl/en/common.json
+++ b/src/intl/en/common.json
@@ -415,6 +415,7 @@
"secret-leader-election": "Secret leader election",
"security": "Security",
"see-contributors": "See contributors",
+ "set-up-a-call": "Set up a call",
"set-up-local-env": "Set up local environment",
"sharding": "Sharding",
"show-all": "Show all",
@@ -464,5 +465,6 @@
"withdrawals": "Staking withdrawals",
"wrapped-ether": "Wrapped Ether",
"yes": "Yes",
+ "your-email": "Your e-mail",
"zero-knowledge-proofs": "Zero-knowledge proofs"
}
diff --git a/src/intl/en/page-enterprise.json b/src/intl/en/page-enterprise.json
index 81dd4b87f24..b219d8779ee 100644
--- a/src/intl/en/page-enterprise.json
+++ b/src/intl/en/page-enterprise.json
@@ -40,8 +40,9 @@
"page-enterprise-reason-3-header": "Reduced risk",
"page-enterprise-reason-4-content": "The only programmable distributed ledger infrastructure that has never gone down, and has thrived through multiple existential threats.",
"page-enterprise-reason-4-header": "Battle-tested",
- "page-enterprise-team-description": "We will answer your questions, help identify potential paths forward, provide technical support, and connect you with relevant industry leaders.",
- "page-enterprise-team-header": "EF Enterprise Team",
+ "page-enterprise-team-description": "We will answer your questions, help identify potential paths forward, provide technical support and connect you with relevant industry leaders.",
+ "page-enterprise-team-header": "Ethereum Enterprise Team",
+ "page-enterprise-team-form-placeholder": "Tell us about your project",
"page-enterprise-why-description": "Ethereum supports enterprise compliance with transparent, auditable infrastructure that aligns with GDPR and KYC while protecting sensitive data in private or consortium environments.",
"page-enterprise-why-header": "Why Ethereum"
}
From ccf2661b7af1f12c06e5397d84c6e7ae6ee2e08e Mon Sep 17 00:00:00 2001
From: Paul Wackerow <54227730+wackerow@users.noreply.github.com>
Date: Fri, 25 Jul 2025 13:45:52 -0700
Subject: [PATCH 02/64] feat: add ui/textarea
---
src/components/ui/textarea.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
index a2c21393a23..9c0680003c9 100644
--- a/src/components/ui/textarea.tsx
+++ b/src/components/ui/textarea.tsx
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils/cn"
const textareaVariants = cva(
- "focus-visible:ring-ring flex min-h-[200px] w-full rounded border border-body bg-background px-3 py-2 text-base ring-offset-background placeholder:text-disabled focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+ "focus-visible:outline focus-visible:outline-primary-hover focus-visible:outline-[3px] focus-visible:-outline-offset-2 flex min-h-[200px] w-full rounded border border-body bg-background px-3 py-2 text-base ring-offset-background placeholder:text-disabled disabled:cursor-not-allowed disabled:opacity-50 hover:not-disabled:border-primary-hover",
{
variants: {
size: {
From 0ea57753b83c4b4cbe7febc29dc83255e63cd96c Mon Sep 17 00:00:00 2001
From: Paul Wackerow <54227730+wackerow@users.noreply.github.com>
Date: Fri, 25 Jul 2025 20:04:05 -0700
Subject: [PATCH 03/64] feat: build out contact form
---
.../_components/ContactForm/index.tsx | 269 ++++++++++++++++--
app/[locale]/enterprise/constants.ts | 2 +
app/[locale]/enterprise/page.tsx | 33 ++-
src/intl/en/common.json | 2 -
src/intl/en/page-enterprise.json | 11 +-
5 files changed, 282 insertions(+), 35 deletions(-)
diff --git a/app/[locale]/enterprise/_components/ContactForm/index.tsx b/app/[locale]/enterprise/_components/ContactForm/index.tsx
index 421f7b5e7e8..51eb2d69f72 100644
--- a/app/[locale]/enterprise/_components/ContactForm/index.tsx
+++ b/app/[locale]/enterprise/_components/ContactForm/index.tsx
@@ -1,37 +1,254 @@
"use client"
+import React, { useState } from "react"
+import { HeartHandshake } from "lucide-react"
+
import { Button } from "@/components/ui/buttons/Button"
import Input from "@/components/ui/input"
+import { Spinner } from "@/components/ui/spinner"
import { Textarea } from "@/components/ui/textarea"
+import { CONTACT_FORM_CHAR_MIN } from "../../constants"
+
type EnterpriseContactFormProps = {
- emailPlaceholder: string
- bodyPlaceholder: string
- buttonLabel: string
+ strings: {
+ error: {
+ domain: string
+ emailInvalid: string
+ general: string
+ minLength: React.ReactNode // constant injected into span
+ required: string
+ }
+ placeholder: {
+ input: string
+ textarea: string
+ }
+ button: {
+ label: string
+ loading: string
+ }
+ success: {
+ heading: string
+ message: string
+ }
+ }
}
-const EnterpriseContactForm = ({
- emailPlaceholder,
- bodyPlaceholder,
- buttonLabel,
-}: EnterpriseContactFormProps) => (
-
-
-
-
-
-)
+type FormState = {
+ email: string
+ message: string
+}
+
+type FormErrors = {
+ email?: string
+ message?: React.ReactNode
+ general?: string
+}
+
+type SubmissionState = "idle" | "submitting" | "success" | "error"
+
+// Consumer email domains to block
+const CONSUMER_DOMAINS = [
+ "gmail.com",
+ "yahoo.com",
+ "hotmail.com",
+ "outlook.com",
+ "icloud.com",
+ "protonmail.com",
+ "proton.me",
+ "pm.me",
+ "aol.com",
+ "mail.com",
+ "yandex.com",
+ "tutanota.com",
+ "fastmail.com",
+ "zoho.com",
+ "gmx.com",
+ "live.com",
+ "msn.com",
+ "me.com",
+ "mac.com",
+ "rocketmail.com",
+ "yahoo.co.uk",
+ "googlemail.com",
+ "mailinator.com",
+ "10minutemail.com",
+ "guerrillamail.com",
+]
+
+const sanitizeInput = (input: string): string =>
+ input
+ .replace(/