Openstatus www.openstatus.dev

feat: add form error if slug not unique (#77)

* feat: add form error if slug not unique

* chore: add use-debounce

* chore: update delay

authored by

Maximilian Kaske and committed by
GitHub
ff48cd1f c18c4733

+71 -7
+45 -7
apps/web/src/components/forms/status-page-form.tsx
··· 18 18 FormMessage, 19 19 } from "@/components/ui/form"; 20 20 import { Input } from "@/components/ui/input"; 21 + import { useDebounce } from "@/hooks/use-debounce"; 22 + import { api } from "@/trpc/client"; 21 23 import { Checkbox } from "../ui/checkbox"; 24 + import { useToast } from "../ui/use-toast"; 22 25 23 26 // REMINDER: only use the props you need! 24 27 ··· 41 44 resolver: zodResolver(insertPageSchemaWithMonitors), 42 45 defaultValues: { 43 46 title: defaultValues?.title || "", 44 - slug: defaultValues?.slug || "", // TODO: verify if is unique 47 + slug: defaultValues?.slug || "", 45 48 description: defaultValues?.description || "", 46 49 monitors: [], 47 50 workspaceId: 0, 48 51 }, 49 52 }); 53 + const watchSlug = form.watch("slug"); 54 + const debouncedSlug = useDebounce(watchSlug, 1000); // using debounce to not exhaust the server 55 + const { toast } = useToast(); 56 + 57 + const checkUniqueSlug = React.useCallback(async () => { 58 + const isUnique = await api.page.getSlugUniqueness.query({ 59 + slug: debouncedSlug, 60 + }); 61 + return isUnique || debouncedSlug === defaultValues?.slug; 62 + }, [debouncedSlug, defaultValues?.slug]); 63 + 64 + React.useEffect(() => { 65 + async function watchSlugChanges() { 66 + const isUnique = await checkUniqueSlug(); 67 + if (!isUnique) { 68 + form.setError("slug", { 69 + message: "Already taken. Please select another slug.", 70 + }); 71 + } else { 72 + form.clearErrors("slug"); 73 + } 74 + } 75 + watchSlugChanges(); 76 + // eslint-disable-next-line react-hooks/exhaustive-deps 77 + }, [checkUniqueSlug]); 78 + 50 79 return ( 51 80 <Form {...form}> 52 81 <form 53 - onSubmit={form.handleSubmit(onSubmit, (e) => { 54 - console.log(e); 55 - console.log(form.getValues()); 56 - })} 82 + onSubmit={async (e) => { 83 + e.preventDefault(); 84 + const isUnique = await checkUniqueSlug(); 85 + if (!isUnique) { 86 + // the user will already have the "error" message - we include a toast as well 87 + toast({ 88 + title: "Slug is already taken.", 89 + description: "Please select another slug. Every slug is unique.", 90 + }); 91 + } else { 92 + form.handleSubmit(onSubmit)(e); 93 + } 94 + }} 57 95 id={id} 58 96 > 59 97 <div className="grid w-full items-center space-y-6"> ··· 66 104 <FormControl> 67 105 <Input placeholder="" {...field} /> 68 106 </FormControl> 69 - <FormDescription>This is title of your page.</FormDescription> 107 + <FormDescription>The title of your page.</FormDescription> 70 108 <FormMessage /> 71 109 </FormItem> 72 110 )} ··· 81 119 <Input placeholder="" {...field} /> 82 120 </FormControl> 83 121 <FormDescription> 84 - This is your url of your page. 122 + The subdomain slug for your status page. At least 3 chars. 85 123 </FormDescription> 86 124 <FormMessage /> 87 125 </FormItem>
+16
apps/web/src/hooks/use-debounce.ts
··· 1 + import * as React from "react"; 2 + 3 + // consider using https://github.com/xnimorz/use-debounce 4 + export function useDebounce<T>(value: T, delay?: number): T { 5 + const [debouncedValue, setDebouncedValue] = React.useState<T>(value); 6 + 7 + React.useEffect(() => { 8 + const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500); 9 + 10 + return () => { 11 + clearTimeout(timer); 12 + }; 13 + }, [value, delay]); 14 + 15 + return debouncedValue; 16 + }
+9
packages/api/src/router/page.ts
··· 96 96 97 97 return selectPageSchemaWithRelation.parse(result); 98 98 }), 99 + 100 + getSlugUniqueness: protectedProcedure 101 + .input(z.object({ slug: z.string() })) 102 + .query(async (opts) => { 103 + const result = await opts.ctx.db.query.page.findMany({ 104 + where: eq(page.slug, opts.input.slug), 105 + }); 106 + return result?.length > 0 ? false : true; 107 + }), 99 108 });
+1
packages/db/src/schema/page.ts
··· 40 40 // Schema for inserting a Page - can be used to validate API requests 41 41 export const insertPageSchema = createInsertSchema(page, { 42 42 customDomain: z.string().optional(), 43 + slug: z.string().min(3), // minimum subdomain length 43 44 }); 44 45 45 46 export const insertPageSchemaWithMonitors = insertPageSchema.extend({