fix: improve onboarding
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { z } from 'zod';
|
||||
import { Or } from '@/components/auth/or';
|
||||
import { SignInEmailForm } from '@/components/auth/sign-in-email-form';
|
||||
import { SignInGithub } from '@/components/auth/sign-in-github';
|
||||
import { SignInGoogle } from '@/components/auth/sign-in-google';
|
||||
import { LogoSquare } from '@/components/logo';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { PAGE_TITLES, createTitle } from '@/utils/title';
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { z } from 'zod';
|
||||
import { useCookieStore } from '@/hooks/use-cookie-store';
|
||||
import { createTitle, PAGE_TITLES } from '@/utils/title';
|
||||
|
||||
export const Route = createFileRoute('/_login/login')({
|
||||
component: LoginPage,
|
||||
@@ -25,16 +25,20 @@ export const Route = createFileRoute('/_login/login')({
|
||||
|
||||
function LoginPage() {
|
||||
const { error, correlationId } = Route.useSearch();
|
||||
const [lastProvider] = useCookieStore<null | string>(
|
||||
'last-auth-provider',
|
||||
null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="col gap-8 w-full text-left">
|
||||
<div className="col w-full gap-8 text-left">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground mb-2">Sign in</h1>
|
||||
<h1 className="mb-2 font-bold text-3xl text-foreground">Sign in</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Don't have an account?{' '}
|
||||
<a
|
||||
className="font-medium text-foreground underline"
|
||||
href="/onboarding"
|
||||
className="underline font-medium text-foreground"
|
||||
>
|
||||
Create one today
|
||||
</a>
|
||||
@@ -42,8 +46,8 @@ function LoginPage() {
|
||||
</div>
|
||||
{error && (
|
||||
<Alert
|
||||
className="mb-6 border-destructive/20 bg-destructive/10 text-left"
|
||||
variant="destructive"
|
||||
className="text-left bg-destructive/10 border-destructive/20 mb-6"
|
||||
>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
@@ -55,7 +59,7 @@ function LoginPage() {
|
||||
<p className="mt-2">
|
||||
Contact us if you have any issues.{' '}
|
||||
<a
|
||||
className="underline font-medium"
|
||||
className="font-medium underline"
|
||||
href={`mailto:hello@openpanel.dev?subject=Login%20Issue%20-%20Correlation%20ID%3A%20${correlationId}`}
|
||||
>
|
||||
hello[at]openpanel.dev
|
||||
@@ -68,11 +72,11 @@ function LoginPage() {
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<SignInGoogle type="sign-in" />
|
||||
<SignInGithub type="sign-in" />
|
||||
<SignInGoogle isLastUsed={lastProvider === 'google'} type="sign-in" />
|
||||
<SignInGithub isLastUsed={lastProvider === 'github'} type="sign-in" />
|
||||
</div>
|
||||
<Or />
|
||||
<SignInEmailForm />
|
||||
<SignInEmailForm isLastUsed={lastProvider === 'email'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
import { LockIcon, XIcon } from 'lucide-react';
|
||||
import { CopyIcon, DownloadIcon, LockIcon, XIcon } from 'lucide-react';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import CopyInput from '@/components/forms/copy-input';
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import FullPageLoadingState from '@/components/full-page-loading-state';
|
||||
import ConnectApp from '@/components/onboarding/connect-app';
|
||||
import ConnectBackend from '@/components/onboarding/connect-backend';
|
||||
import ConnectWeb from '@/components/onboarding/connect-web';
|
||||
import { LinkButton } from '@/components/ui/button';
|
||||
import Syntax from '@/components/syntax';
|
||||
import { Button, LinkButton } from '@/components/ui/button';
|
||||
import { useClientSecret } from '@/hooks/use-client-secret';
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { clipboard } from '@/utils/clipboard';
|
||||
import { createEntityTitle, PAGE_TITLES } from '@/utils/title';
|
||||
|
||||
export const Route = createFileRoute('/_steps/onboarding/$projectId/connect')({
|
||||
@@ -19,7 +18,7 @@ export const Route = createFileRoute('/_steps/onboarding/$projectId/connect')({
|
||||
{ title: createEntityTitle('Connect data', PAGE_TITLES.ONBOARDING) },
|
||||
],
|
||||
}),
|
||||
beforeLoad: async ({ context }) => {
|
||||
beforeLoad: ({ context }) => {
|
||||
if (!context.session?.session) {
|
||||
throw redirect({ to: '/onboarding' });
|
||||
}
|
||||
@@ -54,27 +53,55 @@ function Component() {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="col gap-8 p-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2 font-bold text-xl capitalize">
|
||||
<LockIcon className="size-4" />
|
||||
Credentials
|
||||
</div>
|
||||
<CopyInput label="Client ID" value={client.id} />
|
||||
<CopyInput label="Secret" value={secret} />
|
||||
</div>
|
||||
<div className="-mx-4 h-px bg-muted" />
|
||||
{project?.types?.map((type) => {
|
||||
const Component = {
|
||||
website: ConnectWeb,
|
||||
app: ConnectApp,
|
||||
backend: ConnectBackend,
|
||||
}[type];
|
||||
const credentials = `CLIENT_ID=${client.id}\nCLIENT_SECRET=${secret}`;
|
||||
const download = () => {
|
||||
const blob = new Blob([credentials], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'credentials.txt';
|
||||
a.click();
|
||||
};
|
||||
|
||||
return <Component client={{ ...client, secret }} key={type} />;
|
||||
})}
|
||||
<ButtonContainer>
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div className="scrollbar-thin flex-1 overflow-y-auto">
|
||||
<div className="col gap-4 p-4">
|
||||
<div className="col gap-2">
|
||||
<div className="row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2 font-bold text-xl capitalize">
|
||||
<LockIcon className="size-4" />
|
||||
Client credentials
|
||||
</div>
|
||||
<div className="row gap-2">
|
||||
<Button
|
||||
icon={CopyIcon}
|
||||
onClick={() => clipboard(credentials)}
|
||||
variant="outline"
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
<Button
|
||||
icon={DownloadIcon}
|
||||
onClick={() => download()}
|
||||
variant="outline"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Syntax
|
||||
className="border"
|
||||
code={`CLIENT_ID=${client.id}\nCLIENT_SECRET=${secret}`}
|
||||
copyable={false}
|
||||
language="bash"
|
||||
/>
|
||||
</div>
|
||||
<div className="-mx-4 h-px bg-muted" />
|
||||
<ConnectWeb client={{ ...client, secret }} />
|
||||
</div>
|
||||
</div>
|
||||
<ButtonContainer className="mt-0 flex-shrink-0 border-t bg-background p-4">
|
||||
<div />
|
||||
<LinkButton
|
||||
className="min-w-28 self-start"
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useEffect, useState } from 'react';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { FullPageEmptyState } from '@/components/full-page-empty-state';
|
||||
import FullPageLoadingState from '@/components/full-page-loading-state';
|
||||
import { CurlPreview } from '@/components/onboarding/curl-preview';
|
||||
import VerifyListener from '@/components/onboarding/onboarding-verify-listener';
|
||||
import { VerifyFaq } from '@/components/onboarding/verify-faq';
|
||||
import { LinkButton } from '@/components/ui/button';
|
||||
import { useTRPC } from '@/integrations/trpc/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -16,7 +16,7 @@ export const Route = createFileRoute('/_steps/onboarding/$projectId/verify')({
|
||||
head: () => ({
|
||||
meta: [{ title: createEntityTitle('Verify', PAGE_TITLES.ONBOARDING) }],
|
||||
}),
|
||||
beforeLoad: async ({ context }) => {
|
||||
beforeLoad: ({ context }) => {
|
||||
if (!context.session?.session) {
|
||||
throw redirect({ to: '/onboarding' });
|
||||
}
|
||||
@@ -61,20 +61,23 @@ function Component() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="col gap-8 p-4">
|
||||
<VerifyListener
|
||||
client={client}
|
||||
events={events?.data ?? []}
|
||||
onVerified={() => {
|
||||
refetch();
|
||||
setIsVerified(true);
|
||||
}}
|
||||
project={project}
|
||||
/>
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div className="scrollbar-thin flex-1 overflow-y-auto">
|
||||
<div className="col gap-8 p-4">
|
||||
<VerifyListener
|
||||
client={client}
|
||||
events={events?.data ?? []}
|
||||
onVerified={() => {
|
||||
refetch();
|
||||
setIsVerified(true);
|
||||
}}
|
||||
project={project}
|
||||
/>
|
||||
|
||||
<CurlPreview project={project} />
|
||||
|
||||
<ButtonContainer>
|
||||
<VerifyFaq project={project} />
|
||||
</div>
|
||||
</div>
|
||||
<ButtonContainer className="mt-0 flex-shrink-0 border-t bg-background p-4">
|
||||
<LinkButton
|
||||
className="min-w-28 self-start"
|
||||
href={`/onboarding/${project.id}/connect`}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ServerIcon,
|
||||
SmartphoneIcon,
|
||||
} from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Controller,
|
||||
type SubmitHandler,
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
import { z } from 'zod';
|
||||
import AnimateHeight from '@/components/animate-height';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { CheckboxItem } from '@/components/forms/checkbox-item';
|
||||
import { InputWithLabel, WithLabel } from '@/components/forms/input-with-label';
|
||||
import TagInput from '@/components/forms/tag-input';
|
||||
import FullPageLoadingState from '@/components/full-page-loading-state';
|
||||
@@ -27,6 +26,7 @@ import { Combobox } from '@/components/ui/combobox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useClientSecret } from '@/hooks/use-client-secret';
|
||||
import { handleError, useTRPC } from '@/integrations/trpc/react';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
const validateSearch = z.object({
|
||||
inviteId: z.string().optional(),
|
||||
@@ -34,7 +34,7 @@ const validateSearch = z.object({
|
||||
export const Route = createFileRoute('/_steps/onboarding/project')({
|
||||
component: Component,
|
||||
validateSearch,
|
||||
beforeLoad: async ({ context }) => {
|
||||
beforeLoad: ({ context }) => {
|
||||
if (!context.session?.session) {
|
||||
throw redirect({ to: '/onboarding' });
|
||||
}
|
||||
@@ -105,10 +105,18 @@ function Component() {
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const domain = useWatch({
|
||||
name: 'domain',
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const [showCorsInput, setShowCorsInput] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWebsite) {
|
||||
form.setValue('domain', null);
|
||||
form.setValue('cors', []);
|
||||
setShowCorsInput(false);
|
||||
}
|
||||
}, [isWebsite, form]);
|
||||
|
||||
@@ -121,8 +129,11 @@ function Component() {
|
||||
}, [isWebsite, isApp, isBackend]);
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="p-4">
|
||||
<form
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="scrollbar-thin flex-1 overflow-y-auto p-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{organizations.length > 0 ? (
|
||||
<Controller
|
||||
@@ -174,6 +185,7 @@ function Component() {
|
||||
}))}
|
||||
onChange={field.onChange}
|
||||
placeholder="Select timezone"
|
||||
searchable
|
||||
value={field.value}
|
||||
/>
|
||||
</WithLabel>
|
||||
@@ -183,115 +195,142 @@ function Component() {
|
||||
)}
|
||||
<InputWithLabel
|
||||
error={form.formState.errors.project?.message}
|
||||
label="Project name"
|
||||
label="Your first project name"
|
||||
placeholder="Eg. The Music App"
|
||||
{...form.register('project')}
|
||||
className="col-span-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col divide-y">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="website"
|
||||
render={({ field }) => (
|
||||
<CheckboxItem
|
||||
description="Track events and conversion for your website"
|
||||
disabled={isApp}
|
||||
error={form.formState.errors.website?.message}
|
||||
Icon={MonitorIcon}
|
||||
label="Website"
|
||||
{...field}
|
||||
<div className="mt-4">
|
||||
<Label className="mb-2">What are you tracking?</Label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{
|
||||
key: 'website' as const,
|
||||
label: 'Website',
|
||||
Icon: MonitorIcon,
|
||||
active: isWebsite,
|
||||
},
|
||||
{
|
||||
key: 'app' as const,
|
||||
label: 'App',
|
||||
Icon: SmartphoneIcon,
|
||||
active: isApp,
|
||||
},
|
||||
{
|
||||
key: 'backend' as const,
|
||||
label: 'Backend / API',
|
||||
Icon: ServerIcon,
|
||||
active: isBackend,
|
||||
},
|
||||
].map(({ key, label, Icon, active }) => (
|
||||
<button
|
||||
className={cn(
|
||||
'flex flex-col items-center gap-2 rounded-lg border-2 p-4 transition-colors',
|
||||
active
|
||||
? 'border-primary bg-primary/5 text-primary'
|
||||
: 'border-border text-muted-foreground hover:border-primary/40'
|
||||
)}
|
||||
key={key}
|
||||
onClick={() => {
|
||||
form.setValue(key, !active, { shouldValidate: true });
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<AnimateHeight open={isWebsite && !isApp}>
|
||||
<div className="p-4 pl-14">
|
||||
<InputWithLabel
|
||||
label="Domain"
|
||||
placeholder="Your website address"
|
||||
{...form.register('domain')}
|
||||
className="mb-4"
|
||||
error={form.formState.errors.domain?.message}
|
||||
onBlur={(e) => {
|
||||
const value = e.target.value.trim();
|
||||
if (
|
||||
value.includes('.') &&
|
||||
form.getValues().cors.length === 0 &&
|
||||
!form.formState.errors.domain
|
||||
) {
|
||||
form.setValue('cors', [value]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Icon size={24} />
|
||||
<span className="font-medium text-sm">{label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{(form.formState.errors.website?.message ||
|
||||
form.formState.errors.app?.message ||
|
||||
form.formState.errors.backend?.message) && (
|
||||
<p className="mt-2 text-destructive text-sm">
|
||||
At least one type must be selected
|
||||
</p>
|
||||
)}
|
||||
<AnimateHeight open={isWebsite}>
|
||||
<div className="mt-4">
|
||||
<InputWithLabel
|
||||
label="Domain"
|
||||
placeholder="example.com"
|
||||
{...form.register('domain')}
|
||||
error={form.formState.errors.domain?.message}
|
||||
onBlur={(e) => {
|
||||
const raw = e.target.value.trim();
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="cors"
|
||||
render={({ field }) => (
|
||||
<WithLabel label="Allowed domains">
|
||||
<TagInput
|
||||
{...field}
|
||||
error={form.formState.errors.cors?.message}
|
||||
onChange={(newValue) => {
|
||||
field.onChange(
|
||||
newValue.map((item) => {
|
||||
const trimmed = item.trim();
|
||||
if (
|
||||
trimmed.startsWith('http://') ||
|
||||
trimmed.startsWith('https://') ||
|
||||
trimmed === '*'
|
||||
) {
|
||||
return trimmed;
|
||||
}
|
||||
return `https://${trimmed}`;
|
||||
})
|
||||
);
|
||||
}}
|
||||
placeholder="Accept events from these domains"
|
||||
renderTag={(tag) =>
|
||||
tag === '*'
|
||||
? 'Accept events from any domains'
|
||||
: tag
|
||||
}
|
||||
value={field.value ?? []}
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</AnimateHeight>
|
||||
</CheckboxItem>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="app"
|
||||
render={({ field }) => (
|
||||
<CheckboxItem
|
||||
description="Track events and conversion for your app"
|
||||
disabled={isWebsite}
|
||||
error={form.formState.errors.app?.message}
|
||||
Icon={SmartphoneIcon}
|
||||
label="App"
|
||||
{...field}
|
||||
const hasProtocol =
|
||||
raw.startsWith('http://') || raw.startsWith('https://');
|
||||
const value = hasProtocol ? raw : `https://${raw}`;
|
||||
|
||||
form.setValue('domain', value, { shouldValidate: true });
|
||||
if (form.getValues().cors.length === 0) {
|
||||
form.setValue('cors', [value]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="backend"
|
||||
render={({ field }) => (
|
||||
<CheckboxItem
|
||||
description="Track events and conversion for your backend / API"
|
||||
error={form.formState.errors.backend?.message}
|
||||
Icon={ServerIcon}
|
||||
label="Backend / API"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{domain && (
|
||||
<>
|
||||
<button
|
||||
className="mt-2 text-muted-foreground text-sm hover:text-foreground"
|
||||
onClick={() => setShowCorsInput((open) => !open)}
|
||||
type="button"
|
||||
>
|
||||
All events from{' '}
|
||||
<span className="font-medium text-foreground">
|
||||
{domain}
|
||||
</span>{' '}
|
||||
will be allowed. Do you want to allow any other?
|
||||
</button>
|
||||
<AnimateHeight open={showCorsInput}>
|
||||
<div className="mt-3">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="cors"
|
||||
render={({ field }) => (
|
||||
<WithLabel label="Allowed domains">
|
||||
<TagInput
|
||||
{...field}
|
||||
error={form.formState.errors.cors?.message}
|
||||
onChange={(newValue: string[]) => {
|
||||
field.onChange(
|
||||
newValue.map((item: string) => {
|
||||
const trimmed = item.trim();
|
||||
if (
|
||||
trimmed.startsWith('http://') ||
|
||||
trimmed.startsWith('https://') ||
|
||||
trimmed === '*'
|
||||
) {
|
||||
return trimmed;
|
||||
}
|
||||
return `https://${trimmed}`;
|
||||
})
|
||||
);
|
||||
}}
|
||||
placeholder="Accept events from these domains"
|
||||
renderTag={(tag: string) =>
|
||||
tag === '*'
|
||||
? 'Accept events from any domains'
|
||||
: tag
|
||||
}
|
||||
value={field.value ?? []}
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</AnimateHeight>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</AnimateHeight>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ButtonContainer className="border-t p-4">
|
||||
<ButtonContainer className="mt-0 flex-shrink-0 border-t bg-background p-4">
|
||||
<div />
|
||||
<Button
|
||||
className="min-w-28 self-start"
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { OnboardingLeftPanel } from '@/components/onboarding-left-panel';
|
||||
import { createFileRoute, Outlet, useMatchRoute } from '@tanstack/react-router';
|
||||
import { SkeletonDashboard } from '@/components/skeleton-dashboard';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { PAGE_TITLES, createEntityTitle } from '@/utils/title';
|
||||
import {
|
||||
Outlet,
|
||||
createFileRoute,
|
||||
redirect,
|
||||
useLocation,
|
||||
useMatchRoute,
|
||||
} from '@tanstack/react-router';
|
||||
import { createEntityTitle, PAGE_TITLES } from '@/utils/title';
|
||||
|
||||
export const Route = createFileRoute('/_steps')({
|
||||
component: OnboardingLayout,
|
||||
@@ -19,13 +12,18 @@ export const Route = createFileRoute('/_steps')({
|
||||
|
||||
function OnboardingLayout() {
|
||||
return (
|
||||
<div className="relative min-h-screen pt-32 pb-8">
|
||||
<div className="relative flex min-h-screen items-center justify-center p-4">
|
||||
<div className="fixed inset-0 hidden md:block">
|
||||
<SkeletonDashboard />
|
||||
<div className="fixed inset-0 z-10 bg-def-100/50" />
|
||||
</div>
|
||||
<div className="relative z-10 border bg-background rounded-lg shadow-xl shadow-muted/50 max-w-xl mx-auto">
|
||||
<Progress />
|
||||
<Outlet />
|
||||
<div className="relative z-10 flex max-h-[calc(100vh-2rem)] w-full max-w-xl flex-col overflow-hidden rounded-lg border bg-background shadow-muted/50 shadow-xl">
|
||||
<div className="sticky top-0 z-10 flex-shrink-0 border-b bg-background">
|
||||
<Progress />
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -53,18 +51,18 @@ function Progress() {
|
||||
// @ts-expect-error
|
||||
from: step.match,
|
||||
fuzzy: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="row gap-4 p-4 border-b justify-between items-center flex-1 w-full">
|
||||
<div className="row w-full flex-shrink-0 items-center justify-between gap-4 p-4">
|
||||
<div className="font-bold">{currentStep?.name ?? 'Onboarding'}</div>
|
||||
<div className="row gap-4">
|
||||
{steps.map((step) => (
|
||||
<div
|
||||
className={cn(
|
||||
'w-10 h-2 rounded-full bg-muted',
|
||||
currentStep === step && 'w-20 bg-primary',
|
||||
'h-2 w-10 rounded-full bg-muted',
|
||||
currentStep === step && 'w-20 bg-primary'
|
||||
)}
|
||||
key={step.match}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user