Openstatus
www.openstatus.dev
1"use client";
2
3import { zodResolver } from "@hookform/resolvers/zod";
4import { useTransition } from "react";
5import { useForm } from "react-hook-form";
6import { z } from "zod";
7
8import {
9 Form,
10 FormControl,
11 FormField,
12 FormItem,
13 FormLabel,
14 FormMessage,
15} from "@/components/ui/form";
16
17import { Button } from "@/components/ui/button";
18import { Checkbox } from "@/components/ui/checkbox";
19import { Input } from "@/components/ui/input";
20import { SelectItem } from "@/components/ui/select";
21import { SelectContent, SelectValue } from "@/components/ui/select";
22import { SelectTrigger } from "@/components/ui/select";
23import { Select } from "@/components/ui/select";
24import { Textarea } from "@/components/ui/textarea";
25import { cn } from "@/lib/utils";
26import { toast } from "sonner";
27
28export const types = [
29 {
30 label: "Report a bug",
31 value: "bug" as const,
32 },
33 {
34 label: "Book a demo",
35 value: "demo" as const,
36 },
37 {
38 label: "Suggest a feature",
39 value: "feature" as const,
40 },
41 {
42 label: "Report a security issue",
43 value: "security" as const,
44 },
45 {
46 label: "Something else",
47 value: "question" as const,
48 },
49];
50
51export const schema = z.object({
52 name: z.string().min(1, {
53 error: "Name is required",
54 }),
55 type: z.enum(["bug", "demo", "feature", "security", "question"]),
56 email: z.email({
57 error: "Invalid email address",
58 }),
59 message: z.string().min(1, {
60 error: "Message is required",
61 }),
62 blocker: z.boolean(),
63});
64
65export type FormValues = z.infer<typeof schema>;
66
67interface ContactFormProps {
68 defaultValues?: Partial<FormValues>;
69 onSubmit: (data: FormValues) => Promise<void>;
70 className?: string;
71}
72
73export function ContactForm({
74 defaultValues,
75 onSubmit,
76 className,
77}: ContactFormProps) {
78 const form = useForm<FormValues>({
79 resolver: zodResolver(schema),
80 defaultValues: {
81 name: defaultValues?.name ?? "",
82 email: defaultValues?.email ?? "",
83 type: defaultValues?.type ?? undefined,
84 message: defaultValues?.message ?? "",
85 blocker: defaultValues?.blocker ?? false,
86 },
87 });
88 const [isPending, startTransition] = useTransition();
89 const watchType = form.watch("type");
90
91 async function submitAction(values: FormValues) {
92 if (isPending) return;
93
94 startTransition(async () => {
95 try {
96 const promise = onSubmit(values);
97 toast.promise(promise, {
98 loading: "Sending message...",
99 success: "Message sent. We'll get back to you soon.",
100 error: "Failed to send message. Please try again.",
101 });
102 await promise;
103 } catch (error) {
104 console.error(error);
105 }
106 });
107 }
108
109 return (
110 <Form {...form}>
111 <form
112 onSubmit={form.handleSubmit(submitAction)}
113 className={cn("grid gap-4 sm:grid-cols-2", className)}
114 >
115 <FormField
116 control={form.control}
117 name="name"
118 render={({ field }) => (
119 <FormItem>
120 <FormLabel>Name</FormLabel>
121 <FormControl>
122 <Input placeholder="Max" {...field} />
123 </FormControl>
124 <FormMessage />
125 </FormItem>
126 )}
127 />
128 <FormField
129 control={form.control}
130 name="email"
131 render={({ field }) => (
132 <FormItem>
133 <FormLabel>Email</FormLabel>
134 <FormControl>
135 <Input placeholder="max@openstatus.dev" {...field} />
136 </FormControl>
137 <FormMessage />
138 </FormItem>
139 )}
140 />
141 <FormField
142 control={form.control}
143 name="type"
144 render={({ field }) => (
145 <FormItem className="sm:col-span-full">
146 <FormLabel>Type</FormLabel>
147 <Select onValueChange={field.onChange} defaultValue={field.value}>
148 <FormControl>
149 <SelectTrigger className="w-full">
150 <SelectValue placeholder="What you need help with" />
151 </SelectTrigger>
152 </FormControl>
153 <SelectContent>
154 {types.map((type) => (
155 <SelectItem key={type.value} value={type.value}>
156 {type.label}
157 </SelectItem>
158 ))}
159 </SelectContent>
160 </Select>
161 <FormMessage />
162 </FormItem>
163 )}
164 />
165 {watchType ? (
166 <FormField
167 control={form.control}
168 name="message"
169 render={({ field }) => (
170 <FormItem className="sm:col-span-full">
171 <FormLabel>Message</FormLabel>
172 <FormControl>
173 <Textarea placeholder="Tell us about it..." {...field} />
174 </FormControl>
175 <FormMessage />
176 </FormItem>
177 )}
178 />
179 ) : null}
180 {watchType === "bug" ? (
181 <FormField
182 control={form.control}
183 name="blocker"
184 render={({ field }) => (
185 <FormItem className="flex flex-row items-start sm:col-span-full">
186 <FormControl>
187 <Checkbox
188 checked={field.value}
189 onCheckedChange={field.onChange}
190 />
191 </FormControl>
192 <FormLabel className="font-normal leading-none">
193 This bug prevents me from using the product.
194 </FormLabel>
195 </FormItem>
196 )}
197 />
198 ) : null}
199 <Button
200 type="submit"
201 className="w-full sm:col-span-full"
202 disabled={isPending}
203 >
204 {isPending ? "Submitting..." : "Submit"}
205 </Button>
206 </form>
207 </Form>
208 );
209}