From 8e2e8ddc78f2b8b6470f6e70db76af41deb93651 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 18 Feb 2026 21:03:15 -0600 Subject: [PATCH 1/6] feat: add Partners page and PartnerForm component - Introduced a new Partners page with program details and a call-to-action for potential partners. - Implemented a PartnerForm component for user inquiries, including validation and submission handling. - Added pricing feature data structure and a PricingFeatureTable component to display pricing options and features. - Updated UI components for better integration with the new features. --- apps/website/app/partners/page.tsx | 152 +++++ apps/website/components/PartnerForm.tsx | 239 +++++++ apps/website/components/pricing-legacy.tsx | 430 ++++++++++++ apps/website/components/pricing.tsx | 620 ++++++++---------- .../pricing/PricingFeatureTable.tsx | 76 +++ .../components/pricing/pricing-data.ts | 100 +++ apps/website/components/ui/badge.tsx | 1 + 7 files changed, 1277 insertions(+), 341 deletions(-) create mode 100644 apps/website/app/partners/page.tsx create mode 100644 apps/website/components/PartnerForm.tsx create mode 100644 apps/website/components/pricing-legacy.tsx create mode 100644 apps/website/components/pricing/PricingFeatureTable.tsx create mode 100644 apps/website/components/pricing/pricing-data.ts diff --git a/apps/website/app/partners/page.tsx b/apps/website/app/partners/page.tsx new file mode 100644 index 00000000..3c657bef --- /dev/null +++ b/apps/website/app/partners/page.tsx @@ -0,0 +1,152 @@ +import { Container } from "@/components/Container"; +import { PartnerForm } from "@/components/PartnerForm"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import AnimatedGridPattern from "@/components/ui/animated-grid-pattern"; +import { Check } from "lucide-react"; +import Link from "next/link"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Partners", + description: + "Join the Dokploy partner program. Agency plan, referral program, and reseller options.", +}; + +const PROGRAMS = [ + { + title: "Agency Plan", + badge: "Available", + badgeVariant: "default" as const, + description: + "Premium licensing tier designed for agencies managing multiple clients.", + features: [ + "White-label capabilities", + "Unlimited servers", + "Unlimited projects", + "Unlimited organizations", + ], + cta: "Get started", + href: "#get-started", + }, + { + title: "Referral Program", + badge: "Available", + badgeVariant: "default" as const, + description: + "Earn 20% commission on every customer you refer to Dokploy.", + features: [ + "Co-marketing opportunities", + "Partner dashboard", + "Unique referral links", + "20% of first-year revenue", + ], + cta: "Get started", + href: "#get-started", + }, + { + title: "Reseller Program", + badge: "Coming Soon", + badgeVariant: "secondary" as const, + description: + "Sell Dokploy directly in your market with local presence and relationships.", + features: [ + "Strategic market access", + "Cultural advantage", + "Leverage local expertise", + "Market expansion opportunity", + ], + cta: "Express interest", + href: "#get-started", + }, +]; + +export default function PartnersPage() { + return ( +
+ + {/* Hero */} +
+ +
+

+ Partner with Dokploy +

+

+ Join our partner program to unlock premium features, earn revenue + through referrals, and scale your agency operations. +

+ +
+
+
+ + {/* Program cards */} +
+ +
+ {PROGRAMS.map((program) => ( +
+ + {program.badge} + +

+ {program.title} +

+

+ {program.description} +

+
    + {program.features.map((f) => ( +
  • + + {f} +
  • + ))} +
+
+ +
+
+ ))} +
+
+
+ + {/* Get Started / Form */} +
+ +
+

+ Get Started +

+

+ Join our partner program and start growing with Dokploy. +

+
+ +
+
+
+
+
+ ); +} diff --git a/apps/website/components/PartnerForm.tsx b/apps/website/components/PartnerForm.tsx new file mode 100644 index 00000000..a70cc08c --- /dev/null +++ b/apps/website/components/PartnerForm.tsx @@ -0,0 +1,239 @@ +"use client"; + +import { trackGAEvent } from "@/components/analitycs"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useState } from "react"; + +const PROGRAM_OPTIONS = [ + { value: "agency", label: "Agency Plan" }, + { value: "referral", label: "Referral Program" }, + { value: "reseller", label: "Reseller Program" }, + { value: "all", label: "All Programs" }, +] as const; + +interface PartnerFormData { + firstName: string; + lastName: string; + email: string; + company: string; + programInterest: string; + message: string; +} + +export function PartnerForm() { + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + const [formData, setFormData] = useState({ + firstName: "", + lastName: "", + email: "", + company: "", + programInterest: "", + message: "", + }); + const [errors, setErrors] = useState>({}); + + const validate = (): boolean => { + const newErrors: Record = {}; + if (!formData.firstName.trim()) newErrors.firstName = "Required"; + if (!formData.lastName.trim()) newErrors.lastName = "Required"; + if (!formData.email.trim()) newErrors.email = "Required"; + else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = "Invalid email"; + } + if (!formData.company.trim()) newErrors.company = "Required"; + if (!formData.programInterest) newErrors.programInterest = "Required"; + if (!formData.message.trim()) newErrors.message = "Required"; + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!validate()) return; + + setIsSubmitting(true); + try { + const programLabel = + PROGRAM_OPTIONS.find((o) => o.value === formData.programInterest) + ?.label ?? formData.programInterest; + const fullMessage = `Program Interest: ${programLabel}\n\n${formData.message}`; + + const response = await fetch("/api/contact", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + inquiryType: "sales", + firstName: formData.firstName, + lastName: formData.lastName, + email: formData.email, + company: formData.company, + message: fullMessage, + }), + }); + + if (response.ok) { + trackGAEvent({ + action: "Partner Form Submitted", + category: "Partners", + label: formData.programInterest, + }); + setIsSubmitted(true); + } else { + throw new Error("Failed to submit"); + } + } catch { + setErrors({ message: "Something went wrong. Please try again." }); + } finally { + setIsSubmitting(false); + } + }; + + if (isSubmitted) { + return ( +
+

+ Thank you for your interest +

+

+ We've received your message and will get back to you soon. +

+
+ ); + } + + return ( +
+
+
+ + { + setFormData((p) => ({ ...p, firstName: e.target.value })); + if (errors.firstName) setErrors((p) => ({ ...p, firstName: "" })); + }} + /> + {errors.firstName && ( +

{errors.firstName}

+ )} +
+
+ + { + setFormData((p) => ({ ...p, lastName: e.target.value })); + if (errors.lastName) setErrors((p) => ({ ...p, lastName: "" })); + }} + /> + {errors.lastName && ( +

{errors.lastName}

+ )} +
+
+
+ + { + setFormData((p) => ({ ...p, email: e.target.value })); + if (errors.email) setErrors((p) => ({ ...p, email: "" })); + }} + /> + {errors.email && ( +

{errors.email}

+ )} +
+
+ + { + setFormData((p) => ({ ...p, company: e.target.value })); + if (errors.company) setErrors((p) => ({ ...p, company: "" })); + }} + /> + {errors.company && ( +

{errors.company}

+ )} +
+
+ + + {errors.programInterest && ( +

+ {errors.programInterest} +

+ )} +
+
+ +