feat: enhance contact form with first and last name fields, integrate HubSpot submission, and update localization files

This commit is contained in:
Mauricio Siu
2025-10-11 00:52:26 -06:00
parent cf9b788bd5
commit 7e810fa809
8 changed files with 266 additions and 44 deletions

View File

@@ -2,6 +2,8 @@
Main Landing Page of Dokploy
## Development
Run development server:
```bash
@@ -14,9 +16,20 @@ yarn dev
Open http://localhost:3000 with your browser to see the result.
## Environment Variables
For Blog Page, you can use the following command to generate the static pages:
### Required for Contact Form
```
RESEND_API_KEY=your_resend_api_key_here
```
### Required for HubSpot Integration (Sales Forms)
```
HUBSPOT_PORTAL_ID=147033433
HUBSPOT_FORM_GUID=0d788925-ef54-4fda-9b76-741fb5877056
```
### Required for Blog Page
```
GHOST_URL=""
GHOST_KEY=""

View File

@@ -18,7 +18,8 @@ import { cn } from "@/lib/utils";
interface ContactFormData {
inquiryType: "" | "support" | "sales" | "other";
name: string;
firstName: string;
lastName: string;
email: string;
company: string;
message: string;
@@ -30,7 +31,8 @@ export default function ContactPage() {
const [isSubmitted, setIsSubmitted] = useState(false);
const [formData, setFormData] = useState<ContactFormData>({
inquiryType: "",
name: "",
firstName: "",
lastName: "",
email: "",
company: "",
message: "",
@@ -43,8 +45,11 @@ export default function ContactPage() {
if (!formData.inquiryType) {
newErrors.inquiryType = t("errors.inquiryTypeRequired");
}
if (!formData.name.trim()) {
newErrors.name = t("errors.nameRequired");
if (!formData.firstName.trim()) {
newErrors.firstName = t("errors.firstNameRequired");
}
if (!formData.lastName.trim()) {
newErrors.lastName = t("errors.lastNameRequired");
}
if (!formData.email.trim()) {
newErrors.email = t("errors.emailRequired");
@@ -91,7 +96,8 @@ export default function ContactPage() {
// Reset form and show success
setFormData({
inquiryType: "",
name: "",
firstName: "",
lastName: "",
email: "",
company: "",
message: "",
@@ -211,45 +217,69 @@ export default function ContactPage() {
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div className="space-y-2">
<label
htmlFor="name"
htmlFor="firstName"
className="block text-sm font-medium text-foreground"
>
{t("fields.name.label")}{" "}
{t("fields.firstName.label")}{" "}
<span className="text-red-500">*</span>
</label>
<Input
id="name"
id="firstName"
type="text"
value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)}
placeholder={t("fields.name.placeholder")}
value={formData.firstName}
onChange={(e) =>
handleInputChange("firstName", e.target.value)
}
placeholder={t("fields.firstName.placeholder")}
/>
{errors.name && (
<p className="text-sm text-red-600">{errors.name}</p>
{errors.firstName && (
<p className="text-sm text-red-600">{errors.firstName}</p>
)}
</div>
<div className="space-y-2">
<label
htmlFor="email"
htmlFor="lastName"
className="block text-sm font-medium text-foreground"
>
{t("fields.email.label")}{" "}
{t("fields.lastName.label")}{" "}
<span className="text-red-500">*</span>
</label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder={t("fields.email.placeholder")}
id="lastName"
type="text"
value={formData.lastName}
onChange={(e) =>
handleInputChange("lastName", e.target.value)
}
placeholder={t("fields.lastName.placeholder")}
/>
{errors.email && (
<p className="text-sm text-red-600">{errors.email}</p>
{errors.lastName && (
<p className="text-sm text-red-600">{errors.lastName}</p>
)}
</div>
</div>
<div className="space-y-2">
<label
htmlFor="email"
className="block text-sm font-medium text-foreground"
>
{t("fields.email.label")}{" "}
<span className="text-red-500">*</span>
</label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder={t("fields.email.placeholder")}
/>
{errors.email && (
<p className="text-sm text-red-600">{errors.email}</p>
)}
</div>
<div className="space-y-2">
<label
htmlFor="company"

View File

@@ -1,10 +1,12 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { Resend } from "resend";
import { submitToHubSpot, getHubSpotUTK } from "@/lib/hubspot";
interface ContactFormData {
inquiryType: "support" | "sales" | "other";
name: string;
firstName: string;
lastName: string;
email: string;
company: string;
message: string;
@@ -28,7 +30,8 @@ export async function POST(request: NextRequest) {
// Validate required fields
if (
!body.inquiryType ||
!body.name ||
!body.firstName ||
!body.lastName ||
!body.email ||
!body.company ||
!body.message
@@ -48,15 +51,33 @@ export async function POST(request: NextRequest) {
);
}
// Determine recipient email based on inquiry type
// Submit to HubSpot if it's a sales inquiry
if (body.inquiryType === "sales") {
try {
const hutk = getHubSpotUTK(request.headers.get("cookie") || undefined);
const hubspotSuccess = await submitToHubSpot(body, hutk);
if (hubspotSuccess) {
console.log("Successfully submitted sales inquiry to HubSpot");
} else {
console.warn(
"Failed to submit sales inquiry to HubSpot, but continuing with email",
);
}
} catch (error) {
console.error("Error submitting to HubSpot:", error);
// Continue with email even if HubSpot fails
}
}
// Format email content
const emailSubject = `[${body.inquiryType.toUpperCase()}] New contact form submission from ${body.name}`;
const emailSubject = `[${body.inquiryType.toUpperCase()}] New contact form submission from ${body.firstName} ${body.lastName}`;
const emailBody = `
New contact form submission:
Type: ${body.inquiryType}
Name: ${body.name}
First Name: ${body.firstName}
Last Name: ${body.lastName}
Email: ${body.email}
Company: ${body.company}
@@ -83,7 +104,7 @@ Sent from Dokploy website contact form
const confirmationSubject =
"Thank you for contacting Dokploy - We received your message";
const confirmationBody = `
Hello ${body.name},
Hello ${body.firstName} ${body.lastName},
Thank you for reaching out to us! We have successfully received your message and our team will get back to you as soon as possible.

138
apps/website/lib/hubspot.ts Normal file
View File

@@ -0,0 +1,138 @@
interface HubSpotFormField {
objectTypeId: string;
name: string;
value: string;
}
interface HubSpotFormData {
fields: HubSpotFormField[];
context: {
pageUri: string;
pageName: string;
hutk?: string; // HubSpot UTK from cookies
};
}
interface ContactFormData {
inquiryType: "support" | "sales" | "other";
firstName: string;
lastName: string;
email: string;
company: string;
message: string;
}
/**
* Extract HubSpot UTK (User Token) from cookies
* This is used for tracking and attribution in HubSpot
*/
export function getHubSpotUTK(cookieHeader?: string): string | null {
if (!cookieHeader) return null;
const name = "hubspotutk=";
const decodedCookie = decodeURIComponent(cookieHeader);
const cookieArray = decodedCookie.split(";");
for (let i = 0; i < cookieArray.length; i++) {
const cookie = cookieArray[i].trim();
if (cookie.indexOf(name) === 0) {
return cookie.substring(name.length, cookie.length);
}
}
return null;
}
/**
* Convert contact form data to HubSpot form format
*/
export function formatContactDataForHubSpot(
contactData: ContactFormData,
hutk?: string | null,
): HubSpotFormData {
const formData: HubSpotFormData = {
fields: [
{
objectTypeId: "0-1", // Contact object type
name: "firstname",
value: contactData.firstName,
},
{
objectTypeId: "0-1",
name: "lastname",
value: contactData.lastName,
},
{
objectTypeId: "0-1",
name: "email",
value: contactData.email,
},
{
objectTypeId: "0-1",
name: "message",
value: contactData.message,
},
{
objectTypeId: "0-2", // Company object type
name: "name",
value: contactData.company,
},
],
context: {
pageUri: "https://dokploy.com/contact",
pageName: "Contact Us",
},
};
// Add HubSpot UTK if available
if (hutk) {
formData.context.hutk = hutk;
}
return formData;
}
/**
* Submit form data to HubSpot Forms API
*/
export async function submitToHubSpot(
contactData: ContactFormData,
hutk?: string | null,
): Promise<boolean> {
try {
const portalId = process.env.HUBSPOT_PORTAL_ID;
const formGuid = process.env.HUBSPOT_FORM_GUID;
if (!portalId || !formGuid) {
console.error(
"HubSpot configuration missing: HUBSPOT_PORTAL_ID or HUBSPOT_FORM_GUID not set",
);
return false;
}
const formData = formatContactDataForHubSpot(contactData, hutk);
const response = await fetch(
`https://api.hsforms.com/submissions/v3/integration/submit/${portalId}/${formGuid}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
},
);
if (!response.ok) {
const errorText = await response.text();
console.error("HubSpot API error:", response.status, errorText);
return false;
}
const result = await response.json();
console.log("HubSpot submission successful:", result);
return true;
} catch (error) {
console.error("Error submitting to HubSpot:", error);
return false;
}
}

View File

@@ -233,9 +233,13 @@
"other": "Other"
}
},
"name": {
"label": "Name",
"placeholder": "Your full name"
"firstName": {
"label": "First Name",
"placeholder": "Your first name"
},
"lastName": {
"label": "Last Name",
"placeholder": "Your last name"
},
"email": {
"label": "Email",
@@ -257,7 +261,8 @@
},
"errors": {
"inquiryTypeRequired": "Please select what we can help you with",
"nameRequired": "Name is required",
"firstNameRequired": "First name is required",
"lastNameRequired": "Last name is required",
"emailRequired": "Email is required",
"emailInvalid": "Please enter a valid email address",
"companyRequired": "Company name is required",

View File

@@ -233,9 +233,13 @@
"other": "Otro"
}
},
"name": {
"firstName": {
"label": "Nombre",
"placeholder": "Tu nombre completo"
"placeholder": "Tu nombre"
},
"lastName": {
"label": "Apellido",
"placeholder": "Tu apellido"
},
"email": {
"label": "Correo electrónico",
@@ -257,7 +261,8 @@
},
"errors": {
"inquiryTypeRequired": "Por favor selecciona en qué podemos ayudarte",
"nameRequired": "El nombre es obligatorio",
"firstNameRequired": "El nombre es obligatorio",
"lastNameRequired": "El apellido es obligatorio",
"emailRequired": "El correo electrónico es obligatorio",
"emailInvalid": "Por favor ingresa un correo electrónico válido",
"companyRequired": "El nombre de la empresa es obligatorio",

View File

@@ -227,9 +227,13 @@
"other": "Autre"
}
},
"name": {
"label": "Nom",
"placeholder": "Votre nom complet"
"firstName": {
"label": "Prénom",
"placeholder": "Votre prénom"
},
"lastName": {
"label": "Nom de famille",
"placeholder": "Votre nom de famille"
},
"email": {
"label": "Email",
@@ -251,7 +255,8 @@
},
"errors": {
"inquiryTypeRequired": "Veuillez sélectionner comment nous pouvons vous aider",
"nameRequired": "Le nom est obligatoire",
"firstNameRequired": "Le prénom est obligatoire",
"lastNameRequired": "Le nom de famille est obligatoire",
"emailRequired": "L'email est obligatoire",
"emailInvalid": "Veuillez saisir une adresse email valide",
"companyRequired": "Le nom de l'entreprise est obligatoire",

View File

@@ -238,9 +238,13 @@
"other": "其他"
}
},
"name": {
"label": "名",
"placeholder": "您的名"
"firstName": {
"label": "名",
"placeholder": "您的名"
},
"lastName": {
"label": "姓",
"placeholder": "您的姓"
},
"email": {
"label": "邮箱",
@@ -262,7 +266,8 @@
},
"errors": {
"inquiryTypeRequired": "请选择我们可以为您提供的帮助",
"nameRequired": "名为必填项",
"firstNameRequired": "名为必填项",
"lastNameRequired": "姓为必填项",
"emailRequired": "邮箱为必填项",
"emailInvalid": "请输入有效的邮箱地址",
"companyRequired": "公司名称为必填项",