diff --git a/packages/twenty-website-new/.env.example b/packages/twenty-website-new/.env.example new file mode 100644 index 0000000000..d27ad4304f --- /dev/null +++ b/packages/twenty-website-new/.env.example @@ -0,0 +1 @@ +PARTNER_APPLICATION_WEBHOOK_URL= diff --git a/packages/twenty-website-new/.gitignore b/packages/twenty-website-new/.gitignore index 301c67f0cd..b23d143603 100644 --- a/packages/twenty-website-new/.gitignore +++ b/packages/twenty-website-new/.gitignore @@ -26,6 +26,7 @@ yarn-debug.log* yarn-error.log* # local env files +.env .env*.local # vercel diff --git a/packages/twenty-website-new/package.json b/packages/twenty-website-new/package.json index 8e5ddc718a..67b684a8b5 100644 --- a/packages/twenty-website-new/package.json +++ b/packages/twenty-website-new/package.json @@ -29,7 +29,8 @@ "react-dom": "19.2.3", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", - "three": "^0.183.2" + "three": "^0.183.2", + "zod": "^4.1.11" }, "devDependencies": { "@types/node": "^20", diff --git a/packages/twenty-website-new/public/images/home/testimonials/background-shape.webp b/packages/twenty-website-new/public/images/home/testimonials/background-shape.webp deleted file mode 100644 index 0e85b2351e..0000000000 Binary files a/packages/twenty-website-new/public/images/home/testimonials/background-shape.webp and /dev/null differ diff --git a/packages/twenty-website-new/public/images/partner/testimonials/background-shape.webp b/packages/twenty-website-new/public/images/partner/testimonials/background-shape.webp deleted file mode 100644 index 10a44697d2..0000000000 Binary files a/packages/twenty-website-new/public/images/partner/testimonials/background-shape.webp and /dev/null differ diff --git a/packages/twenty-website-new/src/app/(home)/page.tsx b/packages/twenty-website-new/src/app/(home)/page.tsx index 829b77b352..e62baaf2a0 100644 --- a/packages/twenty-website-new/src/app/(home)/page.tsx +++ b/packages/twenty-website-new/src/app/(home)/page.tsx @@ -210,7 +210,6 @@ export default async function HomePage() { (null); + const formRef = useRef(null); const dropdownRef = useRef(null); const [programId, setProgramId] = useState('technology'); const [dropdownOpen, setDropdownOpen] = useState(false); + const [submitError, setSubmitError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const handleOverlayPointerDown = useCallback( (event: React.PointerEvent) => { @@ -387,6 +369,19 @@ export function PartnerApplicationModal({ [onClose], ); + useEffect(() => { + if (open) { + setSubmitError(null); + return; + } + + formRef.current?.reset(); + setProgramId('technology'); + setDropdownOpen(false); + setSubmitError(null); + setIsSubmitting(false); + }, [open]); + useEffect(() => { if (!open) { return; @@ -427,10 +422,67 @@ export function PartnerApplicationModal({ ); const handleSubmit = useCallback( - (event: React.FormEvent) => { + async (event: React.FormEvent) => { event.preventDefault(); + + if (isSubmitting) { + return; + } + + const formData = new FormData(event.currentTarget); + + const nameValue = formData.get('name'); + const emailValue = formData.get('email'); + const companyValue = formData.get('company'); + const websiteValue = formData.get('website'); + const messageValue = formData.get('message'); + + const name = typeof nameValue === 'string' ? nameValue.trim() : ''; + const email = typeof emailValue === 'string' ? emailValue.trim() : ''; + const company = + typeof companyValue === 'string' ? companyValue.trim() : ''; + const website = + typeof websiteValue === 'string' ? websiteValue.trim() : ''; + const message = + typeof messageValue === 'string' ? messageValue.trim() : ''; + + const validationCopy = PARTNER_APPLICATION_MODAL_COPY.validation; + const emailLooksValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + + setSubmitError(null); + + if (!name || !email || !company || !website || !message) { + setSubmitError(validationCopy.incompleteForm); + return; + } + + if (!emailLooksValid) { + setSubmitError(validationCopy.invalidEmail); + return; + } + + setIsSubmitting(true); + + try { + const response = await fetch('/api/partner-application', { + body: JSON.stringify({ email, name }), + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + }); + + if (!response.ok) { + setSubmitError(validationCopy.submitFailed); + return; + } + + onClose(); + } catch { + setSubmitError(validationCopy.submitFailed); + } finally { + setIsSubmitting(false); + } }, - [], + [isSubmitting, onClose], ); if (!open) { @@ -448,17 +500,34 @@ export function PartnerApplicationModal({ role="dialog" > - - {copy.titleSerif} - <TitleAccent>{copy.titleSans}</TitleAccent> - - -

{copy.subtitleLine1}

-

{copy.subtitleLine2}

-
+ + + + + + +
-
+ {PARTNER_PROGRAM_OPTIONS.map((option) => ( @@ -524,57 +593,69 @@ export function PartnerApplicationModal({