Skip to content

Commit 7e810fa

Browse files
committed
feat: enhance contact form with first and last name fields, integrate HubSpot submission, and update localization files
1 parent cf9b788 commit 7e810fa

File tree

8 files changed

+266
-44
lines changed

8 files changed

+266
-44
lines changed

apps/website/README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Main Landing Page of Dokploy
44

5+
## Development
6+
57
Run development server:
68

79
```bash
@@ -14,9 +16,20 @@ yarn dev
1416

1517
Open http://localhost:3000 with your browser to see the result.
1618

19+
## Environment Variables
20+
21+
### Required for Contact Form
22+
```
23+
RESEND_API_KEY=your_resend_api_key_here
24+
```
1725

18-
For Blog Page, you can use the following command to generate the static pages:
26+
### Required for HubSpot Integration (Sales Forms)
27+
```
28+
HUBSPOT_PORTAL_ID=147033433
29+
HUBSPOT_FORM_GUID=0d788925-ef54-4fda-9b76-741fb5877056
30+
```
1931

32+
### Required for Blog Page
2033
```
2134
GHOST_URL=""
2235
GHOST_KEY=""

apps/website/app/[locale]/contact/page.tsx

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import { cn } from "@/lib/utils";
1818

1919
interface ContactFormData {
2020
inquiryType: "" | "support" | "sales" | "other";
21-
name: string;
21+
firstName: string;
22+
lastName: string;
2223
email: string;
2324
company: string;
2425
message: string;
@@ -30,7 +31,8 @@ export default function ContactPage() {
3031
const [isSubmitted, setIsSubmitted] = useState(false);
3132
const [formData, setFormData] = useState<ContactFormData>({
3233
inquiryType: "",
33-
name: "",
34+
firstName: "",
35+
lastName: "",
3436
email: "",
3537
company: "",
3638
message: "",
@@ -43,8 +45,11 @@ export default function ContactPage() {
4345
if (!formData.inquiryType) {
4446
newErrors.inquiryType = t("errors.inquiryTypeRequired");
4547
}
46-
if (!formData.name.trim()) {
47-
newErrors.name = t("errors.nameRequired");
48+
if (!formData.firstName.trim()) {
49+
newErrors.firstName = t("errors.firstNameRequired");
50+
}
51+
if (!formData.lastName.trim()) {
52+
newErrors.lastName = t("errors.lastNameRequired");
4853
}
4954
if (!formData.email.trim()) {
5055
newErrors.email = t("errors.emailRequired");
@@ -91,7 +96,8 @@ export default function ContactPage() {
9196
// Reset form and show success
9297
setFormData({
9398
inquiryType: "",
94-
name: "",
99+
firstName: "",
100+
lastName: "",
95101
email: "",
96102
company: "",
97103
message: "",
@@ -211,45 +217,69 @@ export default function ContactPage() {
211217
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
212218
<div className="space-y-2">
213219
<label
214-
htmlFor="name"
220+
htmlFor="firstName"
215221
className="block text-sm font-medium text-foreground"
216222
>
217-
{t("fields.name.label")}{" "}
223+
{t("fields.firstName.label")}{" "}
218224
<span className="text-red-500">*</span>
219225
</label>
220226
<Input
221-
id="name"
227+
id="firstName"
222228
type="text"
223-
value={formData.name}
224-
onChange={(e) => handleInputChange("name", e.target.value)}
225-
placeholder={t("fields.name.placeholder")}
229+
value={formData.firstName}
230+
onChange={(e) =>
231+
handleInputChange("firstName", e.target.value)
232+
}
233+
placeholder={t("fields.firstName.placeholder")}
226234
/>
227-
{errors.name && (
228-
<p className="text-sm text-red-600">{errors.name}</p>
235+
{errors.firstName && (
236+
<p className="text-sm text-red-600">{errors.firstName}</p>
229237
)}
230238
</div>
231239

232240
<div className="space-y-2">
233241
<label
234-
htmlFor="email"
242+
htmlFor="lastName"
235243
className="block text-sm font-medium text-foreground"
236244
>
237-
{t("fields.email.label")}{" "}
245+
{t("fields.lastName.label")}{" "}
238246
<span className="text-red-500">*</span>
239247
</label>
240248
<Input
241-
id="email"
242-
type="email"
243-
value={formData.email}
244-
onChange={(e) => handleInputChange("email", e.target.value)}
245-
placeholder={t("fields.email.placeholder")}
249+
id="lastName"
250+
type="text"
251+
value={formData.lastName}
252+
onChange={(e) =>
253+
handleInputChange("lastName", e.target.value)
254+
}
255+
placeholder={t("fields.lastName.placeholder")}
246256
/>
247-
{errors.email && (
248-
<p className="text-sm text-red-600">{errors.email}</p>
257+
{errors.lastName && (
258+
<p className="text-sm text-red-600">{errors.lastName}</p>
249259
)}
250260
</div>
251261
</div>
252262

263+
<div className="space-y-2">
264+
<label
265+
htmlFor="email"
266+
className="block text-sm font-medium text-foreground"
267+
>
268+
{t("fields.email.label")}{" "}
269+
<span className="text-red-500">*</span>
270+
</label>
271+
<Input
272+
id="email"
273+
type="email"
274+
value={formData.email}
275+
onChange={(e) => handleInputChange("email", e.target.value)}
276+
placeholder={t("fields.email.placeholder")}
277+
/>
278+
{errors.email && (
279+
<p className="text-sm text-red-600">{errors.email}</p>
280+
)}
281+
</div>
282+
253283
<div className="space-y-2">
254284
<label
255285
htmlFor="company"

apps/website/app/api/contact/route.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type { NextRequest } from "next/server";
22
import { NextResponse } from "next/server";
33
import { Resend } from "resend";
4+
import { submitToHubSpot, getHubSpotUTK } from "@/lib/hubspot";
45

56
interface ContactFormData {
67
inquiryType: "support" | "sales" | "other";
7-
name: string;
8+
firstName: string;
9+
lastName: string;
810
email: string;
911
company: string;
1012
message: string;
@@ -28,7 +30,8 @@ export async function POST(request: NextRequest) {
2830
// Validate required fields
2931
if (
3032
!body.inquiryType ||
31-
!body.name ||
33+
!body.firstName ||
34+
!body.lastName ||
3235
!body.email ||
3336
!body.company ||
3437
!body.message
@@ -48,15 +51,33 @@ export async function POST(request: NextRequest) {
4851
);
4952
}
5053

51-
// Determine recipient email based on inquiry type
54+
// Submit to HubSpot if it's a sales inquiry
55+
if (body.inquiryType === "sales") {
56+
try {
57+
const hutk = getHubSpotUTK(request.headers.get("cookie") || undefined);
58+
const hubspotSuccess = await submitToHubSpot(body, hutk);
59+
60+
if (hubspotSuccess) {
61+
console.log("Successfully submitted sales inquiry to HubSpot");
62+
} else {
63+
console.warn(
64+
"Failed to submit sales inquiry to HubSpot, but continuing with email",
65+
);
66+
}
67+
} catch (error) {
68+
console.error("Error submitting to HubSpot:", error);
69+
// Continue with email even if HubSpot fails
70+
}
71+
}
5272

5373
// Format email content
54-
const emailSubject = `[${body.inquiryType.toUpperCase()}] New contact form submission from ${body.name}`;
74+
const emailSubject = `[${body.inquiryType.toUpperCase()}] New contact form submission from ${body.firstName} ${body.lastName}`;
5575
const emailBody = `
5676
New contact form submission:
5777
5878
Type: ${body.inquiryType}
59-
Name: ${body.name}
79+
First Name: ${body.firstName}
80+
Last Name: ${body.lastName}
6081
Email: ${body.email}
6182
Company: ${body.company}
6283
@@ -83,7 +104,7 @@ Sent from Dokploy website contact form
83104
const confirmationSubject =
84105
"Thank you for contacting Dokploy - We received your message";
85106
const confirmationBody = `
86-
Hello ${body.name},
107+
Hello ${body.firstName} ${body.lastName},
87108
88109
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.
89110

apps/website/lib/hubspot.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
interface HubSpotFormField {
2+
objectTypeId: string;
3+
name: string;
4+
value: string;
5+
}
6+
7+
interface HubSpotFormData {
8+
fields: HubSpotFormField[];
9+
context: {
10+
pageUri: string;
11+
pageName: string;
12+
hutk?: string; // HubSpot UTK from cookies
13+
};
14+
}
15+
16+
interface ContactFormData {
17+
inquiryType: "support" | "sales" | "other";
18+
firstName: string;
19+
lastName: string;
20+
email: string;
21+
company: string;
22+
message: string;
23+
}
24+
25+
/**
26+
* Extract HubSpot UTK (User Token) from cookies
27+
* This is used for tracking and attribution in HubSpot
28+
*/
29+
export function getHubSpotUTK(cookieHeader?: string): string | null {
30+
if (!cookieHeader) return null;
31+
32+
const name = "hubspotutk=";
33+
const decodedCookie = decodeURIComponent(cookieHeader);
34+
const cookieArray = decodedCookie.split(";");
35+
36+
for (let i = 0; i < cookieArray.length; i++) {
37+
const cookie = cookieArray[i].trim();
38+
if (cookie.indexOf(name) === 0) {
39+
return cookie.substring(name.length, cookie.length);
40+
}
41+
}
42+
return null;
43+
}
44+
45+
/**
46+
* Convert contact form data to HubSpot form format
47+
*/
48+
export function formatContactDataForHubSpot(
49+
contactData: ContactFormData,
50+
hutk?: string | null,
51+
): HubSpotFormData {
52+
const formData: HubSpotFormData = {
53+
fields: [
54+
{
55+
objectTypeId: "0-1", // Contact object type
56+
name: "firstname",
57+
value: contactData.firstName,
58+
},
59+
{
60+
objectTypeId: "0-1",
61+
name: "lastname",
62+
value: contactData.lastName,
63+
},
64+
{
65+
objectTypeId: "0-1",
66+
name: "email",
67+
value: contactData.email,
68+
},
69+
{
70+
objectTypeId: "0-1",
71+
name: "message",
72+
value: contactData.message,
73+
},
74+
{
75+
objectTypeId: "0-2", // Company object type
76+
name: "name",
77+
value: contactData.company,
78+
},
79+
],
80+
context: {
81+
pageUri: "https://dokploy.com/contact",
82+
pageName: "Contact Us",
83+
},
84+
};
85+
86+
// Add HubSpot UTK if available
87+
if (hutk) {
88+
formData.context.hutk = hutk;
89+
}
90+
91+
return formData;
92+
}
93+
94+
/**
95+
* Submit form data to HubSpot Forms API
96+
*/
97+
export async function submitToHubSpot(
98+
contactData: ContactFormData,
99+
hutk?: string | null,
100+
): Promise<boolean> {
101+
try {
102+
const portalId = process.env.HUBSPOT_PORTAL_ID;
103+
const formGuid = process.env.HUBSPOT_FORM_GUID;
104+
105+
if (!portalId || !formGuid) {
106+
console.error(
107+
"HubSpot configuration missing: HUBSPOT_PORTAL_ID or HUBSPOT_FORM_GUID not set",
108+
);
109+
return false;
110+
}
111+
112+
const formData = formatContactDataForHubSpot(contactData, hutk);
113+
114+
const response = await fetch(
115+
`https://api.hsforms.com/submissions/v3/integration/submit/${portalId}/${formGuid}`,
116+
{
117+
method: "POST",
118+
headers: {
119+
"Content-Type": "application/json",
120+
},
121+
body: JSON.stringify(formData),
122+
},
123+
);
124+
125+
if (!response.ok) {
126+
const errorText = await response.text();
127+
console.error("HubSpot API error:", response.status, errorText);
128+
return false;
129+
}
130+
131+
const result = await response.json();
132+
console.log("HubSpot submission successful:", result);
133+
return true;
134+
} catch (error) {
135+
console.error("Error submitting to HubSpot:", error);
136+
return false;
137+
}
138+
}

apps/website/locales/en.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,13 @@
233233
"other": "Other"
234234
}
235235
},
236-
"name": {
237-
"label": "Name",
238-
"placeholder": "Your full name"
236+
"firstName": {
237+
"label": "First Name",
238+
"placeholder": "Your first name"
239+
},
240+
"lastName": {
241+
"label": "Last Name",
242+
"placeholder": "Your last name"
239243
},
240244
"email": {
241245
"label": "Email",
@@ -257,7 +261,8 @@
257261
},
258262
"errors": {
259263
"inquiryTypeRequired": "Please select what we can help you with",
260-
"nameRequired": "Name is required",
264+
"firstNameRequired": "First name is required",
265+
"lastNameRequired": "Last name is required",
261266
"emailRequired": "Email is required",
262267
"emailInvalid": "Please enter a valid email address",
263268
"companyRequired": "Company name is required",

0 commit comments

Comments
 (0)