Openstatus www.openstatus.dev

chore: onboarding improvements (#1332)

* chore: onboarding improvements

* fix: lint

authored by

Maximilian Kaske and committed by
GitHub
a95d5e7a 626fbabd

+78 -4
+23 -3
apps/dashboard/src/app/(dashboard)/onboarding/client.tsx
··· 26 26 import { CreatePageForm } from "@/components/forms/onboarding/create-page"; 27 27 import { LearnFromForm } from "@/components/forms/onboarding/learn-from"; 28 28 import { Button } from "@/components/ui/button"; 29 + import { extractDomain } from "@/lib/domains"; 29 30 import { useTRPC } from "@/lib/trpc/client"; 30 31 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 31 32 import { ArrowUpRight } from "lucide-react"; ··· 133 134 </Section> 134 135 {step === "1" && ( 135 136 <Section> 136 - <SectionHeader> 137 + <SectionHeader className="h-8 flex-row items-center justify-between"> 137 138 <SectionDescription className="tabular-nums"> 138 139 Step <span className="font-medium text-foreground">1</span> of{" "} 139 140 <span className="font-medium text-foreground">2</span> 140 141 </SectionDescription> 142 + <Button 143 + variant="ghost" 144 + size="sm" 145 + className="text-muted-foreground" 146 + onClick={() => setSearchParams({ step: "2" })} 147 + > 148 + Skip 149 + </Button> 141 150 </SectionHeader> 142 151 <FormCard> 143 152 <FormCardHeader> ··· 170 179 )} 171 180 {step === "2" && ( 172 181 <Section> 173 - <SectionHeader> 182 + <SectionHeader className="h-8 flex-row items-center justify-between"> 174 183 <SectionDescription className="tabular-nums"> 175 184 Step <span className="font-medium text-foreground">2</span> of{" "} 176 185 <span className="font-medium text-foreground">2</span> 177 186 </SectionDescription> 187 + <Button 188 + variant="ghost" 189 + size="sm" 190 + className="text-muted-foreground" 191 + onClick={() => setSearchParams({ step: "next" })} 192 + > 193 + Skip 194 + </Button> 178 195 </SectionHeader> 179 196 <FormCard> 180 197 <FormCardHeader> ··· 186 203 <FormCardContent> 187 204 <CreatePageForm 188 205 id="create-page-form" 206 + defaultValues={{ 207 + slug: extractDomain(createMonitorMutation.data?.url ?? ""), 208 + }} 189 209 onSubmit={async (values) => { 190 210 if (!workspace?.id) return; 191 211 ··· 210 230 {step === "next" && ( 211 231 <> 212 232 <Section> 213 - <SectionHeader> 233 + <SectionHeader className="h-8 flex-row items-center justify-between"> 214 234 <SectionDescription> 215 235 We&apos;d love to know how you heard about us. This will help us 216 236 improve our product and services.
+30 -1
apps/dashboard/src/components/forms/onboarding/create-page.tsx
··· 10 10 FormLabel, 11 11 FormMessage, 12 12 } from "@/components/ui/form"; 13 + import { useDebounce } from "@/hooks/use-debounce"; 14 + import { useTRPC } from "@/lib/trpc/client"; 13 15 import { zodResolver } from "@hookform/resolvers/zod"; 16 + import { useQuery } from "@tanstack/react-query"; 14 17 import { isTRPCClientError } from "@trpc/client"; 15 - import { useTransition } from "react"; 18 + import { useEffect, useTransition } from "react"; 16 19 import { useForm } from "react-hook-form"; 17 20 import { toast } from "sonner"; 18 21 import { z } from "zod"; 22 + 23 + const SLUG_UNIQUE_ERROR_MESSAGE = 24 + "This slug is already taken. Please choose another one."; 19 25 20 26 const schema = z.object({ 21 27 slug: z.string().min(3), ··· 31 37 defaultValues?: FormValues; 32 38 onSubmit: (values: FormValues) => Promise<void>; 33 39 }) { 40 + const trpc = useTRPC(); 34 41 const form = useForm({ 35 42 resolver: zodResolver(schema), 36 43 defaultValues: defaultValues ?? { slug: "" }, 37 44 }); 38 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]); 39 62 40 63 function submitAction(values: FormValues) { 41 64 if (isPending) return; 42 65 43 66 startTransition(async () => { 44 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 + 45 74 const promise = onSubmit(values); 46 75 toast.promise(promise, { 47 76 loading: "Saving...",
+25
apps/dashboard/src/lib/domains.ts
··· 19 19 // if it's a normal domain (e.g. dub.sh), we return the domain 20 20 return domain; 21 21 }; 22 + 23 + export function extractDomain(url: string) { 24 + // Use URL constructor to parse 25 + try { 26 + if (url.trim() === "") return ""; 27 + 28 + const hostname = new URL(url).hostname; // e.g. "craft.mxkaske.dev" 29 + 30 + const parts = hostname.split("."); // ["craft", "mxkaske", "dev"] 31 + 32 + if (parts.length === 2) { 33 + // no subdomain 34 + return parts[0]; // "mxkaske" 35 + } 36 + if (parts.length > 2) { 37 + // has subdomain(s) 38 + return `${parts.slice(0, -2).join("-")}-${parts[parts.length - 2]}`; 39 + // "craft-mxkaske" 40 + } 41 + return ""; 42 + } catch (e) { 43 + console.error(e); 44 + return ""; 45 + } 46 + }