Skip to content

Commit 075ab41

Browse files
authored
feat(website): add contact form (#8733)
1 parent ff6d868 commit 075ab41

File tree

10 files changed

+721
-9
lines changed

10 files changed

+721
-9
lines changed

website/src/components/misc/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ export * from "./seo";
1717
export * from "./spinner";
1818
export * from "./sr-only";
1919
export * from "./support-card";
20+
export * from "./support-form";
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import { useFormSubmission, useSupportForm } from "@/hooks";
2+
import { FONT_FAMILY_HEADING, THEME_COLORS } from "@/style";
3+
import { SUPPORT_PLANS, SupportPlan } from "@/types/support";
4+
import React, { FC, FormEvent } from "react";
5+
import styled from "styled-components";
6+
import { Button } from "./button";
7+
8+
interface SupportFormProps {
9+
readonly className?: string;
10+
readonly initialPlan?: SupportPlan;
11+
}
12+
13+
export const SupportForm: FC<SupportFormProps> = ({
14+
className,
15+
initialPlan = "Startup",
16+
}) => {
17+
const { formData, errors, updateField, validateForm } = useSupportForm({
18+
initialPlan,
19+
});
20+
21+
const { isSubmitting, submitForm } = useFormSubmission();
22+
23+
const handleSubmit = async (e: FormEvent) => {
24+
e.preventDefault();
25+
26+
if (!validateForm()) {
27+
return;
28+
}
29+
30+
await submitForm(formData);
31+
};
32+
33+
return (
34+
<FormContainer className={className}>
35+
<FormTitle>Contact Support</FormTitle>
36+
<FormDescription>
37+
Fill out the form below and we'll get back to you as soon as possible.
38+
</FormDescription>
39+
40+
<Form onSubmit={handleSubmit}>
41+
<FormGroup>
42+
<Label htmlFor="name">Name *</Label>
43+
<Input
44+
id="name"
45+
type="text"
46+
value={formData.name}
47+
onChange={(e) => updateField("name", e.target.value)}
48+
$hasError={!!errors.name}
49+
disabled={isSubmitting}
50+
/>
51+
{errors.name && <ErrorText>{errors.name}</ErrorText>}
52+
</FormGroup>
53+
54+
<FormGroup>
55+
<Label htmlFor="email">Email *</Label>
56+
<Input
57+
id="email"
58+
type="email"
59+
value={formData.email}
60+
onChange={(e) => updateField("email", e.target.value)}
61+
$hasError={!!errors.email}
62+
disabled={isSubmitting}
63+
/>
64+
{errors.email && <ErrorText>{errors.email}</ErrorText>}
65+
</FormGroup>
66+
67+
<FormGroup>
68+
<Label htmlFor="company">Company *</Label>
69+
<Input
70+
id="company"
71+
type="text"
72+
value={formData.company}
73+
onChange={(e) => updateField("company", e.target.value)}
74+
$hasError={!!errors.company}
75+
disabled={isSubmitting}
76+
/>
77+
{errors.company && <ErrorText>{errors.company}</ErrorText>}
78+
</FormGroup>
79+
80+
<FormGroup>
81+
<Label htmlFor="supportPlan">Support Plan</Label>
82+
<Select
83+
id="supportPlan"
84+
value={formData.supportPlan}
85+
onChange={(e) => updateField("supportPlan", e.target.value)}
86+
disabled={isSubmitting}
87+
>
88+
{SUPPORT_PLANS.map((plan) => (
89+
<option key={plan} value={plan}>
90+
{plan}
91+
</option>
92+
))}
93+
</Select>
94+
</FormGroup>
95+
96+
<FormGroup>
97+
<Label htmlFor="message">Message *</Label>
98+
<TextArea
99+
id="message"
100+
rows={5}
101+
value={formData.message}
102+
onChange={(e) => updateField("message", e.target.value)}
103+
$hasError={!!errors.message}
104+
disabled={isSubmitting}
105+
placeholder="Please describe how we can help you..."
106+
/>
107+
{errors.message && <ErrorText>{errors.message}</ErrorText>}
108+
</FormGroup>
109+
110+
<SubmitButton type="submit" disabled={isSubmitting}>
111+
{isSubmitting ? "Sending..." : "Send Message"}
112+
</SubmitButton>
113+
</Form>
114+
</FormContainer>
115+
);
116+
};
117+
118+
const FormContainer = styled.div`
119+
display: flex;
120+
flex-direction: column;
121+
border: 1px solid ${THEME_COLORS.boxBorder};
122+
border-radius: var(--box-border-radius);
123+
padding: 40px;
124+
backdrop-filter: blur(2px);
125+
background-image: linear-gradient(
126+
to right bottom,
127+
#379dc83d,
128+
#2b80ad3d,
129+
#2263903d,
130+
#1a48743d,
131+
#112f573d
132+
);
133+
max-width: 600px;
134+
margin: 0 auto;
135+
`;
136+
137+
const FormTitle = styled.h3`
138+
margin: 0 0 16px 0;
139+
color: ${THEME_COLORS.heading};
140+
font-family: ${FONT_FAMILY_HEADING};
141+
`;
142+
143+
const FormDescription = styled.p.attrs({
144+
className: "text-2",
145+
})`
146+
margin: 0 0 32px 0;
147+
color: ${THEME_COLORS.text};
148+
`;
149+
150+
const Form = styled.form`
151+
display: flex;
152+
flex-direction: column;
153+
gap: 24px;
154+
`;
155+
156+
const FormGroup = styled.div`
157+
display: flex;
158+
flex-direction: column;
159+
`;
160+
161+
const Label = styled.label`
162+
margin-bottom: 8px;
163+
color: ${THEME_COLORS.text};
164+
font-weight: 500;
165+
font-size: 0.875rem;
166+
`;
167+
168+
const baseInputStyles = `
169+
padding: 12px 16px;
170+
border: 1px solid ${THEME_COLORS.boxBorder};
171+
border-radius: var(--border-radius);
172+
background-color: rgba(255, 255, 255, 0.05);
173+
color: ${THEME_COLORS.text};
174+
font-size: 1rem;
175+
transition: border-color 0.2s ease-in-out, background-color 0.2s ease-in-out;
176+
177+
&:focus {
178+
outline: none;
179+
border-color: ${THEME_COLORS.primary};
180+
background-color: rgba(255, 255, 255, 0.08);
181+
}
182+
183+
&:disabled {
184+
opacity: 0.6;
185+
cursor: not-allowed;
186+
}
187+
188+
&::placeholder {
189+
color: ${THEME_COLORS.text}80;
190+
}
191+
`;
192+
193+
const Input = styled.input<{ $hasError?: boolean }>`
194+
${baseInputStyles}
195+
196+
${({ $hasError }) =>
197+
$hasError &&
198+
`
199+
border-color: #ef4444;
200+
201+
&:focus {
202+
border-color: #ef4444;
203+
}
204+
`}
205+
`;
206+
207+
const TextArea = styled.textarea<{ $hasError?: boolean }>`
208+
${baseInputStyles}
209+
resize: vertical;
210+
min-height: 120px;
211+
212+
${({ $hasError }) =>
213+
$hasError &&
214+
`
215+
border-color: #ef4444;
216+
217+
&:focus {
218+
border-color: #ef4444;
219+
}
220+
`}
221+
`;
222+
223+
const Select = styled.select`
224+
${baseInputStyles}
225+
cursor: pointer;
226+
`;
227+
228+
const ErrorText = styled.span`
229+
margin-top: 4px;
230+
color: #ef4444;
231+
font-size: 0.875rem;
232+
`;
233+
234+
const SubmitButton = styled(Button)`
235+
align-self: flex-start;
236+
padding: 12px 32px;
237+
font-size: 1rem;
238+
font-weight: 600;
239+
margin-top: 16px;
240+
241+
&:disabled {
242+
opacity: 0.6;
243+
cursor: not-allowed;
244+
}
245+
`;

website/src/hooks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export * from "./use-animation-intersection-observer";
2+
export * from "./use-form-submission";
3+
export * from "./use-support-form";
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { SupportPlan } from "@/types/support";
2+
import { navigate } from "gatsby";
3+
import { useCallback, useState } from "react";
4+
5+
export interface SupportFormData {
6+
name: string;
7+
email: string;
8+
company: string;
9+
message: string;
10+
supportPlan: SupportPlan;
11+
}
12+
13+
export interface SupportFormErrors {
14+
name?: string;
15+
email?: string;
16+
company?: string;
17+
message?: string;
18+
supportPlan?: string;
19+
}
20+
21+
interface UseFormSubmissionProps {
22+
onSuccess?: () => void;
23+
onError?: (error: Error) => void;
24+
}
25+
26+
interface UseFormSubmissionReturn {
27+
isSubmitting: boolean;
28+
submitForm: (data: SupportFormData) => Promise<void>;
29+
}
30+
31+
/**
32+
* Custom hook for handling form submission
33+
*/
34+
export function useFormSubmission({
35+
onSuccess,
36+
onError,
37+
}: UseFormSubmissionProps = {}): UseFormSubmissionReturn {
38+
const [isSubmitting, setIsSubmitting] = useState(false);
39+
40+
const submitForm = useCallback(
41+
async (data: SupportFormData): Promise<void> => {
42+
setIsSubmitting(true);
43+
44+
try {
45+
const submissionData = {
46+
Name: data.name,
47+
Email: data.email,
48+
Company: data.company,
49+
SupportPlan: data.supportPlan,
50+
Message: data.message,
51+
};
52+
53+
const response = await fetch(
54+
"https://forms.chillicream.com/api/SupportForm",
55+
{
56+
method: "POST",
57+
headers: {
58+
"Content-Type": "application/json",
59+
},
60+
body: JSON.stringify(submissionData),
61+
}
62+
);
63+
64+
if (!response.ok) {
65+
throw new Error(`HTTP error! status: ${response.status}`);
66+
}
67+
68+
if (onSuccess) {
69+
onSuccess();
70+
} else {
71+
navigate("/services/support/thank-you");
72+
}
73+
} catch (error) {
74+
const errorObj =
75+
error instanceof Error ? error : new Error("Unknown error occurred");
76+
77+
if (onError) {
78+
onError(errorObj);
79+
} else {
80+
console.error("Form submission error:", errorObj);
81+
alert(
82+
"There was an error submitting your request. Please try again."
83+
);
84+
}
85+
} finally {
86+
setIsSubmitting(false);
87+
}
88+
},
89+
[onSuccess, onError]
90+
);
91+
92+
return {
93+
isSubmitting,
94+
submitForm,
95+
};
96+
}

0 commit comments

Comments
 (0)