Openstatus
www.openstatus.dev
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}