Onboarding
Setup checklists, steppers, timelines, and activity feeds.
Fromblocks.so
Onboarding
"use client"
import {
ArchiveIcon,
ChevronRightIcon,
CircleCheckIcon,
CircleDashedIcon,
EllipsisIcon,
MailIcon,
} from "lucide-react"
import { useState } from "react"
import { Button, buttonVariants } from "@/components/ui/button"
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils"
const steps = [
{
id: "profile",
title: "Complete your profile",
description:
"Add your name, photo, and role so your team knows who you are.",
completed: true,
actionLabel: "Edit profile",
actionHref: "#",
},
{
id: "workspace",
title: "Set up your workspace",
description:
"Customize your workspace with a name, icon, and default settings.",
completed: false,
actionLabel: "Configure workspace",
actionHref: "#",
},
{
id: "invite",
title: "Invite your team",
description:
"Bring your teammates on board so you can collaborate in real time.",
completed: false,
actionLabel: "Send invites",
actionHref: "#",
},
{
id: "integrations",
title: "Connect integrations",
description:
"Link your favorite tools like Slack, GitHub, and Linear to streamline workflows.",
completed: false,
actionLabel: "Browse integrations",
actionHref: "#",
},
{
id: "workflow",
title: "Create your first workflow",
description:
"Automate a repetitive task with a simple drag-and-drop workflow builder.",
completed: false,
actionLabel: "Build workflow",
actionHref: "#",
},
{
id: "notifications",
title: "Set up notifications",
description:
"Choose how and when you want to be notified about updates and mentions.",
completed: false,
actionLabel: "Manage notifications",
actionHref: "#",
},
]
interface OnboardingStep {
id: string
title: string
description: string
completed: boolean
actionLabel: string
actionHref: string
}
function CircularProgress({
completed,
total,
}: {
completed: number
total: number
}) {
const progress = total > 0 ? ((total - completed) / total) * 100 : 0
const strokeDashoffset = 100 - progress
return (
<svg className="-rotate-90" height="14" viewBox="0 0 14 14" width="14">
<circle
className="stroke-muted"
cx="7"
cy="7"
fill="none"
pathLength="100"
r="6"
strokeWidth="2"
/>
<circle
className="stroke-primary"
cx="7"
cy="7"
fill="none"
pathLength="100"
r="6"
strokeDasharray="100"
strokeLinecap="round"
strokeWidth="2"
style={{ strokeDashoffset }}
/>
</svg>
)
}
function StepIndicator({ completed }: { completed: boolean }) {
if (completed) {
return (
<CircleCheckIcon
fill="currentColor"
aria-hidden="true"
className="[&>path]:stroke-background mt-1 size-4.5 shrink-0 text-primary"
/>
)
}
return (
<CircleDashedIcon
aria-hidden="true"
className="mt-1 size-5 shrink-0 stroke-muted-foreground/40"
strokeWidth={2}
/>
)
}
export default function Onboarding01() {
const [currentSteps, setCurrentSteps] = useState<OnboardingStep[]>(steps)
const [openStepId, setOpenStepId] = useState<string | null>(() => {
const firstIncomplete = steps.find((s) => !s.completed)
return firstIncomplete?.id ?? steps[0]?.id ?? null
})
const [dismissed, setDismissed] = useState(false)
const completedCount = currentSteps.filter((s) => s.completed).length
const remainingCount = currentSteps.length - completedCount
const handleStepClick = (stepId: string) => {
setOpenStepId(openStepId === stepId ? null : stepId)
}
const handleStepAction = (step: OnboardingStep) => {
const updated = currentSteps.map((s) =>
s.id === step.id ? { ...s, completed: true } : s
)
setCurrentSteps(updated)
const nextIncomplete = updated.find((s) => !s.completed)
setOpenStepId(nextIncomplete?.id ?? null)
}
if (dismissed) {
return (
<div className="flex min-h-dvh items-center justify-center bg-background p-4">
<div className="text-center">
<p className="text-pretty text-muted-foreground">
Checklist dismissed
</p>
<button
className="mt-2 text-primary text-sm underline"
onClick={() => setDismissed(false)}
>
Show again
</button>
</div>
</div>
)
}
return (
<div className="flex min-h-dvh items-center justify-center bg-background p-4">
<div className="w-full max-w-lg">
<div className="w-md rounded-lg border bg-card p-4 text-card-foreground shadow-xs">
<div className="mr-2 mb-4 flex flex-col justify-between sm:flex-row sm:items-center">
<h3 className="ml-2 text-balance font-semibold text-foreground">
Get started with Acme
</h3>
<div className="mt-2 flex items-center justify-end sm:mt-0">
<CircularProgress
completed={remainingCount}
total={currentSteps.length}
/>
<div className="mr-3 ml-1.5 text-muted-foreground text-sm">
<span className="font-medium text-foreground">
{completedCount}
</span>
{" / "}
<span className="font-medium text-foreground">
{currentSteps.length}
</span>{" "}
completed
</div>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button className="h-6 w-6" size="icon" variant="ghost" />
}
>
<EllipsisIcon
aria-hidden="true"
className="h-4 w-4 shrink-0"
/>
<span className="sr-only">Options</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem onClick={() => setDismissed(true)}>
<ArchiveIcon
aria-hidden="true"
className="mr-2 h-4 w-4 shrink-0"
/>
Dismiss
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
window.open("mailto:support@acme.com?subject=Feedback")
}
>
<MailIcon
aria-hidden="true"
className="mr-2 h-4 w-4 shrink-0"
/>
Give feedback
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="space-y-0">
{currentSteps.map((step, index) => {
const isOpen = openStepId === step.id
const isFirst = index === 0
const prevStep = currentSteps[index - 1]
const isPrevOpen = prevStep && openStepId === prevStep.id
const showBorderTop = !(isFirst || isOpen || isPrevOpen)
return (
<div
className={cn(
"group",
isOpen && "rounded-lg",
showBorderTop && "border-border border-t"
)}
key={step.id}
>
<div
className={cn(
"block w-full cursor-pointer text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isOpen && "rounded-lg"
)}
onClick={() => handleStepClick(step.id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
handleStepClick(step.id)
}
}}
role="button"
tabIndex={0}
>
<div
className={cn(
"relative overflow-hidden rounded-lg transition-colors",
isOpen && "border border-border bg-muted"
)}
>
<div className="relative flex items-center justify-between gap-3 py-3 pr-2 pl-4">
<div className="flex w-full gap-3">
<div className="shrink-0">
<StepIndicator completed={step.completed} />
</div>
<div className="mt-0.5 grow">
<h4
className={cn(
"font-semibold",
step.completed
? "text-primary"
: "text-foreground"
)}
>
{step.title}
</h4>
<Collapsible open={isOpen}>
<CollapsibleContent>
<p className="mt-2 text-pretty text-muted-foreground text-sm sm:max-w-64 md:max-w-xs">
{step.description}
</p>
<a
className={cn(
buttonVariants({ size: "sm" }),
"mt-3"
)}
href={step.actionHref}
onClick={(e) => {
e.stopPropagation()
handleStepAction(step)
}}
>
{step.actionLabel}
</a>
</CollapsibleContent>
</Collapsible>
</div>
</div>
{!isOpen && (
<ChevronRightIcon
aria-hidden="true"
className="h-4 w-4 shrink-0 text-muted-foreground"
/>
)}
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/onboarding-01.jsonOnboarding Workspace Setup
"use client"
import {
CircleCheckIcon,
type LucideIcon,
FolderIcon,
PlugIcon,
RocketIcon,
UsersIcon,
} from "lucide-react"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
interface StepData {
title: string
description: string
icon: LucideIcon
actionLabel: string
}
const steps: StepData[] = [
{
title: "Create your workspace",
description:
"Name your workspace and pick a URL. This is where your team will collaborate on everything.",
icon: RocketIcon,
actionLabel: "Get started",
},
{
title: "Invite your team",
description:
"Add teammates by email so everyone can collaborate in real time across projects.",
icon: UsersIcon,
actionLabel: "Send invites",
},
{
title: "Set up your first project",
description:
"Create a project to organize tasks, docs, and milestones all in one place.",
icon: FolderIcon,
actionLabel: "Create project",
},
{
title: "Connect your tools",
description:
"Link Slack, GitHub, or Figma to keep your workflow in sync automatically.",
icon: PlugIcon,
actionLabel: "Browse integrations",
},
]
function getIconColor(isCompleted: boolean, isActive: boolean) {
if (isCompleted) {
return "text-muted-foreground/30"
}
if (isActive) {
return "text-primary"
}
return "text-muted-foreground/40"
}
function getTitleClass(isCompleted: boolean, isActive: boolean) {
if (isCompleted) {
return "text-muted-foreground/50 line-through"
}
if (isActive) {
return "text-foreground"
}
return "text-muted-foreground"
}
function StepIndicator({
index,
isCompleted,
isActive,
}: {
index: number
isCompleted: boolean
isActive: boolean
}) {
if (isCompleted) {
return (
<div className="flex size-7 items-center justify-center">
<CircleCheckIcon
fill="currentColor"
aria-hidden="true"
className="[&>path]:stroke-background size-7 text-emerald-500"
/>
</div>
)
}
return (
<div
className={cn(
"flex size-7 items-center justify-center rounded-full font-semibold text-xs",
isActive
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground"
)}
>
{index + 1}
</div>
)
}
function StepCard({
step,
index,
isCompleted,
isActive,
onComplete,
}: {
step: StepData
index: number
isCompleted: boolean
isActive: boolean
onComplete: () => void
}) {
const StepIcon = step.icon
return (
<div
className={cn(
"rounded-lg border p-4 transition-colors",
isActive && "border-primary/30 bg-muted/50",
!isActive && "border-border bg-background"
)}
>
<div className="flex gap-3">
<div className="mt-0.5 shrink-0">
<StepIndicator
index={index}
isActive={isActive}
isCompleted={isCompleted}
/>
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start gap-3">
<div className="min-w-0 flex-1">
<p
className={cn(
"font-medium leading-6",
getTitleClass(isCompleted, isActive)
)}
>
{step.title}
</p>
<p
className={cn(
"mt-0.5 text-sm leading-5",
isActive
? "text-muted-foreground"
: "text-muted-foreground/60"
)}
>
{step.description}
</p>
{isActive && (
<Button className="mt-3" onClick={onComplete} size="sm">
<StepIcon
aria-hidden="true"
className="-ml-0.5 size-4 shrink-0"
/>
{step.actionLabel}
</Button>
)}
</div>
<StepIcon
aria-hidden="true"
className={cn(
"mt-0.5 size-5 shrink-0",
getIconColor(isCompleted, isActive)
)}
/>
</div>
</div>
</div>
</div>
)
}
export default function Onboarding02() {
const [completedSteps, setCompletedSteps] = useState<Set<number>>(
() => new Set([0])
)
const currentStep = steps.findIndex((_, i) => !completedSteps.has(i))
const completedCount = completedSteps.size
const allDone = completedCount === steps.length
const handleComplete = (index: number) => {
setCompletedSteps((prev) => {
const next = new Set(prev)
next.add(index)
return next
})
}
return (
<div className="flex min-h-dvh items-center justify-center bg-background p-4">
<div className="w-full max-w-xl">
<div className="mb-6">
<h3 className="font-semibold text-foreground text-lg">
Set up your workspace
</h3>
<p className="mt-1 text-muted-foreground text-sm">
Complete these steps to get your team up and running
</p>
<div className="mt-4">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
{allDone ? (
<span className="font-medium text-emerald-600">
All done 🎉
</span>
) : (
<>
<span className="font-medium text-foreground">
{completedCount}
</span>{" "}
of {steps.length} completed
</>
)}
</span>
</div>
<div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full rounded-full bg-emerald-500 transition-all duration-500"
style={{
width: `${(completedCount / steps.length) * 100}%`,
}}
/>
</div>
</div>
</div>
<div className="space-y-3">
{steps.map((step, index) => (
<StepCard
index={index}
isActive={index === currentStep}
isCompleted={completedSteps.has(index)}
key={step.title}
onComplete={() => handleComplete(index)}
step={step}
/>
))}
</div>
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/onboarding-02.jsonOnboarding Steps with Progress
"use client"
import { CircleCheckIcon } from "lucide-react"
import { useState } from "react"
import { Progress } from "@/components/ui/progress"
import { cn } from "@/lib/utils"
const steps = [
{
id: "1.",
title: "Create your store",
description:
"Choose a name, pick a subdomain, and configure your store's basic settings.",
},
{
id: "2.",
title: "Add your products",
description:
"Upload your catalog with photos, pricing, and inventory details.",
},
{
id: "3.",
title: "Set up payments",
description:
"Connect Stripe or PayPal to start accepting orders from customers.",
},
{
id: "4.",
title: "Launch your store",
description: "Review everything and publish your storefront to the world.",
},
]
export default function Onboarding03() {
const [activeStep, setActiveStep] = useState(0)
return (
<div className="flex min-h-dvh items-center justify-center bg-background p-4">
<div className="sm:mx-auto sm:max-w-lg">
<h3 className="font-semibold text-foreground text-lg">Store setup</h3>
<p className="mt-1 text-muted-foreground text-sm leading-6">
Complete each step to launch your online store.
</p>
<div className="mt-4 flex items-center justify-end space-x-4">
<span className="text-muted-foreground text-sm">
Step {activeStep + 1}/{steps.length}
</span>
<Progress
className="w-32"
value={(activeStep / steps.length) * 100}
/>
</div>
<ul className="mt-4 space-y-4">
{steps.map((step, index) => (
<li key={step.id}>
<button
className={cn(
"relative w-full cursor-pointer rounded-lg border bg-card p-4 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
index === activeStep
? "border-foreground/20"
: "border-border"
)}
onClick={() => setActiveStep(index)}
type="button"
>
<div className="flex items-start space-x-3">
{index < activeStep ? (
<CircleCheckIcon
fill="currentColor"
aria-hidden={true}
className="[&>path]:stroke-background size-6 shrink-0 text-foreground"
/>
) : (
<span
aria-hidden={true}
className="flex size-6 items-center justify-center font-medium text-muted-foreground"
>
{step.id}
</span>
)}
<div>
<h3
className={cn(
"font-medium",
index < activeStep
? "text-muted-foreground line-through"
: "text-foreground"
)}
>
{step.title}
</h3>
<p className="mt-1 text-muted-foreground text-sm leading-6">
{step.description}
</p>
</div>
</div>
</button>
</li>
))}
</ul>
<div className="mt-6 rounded-lg bg-muted p-4">
<h4 className="font-medium text-foreground text-sm">Need help?</h4>
<p className="mt-1 text-muted-foreground text-sm">
Connect with a member of our team at{" "}
<a
className="font-medium text-primary"
href="mailto:help@example.com"
>
help@example.com
</a>
.
</p>
</div>
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/onboarding-03.jsonOnboarding Accordion
"use client"
import {
SlidersHorizontalIcon,
CalculatorIcon,
ChartColumnIcon,
CircleCheckIcon,
DatabaseIcon,
} from "lucide-react"
import { useState } from "react"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
const steps = [
{
title: "Configure your environment",
subtitle: "Environment ready",
icon: SlidersHorizontalIcon,
description:
"Set up your development environment with API keys and preferences.",
buttonText: "Edit settings",
},
{
title: "Connect your database",
subtitle: "Add connection",
icon: DatabaseIcon,
description:
"Link your PostgreSQL, MySQL, or MongoDB instance to get started.",
buttonText: "Add database",
},
{
title: "Define your schema",
subtitle: "Create schema",
icon: CalculatorIcon,
description: "Model your data with types, relations, and validation rules.",
buttonText: "Open editor",
},
{
title: "Build your first API",
subtitle: "Generate endpoints",
icon: ChartColumnIcon,
description:
"Auto-generate REST or GraphQL endpoints from your schema in seconds.",
buttonText: "Generate API",
},
]
export default function Onboarding04() {
const [activeStep, setActiveStep] = useState(1)
const [openItem, setOpenItem] = useState(
activeStep < steps.length ? steps[activeStep].title : ""
)
const handleComplete = () => {
const next = activeStep + 1
setActiveStep(next)
setOpenItem(next < steps.length ? steps[next].title : "")
}
return (
<div className="flex min-h-dvh items-center justify-center bg-background p-4">
<div className="mx-auto w-full min-w-0 max-w-sm sm:min-w-sm">
<h3 className="font-semibold text-foreground text-lg">Quick start</h3>
<p className="mt-1 text-muted-foreground text-sm">
Set up your backend in four simple steps
</p>
<Accordion
className="mt-6 space-y-2"
onValueChange={(value) => setOpenItem(value[0] ?? "")}
value={openItem ? [openItem] : []}
>
{steps.map((step, index) => {
let status: "complete" | "current" | "upcoming" = "upcoming"
if (index < activeStep) {
status = "complete"
} else if (index === activeStep) {
status = "current"
}
return (
<AccordionItem
className="!border-b rounded-lg border shadow-xs"
key={step.title}
value={step.title}
>
<AccordionTrigger className="px-4 hover:no-underline">
<div className="flex items-center space-x-2">
{status === "complete" ? (
<span
aria-hidden
className="flex size-5 items-center justify-center"
>
<CircleCheckIcon
fill="currentColor"
aria-hidden
className="[&>path]:stroke-background size-6 shrink-0 text-foreground"
/>
</span>
) : (
<span
aria-hidden
className="size-5 shrink-0 rounded-full border border-border"
/>
)}
<p
className={cn(
"font-medium text-sm",
status === "upcoming"
? "text-muted-foreground/60"
: "text-foreground"
)}
>
{step.title}
</p>
</div>
</AccordionTrigger>
<AccordionContent className="px-4">
<div className="flex items-center justify-center rounded-md bg-muted px-4 py-5 ring-1 ring-ring ring-inset">
<div className="max-w-xs text-center">
<step.icon
aria-hidden
className="mx-auto size-7 shrink-0 text-muted-foreground"
/>
<p className="mt-4 font-semibold text-muted-foreground text-sm">
{step.subtitle}
</p>
<p className="mt-1 text-muted-foreground text-sm">
{step.description}
</p>
{status === "complete" ? (
<Button className="mt-6" size="sm" variant="outline">
{step.buttonText}
</Button>
) : (
<Button
className="mt-6"
disabled={status === "upcoming"}
onClick={() => {
if (status === "current") {
handleComplete()
}
}}
size="sm"
>
{step.buttonText}
</Button>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
)
})}
</Accordion>
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/onboarding-04.jsonOnboarding Activity Feed
import { cn } from "@/lib/utils"
const steps = [
{
id: 1,
type: "created",
description: 'Created project "Brand Refresh"',
user: { name: "Sarah K.", initial: "S", bgColor: "bg-violet-500" },
activityTime: "3d ago",
},
{
id: 2,
type: "created",
description: "Uploaded 12 design assets",
user: { name: "Marcus L.", initial: "M", bgColor: "bg-orange-500" },
activityTime: "2d ago",
},
{
id: 3,
type: "created",
description: "Shared prototype with stakeholders",
user: { name: "Sarah K.", initial: "S", bgColor: "bg-emerald-500" },
activityTime: "2h ago",
},
{
id: 4,
type: "created",
description: "Left feedback on homepage layout",
user: { name: "Jamie R.", initial: "J", bgColor: "bg-fuchsia-500" },
activityTime: "5min ago",
},
{
id: 5,
type: "in progress",
description: "Scheduled design review meeting",
user: { name: "Team Ops", initial: "T", bgColor: "bg-blue-500" },
activityTime: "today 2:30pm",
},
]
export default function Onboarding05() {
return (
<div className="flex min-h-dvh items-center justify-center bg-background p-4">
<div className="sm:max-w-lg md:mx-auto">
<h3 className="font-medium text-foreground">Project activity</h3>
<p className="mt-1 text-muted-foreground text-sm leading-6">
Recent updates from your team
</p>
<ul className="mt-6 space-y-6 pb-2">
{steps.map((step, stepIdx) => (
<li className="relative flex gap-x-3" key={step.id}>
<div
className={cn(
"absolute top-0 left-0 flex w-6 justify-center",
stepIdx === steps.length - 1 ? "h-6" : "-bottom-6"
)}
>
<span aria-hidden className="w-px bg-border" />
</div>
<div className="flex items-start space-x-2">
<div className="flex items-center space-x-2">
<div className="relative flex size-6 flex-none items-center justify-center bg-background">
{step.type === "created" ? (
<div className="size-3 rounded-full border border-gray-300 bg-muted/50 ring-4 ring-background" />
) : (
<div className="size-3 rounded-full border border-gray-300 bg-background ring-4 ring-background" />
)}
</div>
<span
aria-hidden
className={cn(
step.user.bgColor,
"inline-flex size-6 flex-none items-center justify-center rounded-full text-primary-foreground text-xs"
)}
>
{step.user.initial}
</span>
</div>
<p className="mt-0.5 font-medium text-foreground text-sm">
{step.user.name}
<span className="font-normal text-muted-foreground">
{" "}
{step.description}
</span>
<span className="font-normal text-muted-foreground/60">
{" "}
∙ {step.activityTime}
</span>
</p>
</div>
</li>
))}
</ul>
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/onboarding-05.jsonOnboarding Timeline
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
const steps = [
{
id: 1,
type: "done",
title: "Repository connected",
description: "Linked GitHub repository to deployment pipeline",
activityTime: "3d ago",
},
{
id: 2,
type: "done",
title: "Build configuration set",
description: "Configured build commands and environment variables",
activityTime: "2d ago",
},
{
id: 3,
type: "done",
title: "Domain configured",
description: "Custom domain verified and SSL certificate issued",
activityTime: "31min ago",
},
{
id: 4,
type: "in progress",
title: "Running health checks",
description: "Verifying all endpoints respond correctly",
activityTime: "Running now...",
},
{
id: 5,
type: "open",
title: "Go live",
description: "Switch traffic to the new deployment",
activityTime: "Upcoming",
},
]
export default function Onboarding06() {
return (
<div className="flex min-h-dvh items-center justify-center bg-background p-4">
<div className="sm:mx-auto sm:max-w-lg">
<h3 className="font-medium text-foreground">Deployment progress</h3>
<ul className="mt-6 space-y-6">
{steps.map((step, stepIdx) => (
<li className="relative flex gap-x-3" key={step.id}>
<div
className={cn(
"absolute top-0 left-0 flex w-6 justify-center",
stepIdx === steps.length - 1 ? "h-6" : "-bottom-6"
)}
>
<span aria-hidden className="w-px bg-border" />
</div>
<div className="flex items-start space-x-2.5">
<div className="relative flex size-6 flex-none items-center justify-center bg-background">
{step.type === "done" ? (
<CheckIcon aria-hidden className="size-5 text-primary" />
) : step.type === "in progress" ? (
<div
aria-hidden
className="size-2.5 rounded-full bg-primary ring-4 ring-background"
/>
) : (
<div
aria-hidden
className="size-3 rounded-full border border-border bg-background ring-4 ring-background"
/>
)}
</div>
<div>
<p className="mt-0.5 font-medium text-foreground text-sm">
{step.title}{" "}
<span className="font-normal text-muted-foreground/60">
∙ {step.activityTime}
</span>
</p>
<p className="mt-0.5 text-muted-foreground text-sm leading-6">
{step.description}
</p>
</div>
</div>
</li>
))}
</ul>
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/onboarding-06.jsonOnboarding Deploy Pipeline
"use client"
import { CircleCheckIcon, LoaderCircleIcon, RefreshCwIcon } from "lucide-react"
import { useCallback, useEffect, useRef, useState } from "react"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { Button } from "@/components/ui/button"
import { Progress } from "@/components/ui/progress"
import { cn } from "@/lib/utils"
const initialSteps = [
{
id: 1,
type: "created" as const,
description: "Export records",
value: 100,
createdOn: "2024-03-10 09:32",
runTime: "12min 18s",
},
{
id: 2,
type: "created" as const,
description: "Transform schema",
value: 100,
createdOn: "2024-03-10 10:03",
runTime: "18min 45s",
},
{
id: 3,
type: "in progress" as const,
description: "Import to new system",
value: 45,
createdOn: null as string | null,
runTime: null as string | null,
},
]
export default function Onboarding07() {
const [steps, setSteps] = useState(initialSteps)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const allComplete = steps.every((s) => s.value === 100)
const startAnimation = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
}
intervalRef.current = setInterval(() => {
setSteps((prev) => {
const next = prev.map((step) => {
if (step.id !== 3 || step.value >= 100) {
return step
}
const increment = Math.random() * 0.8 + 0.3
const newValue = Math.min(step.value + increment, 100)
if (newValue >= 100) {
return {
...step,
value: 100,
type: "created" as const,
createdOn: "2024-03-10 10:47",
runTime: "6min 32s",
}
}
return { ...step, value: newValue }
})
if (next[2].value >= 100 && intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
return next
})
}, 50)
}, [])
useEffect(() => {
startAnimation()
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
}
}
}, [startAnimation])
const handleRunAgain = () => {
setSteps(initialSteps)
startAnimation()
}
return (
<div className="flex min-h-dvh items-center justify-center bg-background p-4">
<div className="w-full max-w-xl">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-foreground">Data Migration</h3>
<Button
className={
allComplete ? "opacity-100" : "pointer-events-none opacity-0"
}
onClick={handleRunAgain}
size="icon"
variant="secondary"
>
<RefreshCwIcon aria-hidden="true" className="size-4" />
</Button>
</div>
<p className="mt-1 text-muted-foreground text-sm leading-6">
Migrating records from your legacy system
</p>
<div className="mt-6 flex items-center space-x-2">
{steps.map((step) => (
<div className="w-full truncate" key={step.id}>
<Progress className="h-1.5" value={step.value} />
<div className="mt-2 flex items-center space-x-1 truncate">
{step.value === 100 ? (
<CircleCheckIcon
fill="currentColor"
aria-hidden={true}
className="[&>path]:stroke-background size-4 shrink-0 text-primary"
/>
) : (
<LoaderCircleIcon
aria-hidden={true}
className="size-4 shrink-0 animate-spin text-primary"
/>
)}
<p className="truncate text-muted-foreground text-xs">
{step.description}
</p>
</div>
</div>
))}
</div>
<Accordion className="mt-8" defaultValue={["logs"]}>
<AccordionItem className="rounded-sm border-b-0" value="logs">
<AccordionTrigger className="font-medium text-foreground text-sm">
Logs overview ({steps.length})
</AccordionTrigger>
<AccordionContent>
<ul className="mt-2 space-y-6 pb-2">
{steps.map((step, stepIdx) => (
<li className="relative flex gap-x-3" key={step.id}>
<div
className={cn(
"absolute top-0 left-0 flex w-6 justify-center",
stepIdx === steps.length - 1 ? "h-6" : "-bottom-6"
)}
>
<span aria-hidden={true} className="w-px bg-border" />
</div>
<div className="flex items-start space-x-2.5">
<div className="relative flex size-6 flex-none items-center justify-center bg-background">
{step.type === "created" ? (
<div className="size-3 rounded-full border border-border bg-muted/50 ring-4 ring-background" />
) : (
<div className="size-3 rounded-full border border-border bg-background ring-4 ring-background" />
)}
</div>
<div>
<p className="mt-0.5 font-medium text-foreground text-sm">
{step.description}
</p>
{step.type === "created" ? (
<p className="text-muted-foreground text-sm leading-6">
Created on {step.createdOn}, Runtime: {step.runTime}
</p>
) : (
<p className="text-muted-foreground text-sm leading-6">
12,847 of 28,500 records processed...
</p>
)}
</div>
</div>
</li>
))}
</ul>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/onboarding-07.json