improve onboarding flow

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-05-17 23:38:29 +02:00
parent 1876c19d01
commit 2882498568
6 changed files with 121 additions and 19 deletions

View File

@@ -1,7 +1,12 @@
'use client';
import { useEffect } from 'react';
import { ButtonContainer } from '@/components/button-container';
import { InputWithLabel } from '@/components/forms/input-with-label';
import { Alert } from '@/components/ui/alert';
import { LinkButton } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { KeyIcon, LockIcon } from 'lucide-react';
import type { IServiceProjectWithClients } from '@openpanel/db';
@@ -32,6 +37,18 @@ const Connect = ({ project }: Props) => {
</OnboardingDescription>
}
>
<div className="flex flex-col gap-4 rounded-xl border bg-slate-100 p-4 md:p-6">
<div className="flex items-center gap-2 text-2xl capitalize">
<LockIcon />
Credentials
</div>
<InputWithLabel label="Client ID" disabled value={client.id} />
<InputWithLabel
label="Client Secret"
disabled
value={client.secret ?? 'unknown'}
/>
</div>
{project.types.map((type) => {
const Component = {
website: ConnectWeb,

View File

@@ -6,21 +6,33 @@ import { ButtonContainer } from '@/components/button-container';
import { CheckboxItem } from '@/components/forms/checkbox-item';
import { InputWithLabel } from '@/components/forms/input-with-label';
import { Button } from '@/components/ui/button';
import { Combobox } from '@/components/ui/combobox';
import { Label } from '@/components/ui/label';
import { api, handleError } from '@/trpc/client';
import { zodResolver } from '@hookform/resolvers/zod';
import { MonitorIcon, ServerIcon, SmartphoneIcon } from 'lucide-react';
import {
Building,
MonitorIcon,
ServerIcon,
SmartphoneIcon,
} from 'lucide-react';
import { useRouter } from 'next/navigation';
import type { SubmitHandler } from 'react-hook-form';
import { Controller, useForm, useWatch } from 'react-hook-form';
import type { z } from 'zod';
import type { IServiceOrganization } from '@openpanel/db';
import { zOnboardingProject } from '@openpanel/validation';
import OnboardingLayout, { OnboardingDescription } from '../onboarding-layout';
type IForm = z.infer<typeof zOnboardingProject>;
const Tracking = () => {
const Tracking = ({
organizations,
}: {
organizations: IServiceOrganization[];
}) => {
const router = useRouter();
const mutation = api.onboarding.project.useMutation({
onError: handleError,
@@ -81,13 +93,43 @@ const Tracking = () => {
}
>
<div className="grid gap-4 md:grid-cols-2">
<InputWithLabel
label="Workspace name"
info="This is the name of your workspace. It can be anything you like."
placeholder="Eg. The Music Company"
error={form.formState.errors.organization?.message}
{...form.register('organization')}
/>
{organizations.length > 0 ? (
<Controller
control={form.control}
name="organizationSlug"
render={({ field, formState }) => {
return (
<div>
<Label>Workspace name</Label>
<Combobox
className="w-full"
placeholder="Select workspace"
icon={Building}
error={formState.errors.organizationSlug?.message}
value={field.value}
items={
organizations
.filter((item) => item.slug)
.map((item) => ({
label: item.name,
value: item.slug,
})) ?? []
}
onChange={field.onChange}
/>
</div>
);
}}
/>
) : (
<InputWithLabel
label="Workspace name"
info="This is the name of your workspace. It can be anything you like."
placeholder="Eg. The Music Company"
error={form.formState.errors.organization?.message}
{...form.register('organization')}
/>
)}
<InputWithLabel
label="Project name"
placeholder="Eg. The Music App"

View File

@@ -1,7 +1,9 @@
import { getCurrentOrganizations } from '@openpanel/db';
import OnboardingTracking from './onboarding-tracking';
const Tracking = () => {
return <OnboardingTracking />;
const Tracking = async () => {
return <OnboardingTracking organizations={await getCurrentOrganizations()} />;
};
export default Tracking;

View File

@@ -37,6 +37,7 @@ export interface ComboboxProps<T> {
label?: string;
align?: 'start' | 'end' | 'center';
portal?: boolean;
error?: string;
}
export type ExtendedComboboxProps<T> = Omit<
@@ -59,6 +60,7 @@ export function Combobox<T extends string>({
size,
align = 'start',
portal,
error,
}: ComboboxProps<T>) {
const [open, setOpen] = React.useState(false);
const [search, setSearch] = React.useState('');
@@ -77,7 +79,11 @@ export function Combobox<T extends string>({
variant="outline"
role="combobox"
aria-expanded={open}
className={cn('justify-between', className)}
className={cn(
'justify-between',
!!error && 'border-destructive',
className
)}
>
<div className="flex min-w-0 items-center">
{Icon ? <Icon size={16} className="mr-2 shrink-0" /> : null}

View File

@@ -1,5 +1,6 @@
import { randomUUID } from 'crypto';
import { clerkClient } from '@clerk/fastify';
import type { z } from 'zod';
import { hashPassword, slug, stripTrailingSlash } from '@openpanel/common';
import { db, getId } from '@openpanel/db';
@@ -8,6 +9,27 @@ import { zOnboardingProject } from '@openpanel/validation';
import { createTRPCRouter, protectedProcedure } from '../trpc';
async function createOrGetOrganization(
input: z.infer<typeof zOnboardingProject>,
userId: string
) {
if (input.organizationSlug) {
return await clerkClient.organizations.getOrganization({
slug: input.organizationSlug,
});
}
if (input.organization) {
return await clerkClient.organizations.createOrganization({
name: input.organization,
slug: slug(input.organization),
createdBy: userId,
});
}
return null;
}
export const onboardingRouter = createTRPCRouter({
project: protectedProcedure
.input(zOnboardingProject)
@@ -17,13 +39,12 @@ export const onboardingRouter = createTRPCRouter({
if (input.app) types.push('app');
if (input.backend) types.push('backend');
const organization = await clerkClient.organizations.createOrganization({
name: input.organization,
slug: slug(input.organization),
createdBy: ctx.session.userId,
});
const organization = await createOrGetOrganization(
input,
ctx.session.userId
);
if (!organization.slug) {
if (!organization?.slug) {
throw new Error('Organization slug is missing');
}

View File

@@ -100,7 +100,8 @@ export const zCreateReference = z.object({
export const zOnboardingProject = z
.object({
organization: z.string().min(3),
organization: z.string().optional(),
organizationSlug: z.string().optional(),
project: z.string().min(3),
domain: z.string().url().or(z.literal('').or(z.null())),
website: z.boolean(),
@@ -108,6 +109,19 @@ export const zOnboardingProject = z
backend: z.boolean(),
})
.superRefine((data, ctx) => {
if (!data.organization && !data.organizationSlug) {
ctx.addIssue({
code: 'custom',
message: 'Organization is required',
path: ['organization'],
});
ctx.addIssue({
code: 'custom',
message: 'Organization is required',
path: ['organizationSlug'],
});
}
if (data.website && !data.domain) {
ctx.addIssue({
code: 'custom',