Openstatus www.openstatus.dev
at main 209 lines 5.8 kB view raw
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}