TypeScript 97.5%
SQL 1.4%
Python 0.8%
1"use client";2import * as React from "react";3import Link from "next/link";4import { useRouter } from "next/navigation";5import { ArrowLeft, ArrowRight, Check, Code2, FolderKanban, KeyRound, Play, Route, ShieldCheck, Zap } from "lucide-react";6import type { CreatedKey } from "@/actions/api-keys";7import { completeOnboarding } from "@/actions/account";8import { Button } from "@/components/ui/button";9import { Badge } from "@/components/ui/badge";10import { Alert } from "@/components/ui/alert";11import { CodeBlock } from "@/components/ui/code-block";12import { cn } from "@/lib/utils";13import { curlSnippet } from "@/lib/account-snippets";14import { CreateKeyForm, type ProjectOption } from "@/components/dashboard/api-keys/create-key-dialog";15import { KeyReveal } from "@/components/dashboard/api-keys/key-reveal";16import { CreateProjectDialog } from "@/components/dashboard/projects/create-project-dialog";1718const STEPS = [19 { id: "welcome", label: "Welcome", icon: Zap },20 { id: "project", label: "Project", icon: FolderKanban },21 { id: "key", label: "API key", icon: KeyRound },22 { id: "request", label: "First request", icon: Play },23 { id: "code", label: "Generated code", icon: Code2 },24] as const;2526export function OnboardingFlow({27 projects,28 currentProject,29 hasKey,30 hasRequest,31 alreadyCompleted,32 userName,33}: {34 projects: ProjectOption[];35 currentProject: ProjectOption;36 hasKey: boolean;37 hasRequest: boolean;38 alreadyCompleted: boolean;39 userName: string;40}) {41 const router = useRouter();42 const [step, setStep] = React.useState(0);43 const [created, setCreated] = React.useState<CreatedKey | null>(null);44 const [finishing, startFinish] = React.useTransition();45 const [error, setError] = React.useState<string | null>(null);4647 const keyReady = hasKey || Boolean(created);48 const apiKeyForSnippet = created?.plaintext ?? "fch_live_YOUR_API_KEY";49 const last = step === STEPS.length - 1;5051 function finish() {52 setError(null);53 startFinish(async () => {54 const res = await completeOnboarding();55 if (!res.ok) {56 setError(res.error);57 return;58 }59 router.push("/dashboard");60 router.refresh();61 });62 }6364 return (65 <div className="grid gap-6">66 {/* Progress */}67 <ol className="grid grid-cols-5 gap-2" aria-label="Onboarding progress">68 {STEPS.map((s, i) => {69 const done = i < step || (i === 1) || (i === 2 && keyReady) || (i === 3 && hasRequest);70 const active = i === step;71 const Icon = s.icon;72 return (73 <li key={s.id} className="grid gap-2">74 <button75 type="button"76 onClick={() => setStep(i)}77 className={cn("flex items-center gap-2 rounded-md px-2 py-1.5 text-left text-[12.5px] font-medium transition-colors hover:bg-bg-muted", active ? "text-fg" : "text-fg-muted")}78 aria-current={active ? "step" : undefined}79 >80 <span className={cn("flex size-6 shrink-0 items-center justify-center rounded-full border text-[11px]", active ? "border-accent bg-accent text-accent-fg" : done && !active ? "border-success/40 bg-success-soft text-success" : "border-border bg-bg")}>81 {done && !active ? <Check className="size-3.5" /> : <Icon className="size-3.5" />}82 </span>83 <span className="hidden truncate sm:inline">{s.label}</span>84 </button>85 <span className={cn("h-1 rounded-full", i <= step ? "bg-accent" : "bg-bg-muted")} aria-hidden />86 </li>87 );88 })}89 </ol>9091 <div className="rounded-lg border border-border bg-bg-elevated p-6 shadow-xs sm:p-8">92 {alreadyCompleted && step === 0 ? <Alert variant="info" className="mb-6">You already finished onboarding — feel free to walk through it again.</Alert> : null}9394 {step === 0 ? (95 <div className="grid gap-5">96 <div>97 <div className="text-[11.5px] font-medium uppercase tracking-wide text-fg-subtle">Step 1 of 5</div>98 <h2 className="mt-1 text-xl font-semibold tracking-tight">Welcome to Fetcha{userName ? `, ${userName.split(" ")[0]}` : ""}</h2>99 <p className="mt-2 max-w-xl text-[13.5px] leading-relaxed text-fg-muted">One API that fetches any public web page for you, choosing the right network and location automatically, retrying when a target blocks, and returning clean HTML, text or JSON.</p>100 </div>101 <ul className="grid gap-3 sm:grid-cols-3">102 {[103 { icon: Route, title: "Intelligent routing", desc: "Every request is scored on success history, cost, latency, network health and geography, then escalated only when needed." },104 { icon: ShieldCheck, title: "Built-in resilience", desc: "Retries with a fresh IP or alternate network, circuit breakers per domain and sticky sessions when you need the same identity." },105 { icon: Code2, title: "Developer first", desc: "A single POST /v1/fetch, request IDs on every call, a Playground that writes your code, and SDKs for JS and Python." },106 ].map((b) => (107 <li key={b.title} className="rounded-md border border-border bg-bg p-4">108 <b.icon className="size-4 text-accent" aria-hidden />109 <div className="mt-2 text-[13.5px] font-medium">{b.title}</div>110 <div className="mt-1 text-[12.5px] leading-relaxed text-fg-muted">{b.desc}</div>111 </li>112 ))}113 </ul>114 </div>115 ) : null}116117 {step === 1 ? (118 <div className="grid gap-5">119 <div>120 <div className="text-[11.5px] font-medium uppercase tracking-wide text-fg-subtle">Step 2 of 5</div>121 <h2 className="mt-1 text-xl font-semibold tracking-tight">Create your first project</h2>122 <p className="mt-2 max-w-xl text-[13.5px] leading-relaxed text-fg-muted">Projects separate keys, logs, defaults and spending limits. We created one for you — keep it or add another for a different environment.</p>123 </div>124 <div className="flex flex-col gap-3 rounded-md border border-border bg-bg p-4 sm:flex-row sm:items-center">125 <div className="min-w-0 flex-1">126 <div className="flex items-center gap-2">127 <span className="text-[14px] font-medium">{currentProject.name}</span>128 <Badge variant={currentProject.environment === "production" ? "success" : "warning"}>{currentProject.environment}</Badge>129 <Badge variant="accent" dot>130 Current131 </Badge>132 </div>133 <div className="mt-0.5 font-mono text-[11.5px] text-fg-subtle">{currentProject.id}</div>134 </div>135 <div className="flex items-center gap-2">136 <Button asChild variant="ghost" size="sm">137 <Link href={`/dashboard/projects/${currentProject.id}`}>Edit defaults</Link>138 </Button>139 <CreateProjectDialog140 redirectToDetail={false}141 trigger={142 <Button variant="outline" size="sm">143 New project144 </Button>145 }146 />147 </div>148 </div>149 {projects.length > 1 ? <p className="text-[12.5px] text-fg-subtle">You have {projects.length} projects. The current one is used for the key in the next step.</p> : null}150 </div>151 ) : null}152153 {step === 2 ? (154 <div className="grid gap-5">155 <div>156 <div className="text-[11.5px] font-medium uppercase tracking-wide text-fg-subtle">Step 3 of 5</div>157 <h2 className="mt-1 text-xl font-semibold tracking-tight">Generate an API key</h2>158 <p className="mt-2 max-w-xl text-[13.5px] leading-relaxed text-fg-muted">Keys are shown once and stored hashed. This one gets the default scopes for fetching, sessions and usage.</p>159 </div>160 {created ? (161 <KeyReveal plaintext={created.plaintext} name={created.name} compact />162 ) : hasKey ? (163 <Alert164 variant="success"165 title="You already have an active key"166 action={167 <Button asChild variant="outline" size="xs">168 <Link href="/dashboard/api-keys">Manage keys</Link>169 </Button>170 }171 >172 Continue with it, or create another one below for this walkthrough.173 </Alert>174 ) : null}175 {!created ? (176 <div className="rounded-md border border-border bg-bg p-4">177 <CreateKeyForm projects={projects} defaultProjectId={currentProject.id} onCreated={setCreated} compact defaultName="Getting started" submitLabel="Generate key" />178 </div>179 ) : null}180 </div>181 ) : null}182183 {step === 3 ? (184 <div className="grid gap-5">185 <div>186 <div className="text-[11.5px] font-medium uppercase tracking-wide text-fg-subtle">Step 4 of 5</div>187 <h2 className="mt-1 text-xl font-semibold tracking-tight">Run your first request</h2>188 <p className="mt-2 max-w-xl text-[13.5px] leading-relaxed text-fg-muted">189 Paste this in a terminal. {created ? "It already contains the key you just created." : "Replace the placeholder with your key."} The response includes <code className="font-mono text-[12.5px]">request_id</code>, the page content and routing metadata.190 </p>191 </div>192 {hasRequest ? <Alert variant="success">Nice — this organization has already made requests. The Requests page shows every one of them.</Alert> : null}193 <CodeBlock code={curlSnippet(apiKeyForSnippet)} lang="bash" title="terminal" />194 <div className="flex flex-wrap items-center gap-2">195 <Button asChild variant="outline" size="sm">196 <Link href="/dashboard/playground">197 <Play /> Try it in the Playground198 </Link>199 </Button>200 <Button asChild variant="ghost" size="sm">201 <Link href="/dashboard/requests">View requests</Link>202 </Button>203 </div>204 </div>205 ) : null}206207 {step === 4 ? (208 <div className="grid gap-5">209 <div>210 <div className="text-[11.5px] font-medium uppercase tracking-wide text-fg-subtle">Step 5 of 5</div>211 <h2 className="mt-1 text-xl font-semibold tracking-tight">Get generated code</h2>212 <p className="mt-2 max-w-xl text-[13.5px] leading-relaxed text-fg-muted">The Playground writes the request in cURL, JavaScript, Python, Go, PHP, Ruby, Java and C# as you configure it. Copy it straight into your project.</p>213 </div>214 <div className="grid gap-3 sm:grid-cols-2">215 <Link href="/dashboard/playground?tab=code" className="group rounded-md border border-border bg-bg p-4 transition-colors hover:border-border-strong">216 <Code2 className="size-4 text-accent" aria-hidden />217 <div className="mt-2 flex items-center gap-1 text-[13.5px] font-medium">218 Playground · Code tab <ArrowRight className="size-3.5 transition-transform group-hover:translate-x-0.5" />219 </div>220 <div className="mt-1 text-[12.5px] text-fg-muted">Snippets stay in sync with the request you build.</div>221 </Link>222 <Link href="/docs" className="group rounded-md border border-border bg-bg p-4 transition-colors hover:border-border-strong">223 <ShieldCheck className="size-4 text-accent" aria-hidden />224 <div className="mt-2 flex items-center gap-1 text-[13.5px] font-medium">225 Documentation <ArrowRight className="size-3.5 transition-transform group-hover:translate-x-0.5" />226 </div>227 <div className="mt-1 text-[12.5px] text-fg-muted">API reference, error codes, sessions, SDK install from source.</div>228 </Link>229 </div>230 {error ? <Alert variant="danger">{error}</Alert> : null}231 </div>232 ) : null}233234 <div className="mt-8 flex items-center justify-between border-t border-border pt-5">235 <Button variant="ghost" size="sm" onClick={() => setStep((s) => Math.max(0, s - 1))} disabled={step === 0 || finishing}>236 <ArrowLeft /> Back237 </Button>238 <div className="flex items-center gap-2">239 {!last ? (240 <Button variant="link" size="sm" onClick={finish} disabled={finishing} className="text-fg-muted">241 Skip for now242 </Button>243 ) : null}244 {last ? (245 <Button variant="primary" onClick={finish} loading={finishing}>246 Finish <Check />247 </Button>248 ) : (249 <Button variant="primary" onClick={() => setStep((s) => Math.min(STEPS.length - 1, s + 1))}>250 {step === 2 && !keyReady ? "Skip this step" : "Continue"} <ArrowRight />251 </Button>252 )}253 </div>254 </div>255 </div>256 </div>257 );258}259