Skip to content

Commit 66bcafa

Browse files
committed
feat: implement new contact form and modal for improved user inquiries
- Introduced a new ContactForm component to streamline user inquiries with validation and submission handling. - Added ContactFormModal to encapsulate the contact form in a modal dialog, enhancing user experience. - Updated the Pricing component to include a button that opens the contact modal for enterprise inquiries. - Refactored the ContactPage to utilize the new ContactForm component, simplifying the code and improving maintainability. - Updated TypeScript configuration to include additional type definitions for better type safety.
1 parent 0a5e3a1 commit 66bcafa

File tree

7 files changed

+775
-397
lines changed

7 files changed

+775
-397
lines changed

apps/website/app/contact/page.tsx

Lines changed: 10 additions & 362 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,13 @@
11
"use client";
22

33
import { Container } from "@/components/Container";
4-
import { trackGAEvent } from "@/components/analitycs";
4+
import { ContactForm } from "@/components/ContactForm";
55
import AnimatedGridPattern from "@/components/ui/animated-grid-pattern";
6-
import { Button } from "@/components/ui/button";
7-
import { Input } from "@/components/ui/input";
8-
import {
9-
Select,
10-
SelectContent,
11-
SelectItem,
12-
SelectTrigger,
13-
SelectValue,
14-
} from "@/components/ui/select";
156
import { cn } from "@/lib/utils";
167
import { useState } from "react";
178

