Openstatus www.openstatus.dev
at main 120 lines 3.2 kB view raw
1"use client"; 2 3import { InputWithAddons } from "@/components/common/input-with-addons"; 4import { 5 Form, 6 FormControl, 7 FormDescription, 8 FormField, 9 FormItem, 10 FormLabel, 11 FormMessage, 12} from "@/components/ui/form"; 13import { useDebounce } from "@/hooks/use-debounce"; 14import { useTRPC } from "@/lib/trpc/client"; 15import { zodResolver } from "@hookform/resolvers/zod"; 16import { useQuery } from "@tanstack/react-query"; 17import { isTRPCClientError } from "@trpc/client"; 18import { useEffect, useTransition } from "react"; 19import { useForm } from "react-hook-form"; 20import { toast } from "sonner"; 21import { z } from "zod"; 22 23const SLUG_UNIQUE_ERROR_MESSAGE = 24 "This slug is already taken. Please choose another one."; 25 26const schema = z.object({ 27 slug: z.string().min(3), 28}); 29 30export type FormValues = z.infer<typeof schema>; 31 32export function CreatePageForm({ 33 defaultValues, 34 onSubmit, 35 ...props 36}: Omit<React.ComponentProps<"form">, "onSubmit"> & { 37 defaultValues?: FormValues; 38 onSubmit: (values: FormValues) => Promise<void>; 39}) { 40 const trpc = useTRPC(); 41 const form = useForm({ 42 resolver: zodResolver(schema), 43 defaultValues: defaultValues ?? { slug: "" }, 44 }); 45 const [isPending, startTransition] = useTransition(); 46 const watchSlug = form.watch("slug"); 47 const debouncedSlug = useDebounce(watchSlug, 500); 48 const { data: isUnique } = useQuery( 49 trpc.page.getSlugUniqueness.queryOptions( 50 { slug: debouncedSlug }, 51 { enabled: debouncedSlug.length > 0 }, 52 ), 53 ); 54 55 useEffect(() => { 56 if (isUnique === false) { 57 form.setError("slug", { message: SLUG_UNIQUE_ERROR_MESSAGE }); 58 } else { 59 form.clearErrors("slug"); 60 } 61 }, [isUnique, form]); 62 63 function submitAction(values: FormValues) { 64 if (isPending) return; 65 66 startTransition(async () => { 67 try { 68 if (isUnique === false) { 69 toast.error(SLUG_UNIQUE_ERROR_MESSAGE); 70 form.setError("slug", { message: SLUG_UNIQUE_ERROR_MESSAGE }); 71 return; 72 } 73 74 const promise = onSubmit(values); 75 toast.promise(promise, { 76 loading: "Saving...", 77 success: () => "Saved", 78 error: (error) => { 79 if (isTRPCClientError(error)) { 80 return error.message; 81 } 82 console.error(error); 83 return "Failed to save"; 84 }, 85 }); 86 await promise; 87 } catch (error) { 88 console.error(error); 89 } 90 }); 91 } 92 93 return ( 94 <Form {...form}> 95 <form onSubmit={form.handleSubmit(submitAction)} {...props}> 96 <FormField 97 control={form.control} 98 name="slug" 99 render={({ field }) => ( 100 <FormItem> 101 <FormLabel>Slug</FormLabel> 102 <FormControl> 103 <InputWithAddons 104 placeholder="status" 105 trailing=".openstatus.dev" 106 {...field} 107 /> 108 </FormControl> 109 <FormMessage /> 110 <FormDescription> 111 Choose a unique subdomain for your status page (minimum 3 112 characters). 113 </FormDescription> 114 </FormItem> 115 )} 116 /> 117 </form> 118 </Form> 119 ); 120}