mirror of
https://github.com/Dokploy/website.git
synced 2026-06-15 20:25:25 +02:00
feat: enhance contact form with first and last name fields, integrate HubSpot submission, and update localization files
This commit is contained in:
@@ -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=""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
138
apps/website/lib/hubspot.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "公司名称为必填项",
|
||||
|
||||
Reference in New Issue
Block a user