18-
interface ContactFormData {
19-
inquiryType: "" | "support" | "sales" | "other";
20-
deploymentType: "" | "cloud" | "self-hosted";
21-
firstName: string;
22-
lastName: string;
23-
email: string;
24-
company: string;
25-
message: string;
26-
}
27-
289
export default function ContactPage() {
29-
const [isSubmitting, setIsSubmitting] = useState(false);
3010
const [isSubmitted, setIsSubmitted] = useState(false);
31-
const [formData, setFormData] = useState<ContactFormData>({
32-
inquiryType: "",
33-
deploymentType: "",
34-
firstName: "",
35-
lastName: "",
36-
email: "",
37-
company: "",
38-
message: "",
39-
});
40-
const [errors, setErrors] = useState<Record<string, string>>({});
41-
42-
const validateForm = (): boolean => {
43-
const newErrors: Record<string, string> = {};
44-
45-
if (!formData.inquiryType) {
46-
newErrors.inquiryType = "Please select what we can help you with";
47-
}
48-
if (formData.inquiryType === "support" && !formData.deploymentType) {
49-
newErrors.deploymentType = "Please select your deployment type";
50-
}
51-
if (!formData.firstName.trim()) {
52-
newErrors.firstName = "First name is required";
53-
}
54-
if (!formData.lastName.trim()) {
55-
newErrors.lastName = "Last name is required";
56-
}
57-
if (!formData.email.trim()) {
58-
newErrors.email = "Email is required";
59-
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
60-
newErrors.email = "Please enter a valid email address";
61-
}
62-
if (!formData.company.trim()) {
63-
newErrors.company = "Company name is required";
64-
}
65-
if (!formData.message.trim()) {
66-
newErrors.message = "Message is required";
67-
}
68-
69-
setErrors(newErrors);
70-
return Object.keys(newErrors).length === 0;
71-
};
72-
73-
const handleSubmit = async (e: React.FormEvent) => {
74-
e.preventDefault();
75-
76-
if (!validateForm()) {
77-
return;
78-
}
79-
80-
// Prevent submission for self-hosted support requests
81-
if (
82-
formData.inquiryType === "support" &&
83-
formData.deploymentType === "self-hosted"
84-
) {
85-
return;
86-
}
87-
88-
setIsSubmitting(true);
89-
90-
try {
91-
const response = await fetch("/api/contact", {
92-
method: "POST",
93-
headers: {
94-
"Content-Type": "application/json",
95-
},
96-
body: JSON.stringify(formData),
97-
});
98-
99-
if (response.ok) {
100-
trackGAEvent({
101-
action: "Contact Form Submitted",
102-
category: "Contact",
103-
label: formData.inquiryType,
104-
});
105-
106-
setFormData({
107-
inquiryType: "",
108-
deploymentType: "",
109-
firstName: "",
110-
lastName: "",
111-
email: "",
112-
company: "",
113-
message: "",
114-
});
115-
setErrors({});
116-
setIsSubmitted(true);
117-
} else {
118-
throw new Error("Failed to submit form");
119-
}
120-
} catch (error) {
121-
console.error("Error submitting form:", error);
122-
alert("There was an error sending your message. Please try again.");
123-
} finally {
124-
setIsSubmitting(false);
125-
}
126-
};
127-
128-
const handleInputChange = (field: keyof ContactFormData, value: any) => {
129-
setFormData((prev) => {
130-
const updated = { ...prev, [field]: value };
131-
// Reset deploymentType when inquiryType changes and is not support
132-
if (field === "inquiryType" && value !== "support") {
133-
updated.deploymentType = "";
134-
}
135-
return updated;
136-
});
137-
if (errors[field]) {
138-
setErrors((prev) => {
139-
const newErrors = { ...prev };
140-
delete newErrors[field];
141-
return newErrors;
142-
});
143-
}
144-
};
14511

14612
if (isSubmitted) {
14713
return (
@@ -156,9 +22,13 @@ export default function ContactPage() {
15622
possible.
15723
</p>
15824
<div className="mt-10">
159-
<Button onClick={() => setIsSubmitted(false)} variant="outline">
25+
<button
26+
type="button"
27+
onClick={() => setIsSubmitted(false)}
28+
className="rounded-md border border-input bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-accent hover:text-accent-foreground"
29+
>
16030
Send Another Message
161-
</Button>
31+
</button>
16232
</div>
16333
</div>
16434
</Container>
@@ -192,231 +62,9 @@ export default function ContactPage() {
19262
</p>
19363
</div>
19464

195-
<form onSubmit={handleSubmit} className="mt-16 space-y-6">
196-
<div className="space-y-2">
197-
<label
198-
htmlFor="inquiryType"
199-
className="block text-sm font-medium text-foreground"
200-
>
201-
What can we help you with today?{" "}
202-
<span className="text-red-500">*</span>
203-
</label>
204-
<Select
205-
value={formData.inquiryType}
206-
onValueChange={(value) =>
207-
handleInputChange(
208-
"inquiryType",
209-
value as "support" | "sales" | "other",
210-
)
211-
}
212-
>
213-
<SelectTrigger className="bg-input">
214-
<SelectValue placeholder="Select an option" />
215-
</SelectTrigger>
216-
<SelectContent>
217-
<SelectItem value="support">Support</SelectItem>
218-
<SelectItem value="sales">Sales</SelectItem>
219-
<SelectItem value="other">Other</SelectItem>
220-
</SelectContent>
221-
</Select>
222-
{errors.inquiryType && (
223-
<p className="text-sm text-red-600">{errors.inquiryType}</p>
224-
)}
225-
</div>
226-
227-
{formData.inquiryType === "support" && (
228-
<div className="space-y-2">
229-
<label
230-
htmlFor="deploymentType"
231-
className="block text-sm font-medium text-foreground"
232-
>
233-
What version of Dokploy are you using?{" "}
234-
<span className="text-red-500">*</span>
235-
</label>
236-
<Select
237-
value={formData.deploymentType}
238-
onValueChange={(value) =>
239-
handleInputChange(
240-
"deploymentType",
241-
value as "cloud" | "self-hosted",
242-
)
243-
}
244-
>
245-
<SelectTrigger className="bg-input">
246-
<SelectValue placeholder="Select deployment type" />
247-
</SelectTrigger>
248-
<SelectContent>
249-
<SelectItem value="cloud">Cloud Version</SelectItem>
250-
<SelectItem value="self-hosted">Self Hosted</SelectItem>
251-
</SelectContent>
252-
</Select>
253-
{errors.deploymentType && (
254-
<p className="text-sm text-red-600">
255-
{errors.deploymentType}
256-
</p>
257-
)}
258-
259-
{formData.deploymentType === "self-hosted" && (
260-
<div className="mt-4 rounded-lg border border-amber-500/50 bg-amber-500/10 p-4">
261-
<h3 className="mb-2 text-sm font-semibold text-amber-500">
262-
Self-Hosted Support
263-
</h3>
264-
<p className="mb-3 text-sm text-muted-foreground">
265-
We currently don't provide direct support for self-hosted
266-
deployments through this form. However, our community is
267-
here to help!
268-
</p>
269-
<div className="space-y-2 text-sm">
270-
<p className="text-muted-foreground">
271-
Please use one of these channels to get assistance:
272-
</p>
273-
<ul className="ml-4 list-disc space-y-1 text-muted-foreground">
274-
<li>
275-
Join our{" "}
276-
<a
277-
href="https://discord.gg/2tBnJ3jDJc"
278-
target="_blank"
279-
rel="noopener noreferrer"
280-
className="text-primary underline hover:text-primary/80"
281-
>
282-
Discord community
283-
</a>{" "}
284-
for real-time help
285-
</li>
286-
<li>
287-
Open a discussion on{" "}
288-
<a
289-
href="https://github.com/Dokploy/dokploy/discussions"
290-
target="_blank"
291-
rel="noopener noreferrer"
292-
className="text-primary underline hover:text-primary/80"
293-
>
294-
GitHub Discussions
295-
</a>
296-
</li>
297-
</ul>
298-
</div>
299-
</div>
300-
)}
301-
</div>
302-
)}
303-
304-
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
305-
<div className="space-y-2">
306-
<label
307-
htmlFor="firstName"
308-
className="block text-sm font-medium text-foreground"
309-
>
310-
First Name <span className="text-red-500">*</span>
311-
</label>
312-
<Input
313-
id="firstName"
314-
type="text"
315-
value={formData.firstName}
316-
onChange={(e) =>
317-
handleInputChange("firstName", e.target.value)
318-
}
319-
placeholder="Your first name"
320-
/>
321-
{errors.firstName && (
322-
<p className="text-sm text-red-600">{errors.firstName}</p>
323-
)}
324-
</div>
325-
326-
<div className="space-y-2">
327-
<label
328-
htmlFor="lastName"
329-
className="block text-sm font-medium text-foreground"
330-
>
331-
Last Name <span className="text-red-500">*</span>
332-
</label>
333-
<Input
334-
id="lastName"
335-
type="text"
336-
value={formData.lastName}
337-
onChange={(e) =>
338-
handleInputChange("lastName", e.target.value)
339-
}
340-
placeholder="Your last name"
341-
/>
342-
{errors.lastName && (
343-
<p className="text-sm text-red-600">{errors.lastName}</p>
344-
)}
345-
</div>
346-
</div>
347-
348-
<div className="space-y-2">
349-
<label
350-
htmlFor="email"
351-
className="block text-sm font-medium text-foreground"
352-
>
353-
Email <span className="text-red-500">*</span>
354-
</label>
355-
<Input
356-
id="email"
357-
type="email"
358-
value={formData.email}
359-
onChange={(e) => handleInputChange("email", e.target.value)}
360-
placeholder="[email protected]"
361-
/>
362-
{errors.email && (
363-
<p className="text-sm text-red-600">{errors.email}</p>
364-
)}
365-
</div>
366-
367-
<div className="space-y-2">
368-
<label
369-
htmlFor="company"
370-
className="block text-sm font-medium text-foreground"
371-
>
372-
Company Name <span className="text-red-500">*</span>
373-
</label>
374-
<Input
375-
id="company"
376-
type="text"
377-
value={formData.company}
378-
onChange={(e) => handleInputChange("company", e.target.value)}
379-
placeholder="Your company name"
380-
/>
381-
{errors.company && (
382-
<p className="text-sm text-red-600">{errors.company}</p>
383-
)}
384-
</div>
385-
386-
<div className="space-y-2">
387-
<label
388-
htmlFor="message"
389-
className="block text-sm font-medium text-foreground"
390-
>
391-
How can we help? <span className="text-red-500">*</span>
392-
</label>
393-
<textarea
394-
id="message"
395-
value={formData.message}
396-
onChange={(e) => handleInputChange("message", e.target.value)}
397-
placeholder="Tell us more about your inquiry..."
398-
rows={6}
399-
className="flex w-full resize-none rounded-md border border-input bg-background bg-input px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
400-
/>
401-
{errors.message && (
402-
<p className="text-sm text-red-600">{errors.message}</p>
403-
)}
404-
</div>
405-
406-
<div className="flex justify-end">
407-
<Button
408-
type="submit"
409-
disabled={
410-
isSubmitting ||
411-
(formData.inquiryType === "support" &&
412-
formData.deploymentType === "self-hosted")
413-
}
414-
className="min-w-[120px]"
415-
>
416-
{isSubmitting ? "Sending..." : "Send Message"}
417-
</Button>
418-
</div>
419-
</form>
65+
<div className="mt-16">
66+
<ContactForm onSuccess={() => setIsSubmitted(true)} />
67+
</div>
42068
</div>
42169
</Container>
42270
</div>

0 commit comments

Comments
 (0)